We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
No invitation link after adding a new user when email server is not configured
<!--

We use GitHub only for bug reports 🐛

Anything else should be posted to https://discuss.redash.io 👫

🚨For support, help & questions use https://discuss.redash.io/c/support
💡For feature requests & ideas use https://discuss.redash.io/c/feature-requests

**Found a security vulnerability?** Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use this PGP key.

-->

### Issue Summary

After #3267 and #3229 admins no longer receive a link to invite users when the server doesn't have an email server configured.

### Steps to Reproduce

1. Open `/users/new` in a server without email configured (e.g.: [preview](https://redash-preview.netlify.com/users/new))
2. Create a new user and check that no link is provided.
3. Open the pending invitation User Profile and notice there's only the "Resend" option (again it leads to nowhere)

Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
The admin should receive a link to send to the user manually.

### Technical details:

* Redash Version: Latest (Redash Preview)
* Browser/OS: --
* How did you install Redash: --


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `redash/handlers/users.py`
Content:
```
1 import re
2 import time
3 from flask import request
4 from flask_restful import abort
5 from flask_login import current_user, login_user
6 from funcy import project
7 from sqlalchemy.exc import IntegrityError
8 from disposable_email_domains import blacklist
9 from funcy import partial
10 
11 from redash import models
12 from redash.permissions import require_permission, require_admin_or_owner, is_admin_or_owner, \
13     require_permission_or_owner, require_admin
14 from redash.handlers.base import BaseResource, require_fields, get_object_or_404, paginate, order_results as _order_results
15 
16 from redash.authentication.account import invite_link_for_user, send_invite_email, send_password_reset_email, send_verify_email
17 from redash.settings import parse_boolean
18 
19 
20 # Ordering map for relationships
21 order_map = {
22     'name': 'name',
23     '-name': '-name',
24     'active_at': 'active_at',
25     '-active_at': '-active_at',
26     'created_at': 'created_at',
27     '-created_at': '-created_at',
28     'groups': 'group_ids',
29     '-groups': '-group_ids',
30 }
31 
32 order_results = partial(
33     _order_results,
34     default_order='-created_at',
35     allowed_orders=order_map,
36 )
37 
38 
39 def invite_user(org, inviter, user):
40     invite_url = invite_link_for_user(user)
41     send_invite_email(inviter, user, invite_url, org)
42 
43 
44 class UserListResource(BaseResource):
45     def get_users(self, disabled, pending, search_term):
46         if disabled:
47             users = models.User.all_disabled(self.current_org)
48         else:
49             users = models.User.all(self.current_org)
50 
51         if pending is not None:
52             users = models.User.pending(users, pending)
53 
54         if search_term:
55             users = models.User.search(users, search_term)
56             self.record_event({
57                 'action': 'search',
58                 'object_type': 'user',
59                 'term': search_term,
60                 'pending': pending,
61             })
62         else:
63             self.record_event({
64                 'action': 'list',
65                 'object_type': 'user',
66                 'pending': pending,
67             })
68 
69         # order results according to passed order parameter,
70         # special-casing search queries where the database
71         # provides an order by search rank
72         return order_results(users, fallback=bool(search_term))
73 
74     @require_permission('list_users')
75     def get(self):
76         page = request.args.get('page', 1, type=int)
77         page_size = request.args.get('page_size', 25, type=int)
78 
79         groups = {group.id: group for group in models.Group.all(self.current_org)}
80 
81         def serialize_user(user):
82             d = user.to_dict()
83             user_groups = []
84             for group_id in set(d['groups']):
85                 group = groups.get(group_id)
86 
87                 if group:
88                     user_groups.append({'id': group.id, 'name': group.name})
89 
90             d['groups'] = user_groups
91 
92             return d
93 
94         search_term = request.args.get('q', '')
95 
96         disabled = request.args.get('disabled', 'false')  # get enabled users by default
97         disabled = parse_boolean(disabled)
98 
99         pending = request.args.get('pending', None)  # get both active and pending by default
100         if pending is not None:
101             pending = parse_boolean(pending)
102 
103         users = self.get_users(disabled, pending, search_term)
104 
105         return paginate(users, page, page_size, serialize_user)
106 
107     @require_admin
108     def post(self):
109         req = request.get_json(force=True)
110         require_fields(req, ('name', 'email'))
111 
112         if '@' not in req['email']:
113             abort(400, message='Bad email address.')
114         name, domain = req['email'].split('@', 1)
115 
116         if domain.lower() in blacklist or domain.lower() == 'qq.com':
117             abort(400, message='Bad email address.')
118 
119         user = models.User(org=self.current_org,
120                            name=req['name'],
121                            email=req['email'],
122                            is_invitation_pending=True,
123                            group_ids=[self.current_org.default_group.id])
124 
125         try:
126             models.db.session.add(user)
127             models.db.session.commit()
128         except IntegrityError as e:
129             if "email" in e.message:
130                 abort(400, message='Email already taken.')
131             abort(500)
132 
133         self.record_event({
134             'action': 'create',
135             'object_id': user.id,
136             'object_type': 'user'
137         })
138 
139         should_send_invitation = 'no_invite' not in request.args
140         if should_send_invitation:
141             invite_user(self.current_org, self.current_user, user)
142 
143         return user.to_dict()
144 
145 
146 class UserInviteResource(BaseResource):
147     @require_admin
148     def post(self, user_id):
149         user = models.User.get_by_id_and_org(user_id, self.current_org)
150         invite_url = invite_user(self.current_org, self.current_user, user)
151 
152         return user.to_dict()
153 
154 
155 class UserResetPasswordResource(BaseResource):
156     @require_admin
157     def post(self, user_id):
158         user = models.User.get_by_id_and_org(user_id, self.current_org)
159         if user.is_disabled:
160             abort(404, message='Not found')
161         reset_link = send_password_reset_email(user)
162 
163         return {
164             'reset_link': reset_link,
165         }
166 
167 
168 class UserRegenerateApiKeyResource(BaseResource):
169     def post(self, user_id):
170         user = models.User.get_by_id_and_org(user_id, self.current_org)
171         if user.is_disabled:
172             abort(404, message='Not found')
173         if not is_admin_or_owner(user_id):
174             abort(403)
175 
176         user.regenerate_api_key()
177         models.db.session.commit()
178 
179         self.record_event({
180             'action': 'regnerate_api_key',
181             'object_id': user.id,
182             'object_type': 'user'
183         })
184 
185         return user.to_dict(with_api_key=True)
186 
187 
188 class UserResource(BaseResource):
189     def get(self, user_id):
190         require_permission_or_owner('list_users', user_id)
191         user = get_object_or_404(models.User.get_by_id_and_org, user_id, self.current_org)
192 
193         self.record_event({
194             'action': 'view',
195             'object_id': user_id,
196             'object_type': 'user',
197         })
198 
199         return user.to_dict(with_api_key=is_admin_or_owner(user_id))
200 
201     def post(self, user_id):
202         require_admin_or_owner(user_id)
203         user = models.User.get_by_id_and_org(user_id, self.current_org)
204 
205         req = request.get_json(True)
206 
207         params = project(req, ('email', 'name', 'password', 'old_password', 'groups'))
208 
209         if 'password' in params and 'old_password' not in params:
210             abort(403, message="Must provide current password to update password.")
211 
212         if 'old_password' in params and not user.verify_password(params['old_password']):
213             abort(403, message="Incorrect current password.")
214 
215         if 'password' in params:
216             user.hash_password(params.pop('password'))
217             params.pop('old_password')
218 
219         if 'groups' in params and not self.current_user.has_permission('admin'):
220             abort(403, message="Must be admin to change groups membership.")
221 
222         if 'email' in params:
223             _, domain = params['email'].split('@', 1)
224 
225             if domain.lower() in blacklist or domain.lower() == 'qq.com':
226                 abort(400, message='Bad email address.')
227 
228         email_changed = 'email' in params and params['email'] != user.email
229         if email_changed:
230             user.is_email_verified = False
231 
232         try:
233             self.update_model(user, params)
234             models.db.session.commit()
235 
236             if email_changed:
237                 send_verify_email(user, self.current_org)
238 
239             # The user has updated their email or password. This should invalidate all _other_ sessions,
240             # forcing them to log in again. Since we don't want to force _this_ session to have to go
241             # through login again, we call `login_user` in order to update the session with the new identity details.
242             if current_user.id == user.id:
243                 login_user(user, remember=True)
244         except IntegrityError as e:
245             if "email" in e.message:
246                 message = "Email already taken."
247             else:
248                 message = "Error updating record"
249 
250             abort(400, message=message)
251 
252         self.record_event({
253             'action': 'edit',
254             'object_id': user.id,
255             'object_type': 'user',
256             'updated_fields': params.keys()
257         })
258 
259         return user.to_dict(with_api_key=is_admin_or_owner(user_id))
260 
261     @require_admin
262     def delete(self, user_id):
263         user = models.User.get_by_id_and_org(user_id, self.current_org)
264         # admin cannot delete self; current user is an admin (`@require_admin`)
265         # so just check user id
266         if user.id == current_user.id:
267             abort(403, message="You cannot delete your own account. "
268                                "Please ask another admin to do this for you.")
269         elif not user.is_invitation_pending:
270             abort(403, message="You cannot delete activated users. "
271                                "Please disable the user instead.")
272         models.db.session.delete(user)
273         models.db.session.commit()
274 
275         return user.to_dict(with_api_key=is_admin_or_owner(user_id))
276 
277 
278 class UserDisableResource(BaseResource):
279     @require_admin
280     def post(self, user_id):
281         user = models.User.get_by_id_and_org(user_id, self.current_org)
282         # admin cannot disable self; current user is an admin (`@require_admin`)
283         # so just check user id
284         if user.id == current_user.id:
285             abort(403, message="You cannot disable your own account. "
286                                "Please ask another admin to do this for you.")
287         user.disable()
288         models.db.session.commit()
289 
290         return user.to_dict(with_api_key=is_admin_or_owner(user_id))
291 
292     @require_admin
293     def delete(self, user_id):
294         user = models.User.get_by_id_and_org(user_id, self.current_org)
295         user.enable()
296         models.db.session.commit()
297 
298         return user.to_dict(with_api_key=is_admin_or_owner(user_id))
299
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/redash/handlers/users.py b/redash/handlers/users.py
--- a/redash/handlers/users.py
+++ b/redash/handlers/users.py
@@ -15,6 +15,7 @@
 
 from redash.authentication.account import invite_link_for_user, send_invite_email, send_password_reset_email, send_verify_email
 from redash.settings import parse_boolean
+from redash import settings
 
 
 # Ordering map for relationships
@@ -36,9 +37,17 @@
 )
 
 
-def invite_user(org, inviter, user):
+def invite_user(org, inviter, user, send_email=True):
+    email_configured = settings.MAIL_DEFAULT_SENDER is not None
+    d = user.to_dict()
+
     invite_url = invite_link_for_user(user)
-    send_invite_email(inviter, user, invite_url, org)
+    if email_configured and send_email:
+        send_invite_email(inviter, user, invite_url, org)
+    else:
+        d['invite_link'] = invite_url
+
+    return d
 
 
 class UserListResource(BaseResource):
@@ -137,19 +146,14 @@
         })
 
         should_send_invitation = 'no_invite' not in request.args
-        if should_send_invitation:
-            invite_user(self.current_org, self.current_user, user)
-
-        return user.to_dict()
+        return invite_user(self.current_org, self.current_user, user, send_email=should_send_invitation)
 
 
 class UserInviteResource(BaseResource):
     @require_admin
     def post(self, user_id):
         user = models.User.get_by_id_and_org(user_id, self.current_org)
-        invite_url = invite_user(self.current_org, self.current_user, user)
-
-        return user.to_dict()
+        return invite_user(self.current_org, self.current_user, user)
 
 
 class UserResetPasswordResource(BaseResource):

{"golden_diff": "diff --git a/redash/handlers/users.py b/redash/handlers/users.py\n--- a/redash/handlers/users.py\n+++ b/redash/handlers/users.py\n@@ -15,6 +15,7 @@\n \n from redash.authentication.account import invite_link_for_user, send_invite_email, send_password_reset_email, send_verify_email\n from redash.settings import parse_boolean\n+from redash import settings\n \n \n # Ordering map for relationships\n@@ -36,9 +37,17 @@\n )\n \n \n-def invite_user(org, inviter, user):\n+def invite_user(org, inviter, user, send_email=True):\n+    email_configured = settings.MAIL_DEFAULT_SENDER is not None\n+    d = user.to_dict()\n+\n     invite_url = invite_link_for_user(user)\n-    send_invite_email(inviter, user, invite_url, org)\n+    if email_configured and send_email:\n+        send_invite_email(inviter, user, invite_url, org)\n+    else:\n+        d['invite_link'] = invite_url\n+\n+    return d\n \n \n class UserListResource(BaseResource):\n@@ -137,19 +146,14 @@\n         })\n \n         should_send_invitation = 'no_invite' not in request.args\n-        if should_send_invitation:\n-            invite_user(self.current_org, self.current_user, user)\n-\n-        return user.to_dict()\n+        return invite_user(self.current_org, self.current_user, user, send_email=should_send_invitation)\n \n \n class UserInviteResource(BaseResource):\n     @require_admin\n     def post(self, user_id):\n         user = models.User.get_by_id_and_org(user_id, self.current_org)\n-        invite_url = invite_user(self.current_org, self.current_user, user)\n-\n-        return user.to_dict()\n+        return invite_user(self.current_org, self.current_user, user)\n \n \n class UserResetPasswordResource(BaseResource):\n", "issue": "No invitation link after adding a new user when email server is not configured\n<!--\r\n\r\nWe use GitHub only for bug reports \ud83d\udc1b\r\n\r\nAnything else should be posted to https://discuss.redash.io \ud83d\udc6b\r\n\r\n\ud83d\udea8For support, help & questions use https://discuss.redash.io/c/support\r\n\ud83d\udca1For feature requests & ideas use https://discuss.redash.io/c/feature-requests\r\n\r\n**Found a security vulnerability?** Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use this PGP key.\r\n\r\n-->\r\n\r\n### Issue Summary\r\n\r\nAfter #3267 and #3229 admins no longer receive a link to invite users when the server doesn't have an email server configured.\r\n\r\n### Steps to Reproduce\r\n\r\n1. Open `/users/new` in a server without email configured (e.g.: [preview](https://redash-preview.netlify.com/users/new))\r\n2. Create a new user and check that no link is provided.\r\n3. Open the pending invitation User Profile and notice there's only the \"Resend\" option (again it leads to nowhere)\r\n\r\nAny other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?\r\nThe admin should receive a link to send to the user manually.\r\n\r\n### Technical details:\r\n\r\n* Redash Version: Latest (Redash Preview)\r\n* Browser/OS: --\r\n* How did you install Redash: --\r\n\n", "before_files": [{"content": "import re\nimport time\nfrom flask import request\nfrom flask_restful import abort\nfrom flask_login import current_user, login_user\nfrom funcy import project\nfrom sqlalchemy.exc import IntegrityError\nfrom disposable_email_domains import blacklist\nfrom funcy import partial\n\nfrom redash import models\nfrom redash.permissions import require_permission, require_admin_or_owner, is_admin_or_owner, \\\n    require_permission_or_owner, require_admin\nfrom redash.handlers.base import BaseResource, require_fields, get_object_or_404, paginate, order_results as _order_results\n\nfrom redash.authentication.account import invite_link_for_user, send_invite_email, send_password_reset_email, send_verify_email\nfrom redash.settings import parse_boolean\n\n\n# Ordering map for relationships\norder_map = {\n    'name': 'name',\n    '-name': '-name',\n    'active_at': 'active_at',\n    '-active_at': '-active_at',\n    'created_at': 'created_at',\n    '-created_at': '-created_at',\n    'groups': 'group_ids',\n    '-groups': '-group_ids',\n}\n\norder_results = partial(\n    _order_results,\n    default_order='-created_at',\n    allowed_orders=order_map,\n)\n\n\ndef invite_user(org, inviter, user):\n    invite_url = invite_link_for_user(user)\n    send_invite_email(inviter, user, invite_url, org)\n\n\nclass UserListResource(BaseResource):\n    def get_users(self, disabled, pending, search_term):\n        if disabled:\n            users = models.User.all_disabled(self.current_org)\n        else:\n            users = models.User.all(self.current_org)\n\n        if pending is not None:\n            users = models.User.pending(users, pending)\n\n        if search_term:\n            users = models.User.search(users, search_term)\n            self.record_event({\n                'action': 'search',\n                'object_type': 'user',\n                'term': search_term,\n                'pending': pending,\n            })\n        else:\n            self.record_event({\n                'action': 'list',\n                'object_type': 'user',\n                'pending': pending,\n            })\n\n        # order results according to passed order parameter,\n        # special-casing search queries where the database\n        # provides an order by search rank\n        return order_results(users, fallback=bool(search_term))\n\n    @require_permission('list_users')\n    def get(self):\n        page = request.args.get('page', 1, type=int)\n        page_size = request.args.get('page_size', 25, type=int)\n\n        groups = {group.id: group for group in models.Group.all(self.current_org)}\n\n        def serialize_user(user):\n            d = user.to_dict()\n            user_groups = []\n            for group_id in set(d['groups']):\n                group = groups.get(group_id)\n\n                if group:\n                    user_groups.append({'id': group.id, 'name': group.name})\n\n            d['groups'] = user_groups\n\n            return d\n\n        search_term = request.args.get('q', '')\n\n        disabled = request.args.get('disabled', 'false')  # get enabled users by default\n        disabled = parse_boolean(disabled)\n\n        pending = request.args.get('pending', None)  # get both active and pending by default\n        if pending is not None:\n            pending = parse_boolean(pending)\n\n        users = self.get_users(disabled, pending, search_term)\n\n        return paginate(users, page, page_size, serialize_user)\n\n    @require_admin\n    def post(self):\n        req = request.get_json(force=True)\n        require_fields(req, ('name', 'email'))\n\n        if '@' not in req['email']:\n            abort(400, message='Bad email address.')\n        name, domain = req['email'].split('@', 1)\n\n        if domain.lower() in blacklist or domain.lower() == 'qq.com':\n            abort(400, message='Bad email address.')\n\n        user = models.User(org=self.current_org,\n                           name=req['name'],\n                           email=req['email'],\n                           is_invitation_pending=True,\n                           group_ids=[self.current_org.default_group.id])\n\n        try:\n            models.db.session.add(user)\n            models.db.session.commit()\n        except IntegrityError as e:\n            if \"email\" in e.message:\n                abort(400, message='Email already taken.')\n            abort(500)\n\n        self.record_event({\n            'action': 'create',\n            'object_id': user.id,\n            'object_type': 'user'\n        })\n\n        should_send_invitation = 'no_invite' not in request.args\n        if should_send_invitation:\n            invite_user(self.current_org, self.current_user, user)\n\n        return user.to_dict()\n\n\nclass UserInviteResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        invite_url = invite_user(self.current_org, self.current_user, user)\n\n        return user.to_dict()\n\n\nclass UserResetPasswordResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        if user.is_disabled:\n            abort(404, message='Not found')\n        reset_link = send_password_reset_email(user)\n\n        return {\n            'reset_link': reset_link,\n        }\n\n\nclass UserRegenerateApiKeyResource(BaseResource):\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        if user.is_disabled:\n            abort(404, message='Not found')\n        if not is_admin_or_owner(user_id):\n            abort(403)\n\n        user.regenerate_api_key()\n        models.db.session.commit()\n\n        self.record_event({\n            'action': 'regnerate_api_key',\n            'object_id': user.id,\n            'object_type': 'user'\n        })\n\n        return user.to_dict(with_api_key=True)\n\n\nclass UserResource(BaseResource):\n    def get(self, user_id):\n        require_permission_or_owner('list_users', user_id)\n        user = get_object_or_404(models.User.get_by_id_and_org, user_id, self.current_org)\n\n        self.record_event({\n            'action': 'view',\n            'object_id': user_id,\n            'object_type': 'user',\n        })\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    def post(self, user_id):\n        require_admin_or_owner(user_id)\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n\n        req = request.get_json(True)\n\n        params = project(req, ('email', 'name', 'password', 'old_password', 'groups'))\n\n        if 'password' in params and 'old_password' not in params:\n            abort(403, message=\"Must provide current password to update password.\")\n\n        if 'old_password' in params and not user.verify_password(params['old_password']):\n            abort(403, message=\"Incorrect current password.\")\n\n        if 'password' in params:\n            user.hash_password(params.pop('password'))\n            params.pop('old_password')\n\n        if 'groups' in params and not self.current_user.has_permission('admin'):\n            abort(403, message=\"Must be admin to change groups membership.\")\n\n        if 'email' in params:\n            _, domain = params['email'].split('@', 1)\n\n            if domain.lower() in blacklist or domain.lower() == 'qq.com':\n                abort(400, message='Bad email address.')\n\n        email_changed = 'email' in params and params['email'] != user.email\n        if email_changed:\n            user.is_email_verified = False\n\n        try:\n            self.update_model(user, params)\n            models.db.session.commit()\n\n            if email_changed:\n                send_verify_email(user, self.current_org)\n\n            # The user has updated their email or password. This should invalidate all _other_ sessions,\n            # forcing them to log in again. Since we don't want to force _this_ session to have to go\n            # through login again, we call `login_user` in order to update the session with the new identity details.\n            if current_user.id == user.id:\n                login_user(user, remember=True)\n        except IntegrityError as e:\n            if \"email\" in e.message:\n                message = \"Email already taken.\"\n            else:\n                message = \"Error updating record\"\n\n            abort(400, message=message)\n\n        self.record_event({\n            'action': 'edit',\n            'object_id': user.id,\n            'object_type': 'user',\n            'updated_fields': params.keys()\n        })\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    @require_admin\n    def delete(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        # admin cannot delete self; current user is an admin (`@require_admin`)\n        # so just check user id\n        if user.id == current_user.id:\n            abort(403, message=\"You cannot delete your own account. \"\n                               \"Please ask another admin to do this for you.\")\n        elif not user.is_invitation_pending:\n            abort(403, message=\"You cannot delete activated users. \"\n                               \"Please disable the user instead.\")\n        models.db.session.delete(user)\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n\nclass UserDisableResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        # admin cannot disable self; current user is an admin (`@require_admin`)\n        # so just check user id\n        if user.id == current_user.id:\n            abort(403, message=\"You cannot disable your own account. \"\n                               \"Please ask another admin to do this for you.\")\n        user.disable()\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    @require_admin\n    def delete(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        user.enable()\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n", "path": "redash/handlers/users.py"}], "after_files": [{"content": "import re\nimport time\nfrom flask import request\nfrom flask_restful import abort\nfrom flask_login import current_user, login_user\nfrom funcy import project\nfrom sqlalchemy.exc import IntegrityError\nfrom disposable_email_domains import blacklist\nfrom funcy import partial\n\nfrom redash import models\nfrom redash.permissions import require_permission, require_admin_or_owner, is_admin_or_owner, \\\n    require_permission_or_owner, require_admin\nfrom redash.handlers.base import BaseResource, require_fields, get_object_or_404, paginate, order_results as _order_results\n\nfrom redash.authentication.account import invite_link_for_user, send_invite_email, send_password_reset_email, send_verify_email\nfrom redash.settings import parse_boolean\nfrom redash import settings\n\n\n# Ordering map for relationships\norder_map = {\n    'name': 'name',\n    '-name': '-name',\n    'active_at': 'active_at',\n    '-active_at': '-active_at',\n    'created_at': 'created_at',\n    '-created_at': '-created_at',\n    'groups': 'group_ids',\n    '-groups': '-group_ids',\n}\n\norder_results = partial(\n    _order_results,\n    default_order='-created_at',\n    allowed_orders=order_map,\n)\n\n\ndef invite_user(org, inviter, user, send_email=True):\n    email_configured = settings.MAIL_DEFAULT_SENDER is not None\n    d = user.to_dict()\n\n    invite_url = invite_link_for_user(user)\n    if email_configured and send_email:\n        send_invite_email(inviter, user, invite_url, org)\n    else:\n        d['invite_link'] = invite_url\n\n    return d\n\n\nclass UserListResource(BaseResource):\n    def get_users(self, disabled, pending, search_term):\n        if disabled:\n            users = models.User.all_disabled(self.current_org)\n        else:\n            users = models.User.all(self.current_org)\n\n        if pending is not None:\n            users = models.User.pending(users, pending)\n\n        if search_term:\n            users = models.User.search(users, search_term)\n            self.record_event({\n                'action': 'search',\n                'object_type': 'user',\n                'term': search_term,\n                'pending': pending,\n            })\n        else:\n            self.record_event({\n                'action': 'list',\n                'object_type': 'user',\n                'pending': pending,\n            })\n\n        # order results according to passed order parameter,\n        # special-casing search queries where the database\n        # provides an order by search rank\n        return order_results(users, fallback=bool(search_term))\n\n    @require_permission('list_users')\n    def get(self):\n        page = request.args.get('page', 1, type=int)\n        page_size = request.args.get('page_size', 25, type=int)\n\n        groups = {group.id: group for group in models.Group.all(self.current_org)}\n\n        def serialize_user(user):\n            d = user.to_dict()\n            user_groups = []\n            for group_id in set(d['groups']):\n                group = groups.get(group_id)\n\n                if group:\n                    user_groups.append({'id': group.id, 'name': group.name})\n\n            d['groups'] = user_groups\n\n            return d\n\n        search_term = request.args.get('q', '')\n\n        disabled = request.args.get('disabled', 'false')  # get enabled users by default\n        disabled = parse_boolean(disabled)\n\n        pending = request.args.get('pending', None)  # get both active and pending by default\n        if pending is not None:\n            pending = parse_boolean(pending)\n\n        users = self.get_users(disabled, pending, search_term)\n\n        return paginate(users, page, page_size, serialize_user)\n\n    @require_admin\n    def post(self):\n        req = request.get_json(force=True)\n        require_fields(req, ('name', 'email'))\n\n        if '@' not in req['email']:\n            abort(400, message='Bad email address.')\n        name, domain = req['email'].split('@', 1)\n\n        if domain.lower() in blacklist or domain.lower() == 'qq.com':\n            abort(400, message='Bad email address.')\n\n        user = models.User(org=self.current_org,\n                           name=req['name'],\n                           email=req['email'],\n                           is_invitation_pending=True,\n                           group_ids=[self.current_org.default_group.id])\n\n        try:\n            models.db.session.add(user)\n            models.db.session.commit()\n        except IntegrityError as e:\n            if \"email\" in e.message:\n                abort(400, message='Email already taken.')\n            abort(500)\n\n        self.record_event({\n            'action': 'create',\n            'object_id': user.id,\n            'object_type': 'user'\n        })\n\n        should_send_invitation = 'no_invite' not in request.args\n        return invite_user(self.current_org, self.current_user, user, send_email=should_send_invitation)\n\n\nclass UserInviteResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        return invite_user(self.current_org, self.current_user, user)\n\n\nclass UserResetPasswordResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        if user.is_disabled:\n            abort(404, message='Not found')\n        reset_link = send_password_reset_email(user)\n\n        return {\n            'reset_link': reset_link,\n        }\n\n\nclass UserRegenerateApiKeyResource(BaseResource):\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        if user.is_disabled:\n            abort(404, message='Not found')\n        if not is_admin_or_owner(user_id):\n            abort(403)\n\n        user.regenerate_api_key()\n        models.db.session.commit()\n\n        self.record_event({\n            'action': 'regnerate_api_key',\n            'object_id': user.id,\n            'object_type': 'user'\n        })\n\n        return user.to_dict(with_api_key=True)\n\n\nclass UserResource(BaseResource):\n    def get(self, user_id):\n        require_permission_or_owner('list_users', user_id)\n        user = get_object_or_404(models.User.get_by_id_and_org, user_id, self.current_org)\n\n        self.record_event({\n            'action': 'view',\n            'object_id': user_id,\n            'object_type': 'user',\n        })\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    def post(self, user_id):\n        require_admin_or_owner(user_id)\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n\n        req = request.get_json(True)\n\n        params = project(req, ('email', 'name', 'password', 'old_password', 'groups'))\n\n        if 'password' in params and 'old_password' not in params:\n            abort(403, message=\"Must provide current password to update password.\")\n\n        if 'old_password' in params and not user.verify_password(params['old_password']):\n            abort(403, message=\"Incorrect current password.\")\n\n        if 'password' in params:\n            user.hash_password(params.pop('password'))\n            params.pop('old_password')\n\n        if 'groups' in params and not self.current_user.has_permission('admin'):\n            abort(403, message=\"Must be admin to change groups membership.\")\n\n        if 'email' in params:\n            _, domain = params['email'].split('@', 1)\n\n            if domain.lower() in blacklist or domain.lower() == 'qq.com':\n                abort(400, message='Bad email address.')\n\n        email_changed = 'email' in params and params['email'] != user.email\n        if email_changed:\n            user.is_email_verified = False\n\n        try:\n            self.update_model(user, params)\n            models.db.session.commit()\n\n            if email_changed:\n                send_verify_email(user, self.current_org)\n\n            # The user has updated their email or password. This should invalidate all _other_ sessions,\n            # forcing them to log in again. Since we don't want to force _this_ session to have to go\n            # through login again, we call `login_user` in order to update the session with the new identity details.\n            if current_user.id == user.id:\n                login_user(user, remember=True)\n        except IntegrityError as e:\n            if \"email\" in e.message:\n                message = \"Email already taken.\"\n            else:\n                message = \"Error updating record\"\n\n            abort(400, message=message)\n\n        self.record_event({\n            'action': 'edit',\n            'object_id': user.id,\n            'object_type': 'user',\n            'updated_fields': params.keys()\n        })\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    @require_admin\n    def delete(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        # admin cannot delete self; current user is an admin (`@require_admin`)\n        # so just check user id\n        if user.id == current_user.id:\n            abort(403, message=\"You cannot delete your own account. \"\n                               \"Please ask another admin to do this for you.\")\n        elif not user.is_invitation_pending:\n            abort(403, message=\"You cannot delete activated users. \"\n                               \"Please disable the user instead.\")\n        models.db.session.delete(user)\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n\nclass UserDisableResource(BaseResource):\n    @require_admin\n    def post(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        # admin cannot disable self; current user is an admin (`@require_admin`)\n        # so just check user id\n        if user.id == current_user.id:\n            abort(403, message=\"You cannot disable your own account. \"\n                               \"Please ask another admin to do this for you.\")\n        user.disable()\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n\n    @require_admin\n    def delete(self, user_id):\n        user = models.User.get_by_id_and_org(user_id, self.current_org)\n        user.enable()\n        models.db.session.commit()\n\n        return user.to_dict(with_api_key=is_admin_or_owner(user_id))\n", "path": "redash/handlers/users.py"}]}