udata 10.1.3.dev34283__py2.py3-none-any.whl → 10.1.3.dev34313__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/core/user/api.py +8 -1
- udata/core/user/models.py +16 -11
- udata/core/user/tasks.py +81 -2
- udata/core/user/tests/test_user_model.py +29 -12
- udata/settings.py +5 -0
- udata/static/chunks/{10.8ca60413647062717b1e.js → 10.471164b2a9fe15614797.js} +3 -3
- udata/static/chunks/{10.8ca60413647062717b1e.js.map → 10.471164b2a9fe15614797.js.map} +1 -1
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.51d706fb9521c16976bc.js} +3 -3
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.51d706fb9521c16976bc.js.map} +1 -1
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.f29411b06be1883356a3.js} +2 -2
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.f29411b06be1883356a3.js.map} +1 -1
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.3bd0340930d4a314ce9c.js} +2 -2
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.3bd0340930d4a314ce9c.js.map} +1 -1
- udata/static/chunks/{19.f03a102365af4315f9db.js → 19.8da42e8359d72afc2618.js} +3 -3
- udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.8da42e8359d72afc2618.js.map} +1 -1
- udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
- udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
- udata/static/chunks/{9.033d7e190ca9e226a5d0.js → 9.07515e5187f475bce828.js} +3 -3
- udata/static/chunks/{9.033d7e190ca9e226a5d0.js.map → 9.07515e5187f475bce828.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/templates/mail/account_inactivity.html +29 -0
- udata/templates/mail/account_inactivity.txt +22 -0
- udata/templates/mail/inactive_account_deleted.html +5 -0
- udata/templates/mail/inactive_account_deleted.txt +6 -0
- udata/tests/api/test_me_api.py +1 -1
- udata/tests/api/test_user_api.py +47 -8
- udata/tests/user/test_user_tasks.py +144 -0
- udata/translations/udata.pot +83 -54
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/METADATA +4 -1
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/RECORD +35 -30
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/LICENSE +0 -0
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/WHEEL +0 -0
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/entry_points.txt +0 -0
- {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/top_level.txt +0 -0
udata/core/user/api.py
CHANGED
|
@@ -275,6 +275,13 @@ delete_parser.add_argument(
|
|
|
275
275
|
location="args",
|
|
276
276
|
default=False,
|
|
277
277
|
)
|
|
278
|
+
delete_parser.add_argument(
|
|
279
|
+
"delete_comments",
|
|
280
|
+
type=bool,
|
|
281
|
+
help="Delete comments posted by the user upon user deletion",
|
|
282
|
+
location="args",
|
|
283
|
+
default=False,
|
|
284
|
+
)
|
|
278
285
|
|
|
279
286
|
|
|
280
287
|
@ns.route("/<user:user>/", endpoint="user")
|
|
@@ -317,7 +324,7 @@ class UserAPI(API):
|
|
|
317
324
|
403, "You cannot delete yourself with this API. " + 'Use the "me" API instead.'
|
|
318
325
|
)
|
|
319
326
|
|
|
320
|
-
user.mark_as_deleted(notify=not args["no_mail"])
|
|
327
|
+
user.mark_as_deleted(notify=not args["no_mail"], delete_comments=args["delete_comments"])
|
|
321
328
|
return "", 204
|
|
322
329
|
|
|
323
330
|
|
udata/core/user/models.py
CHANGED
|
@@ -82,6 +82,10 @@ class User(WithMetrics, UserMixin, db.Document):
|
|
|
82
82
|
ext = db.MapField(db.GenericEmbeddedDocumentField())
|
|
83
83
|
extras = db.ExtrasField()
|
|
84
84
|
|
|
85
|
+
# Used to track notification for automatic inactive users deletion
|
|
86
|
+
# when YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION is set
|
|
87
|
+
inactive_deletion_notified_at = db.DateTimeField()
|
|
88
|
+
|
|
85
89
|
before_save = Signal()
|
|
86
90
|
after_save = Signal()
|
|
87
91
|
on_create = Signal()
|
|
@@ -237,7 +241,7 @@ class User(WithMetrics, UserMixin, db.Document):
|
|
|
237
241
|
raise NotImplementedError("""This method should not be using directly.
|
|
238
242
|
Use `mark_as_deleted` (or `_delete` if you know what you're doing)""")
|
|
239
243
|
|
|
240
|
-
def mark_as_deleted(self, notify: bool = True):
|
|
244
|
+
def mark_as_deleted(self, notify: bool = True, delete_comments: bool = False):
|
|
241
245
|
if self.avatar.filename is not None:
|
|
242
246
|
storage = storages.avatars
|
|
243
247
|
storage.delete(self.avatar.filename)
|
|
@@ -265,16 +269,17 @@ class User(WithMetrics, UserMixin, db.Document):
|
|
|
265
269
|
member for member in organization.members if member.user != self
|
|
266
270
|
]
|
|
267
271
|
organization.save()
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
discussion.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
message.
|
|
277
|
-
|
|
272
|
+
if delete_comments:
|
|
273
|
+
for discussion in Discussion.objects(discussion__posted_by=self):
|
|
274
|
+
# Remove all discussions with current user as only participant
|
|
275
|
+
if all(message.posted_by == self for message in discussion.discussion):
|
|
276
|
+
discussion.delete()
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
for message in discussion.discussion:
|
|
280
|
+
if message.posted_by == self:
|
|
281
|
+
message.content = "DELETED"
|
|
282
|
+
discussion.save()
|
|
278
283
|
Follow.objects(follower=self).delete()
|
|
279
284
|
Follow.objects(following=self).delete()
|
|
280
285
|
|
udata/core/user/tasks.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from copy import copy
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from flask import current_app
|
|
2
6
|
|
|
3
7
|
from udata import mail
|
|
4
8
|
from udata.i18n import lazy_gettext as _
|
|
5
|
-
from udata.tasks import task
|
|
9
|
+
from udata.tasks import job, task
|
|
6
10
|
|
|
7
|
-
from .models import datastore
|
|
11
|
+
from .models import User, datastore
|
|
8
12
|
|
|
9
13
|
log = logging.getLogger(__name__)
|
|
10
14
|
|
|
@@ -13,3 +17,78 @@ log = logging.getLogger(__name__)
|
|
|
13
17
|
def send_test_mail(email):
|
|
14
18
|
user = datastore.find_user(email=email)
|
|
15
19
|
mail.send(_("Test mail"), user, "test")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@job("notify-inactive-users")
|
|
23
|
+
def notify_inactive_users(self):
|
|
24
|
+
if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"]:
|
|
25
|
+
logging.warning(
|
|
26
|
+
"YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION setting is not set, no deletion planned"
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
notification_comparison_date = (
|
|
30
|
+
datetime.utcnow()
|
|
31
|
+
- timedelta(days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"] * 365)
|
|
32
|
+
+ timedelta(days=current_app.config["DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY"])
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
users_to_notify = User.objects(
|
|
36
|
+
deleted=None,
|
|
37
|
+
inactive_deletion_notified_at=None,
|
|
38
|
+
current_login_at__lte=notification_comparison_date,
|
|
39
|
+
)
|
|
40
|
+
for i, user in enumerate(users_to_notify):
|
|
41
|
+
if i >= current_app.config["MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS"]:
|
|
42
|
+
logging.warning("MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS reached, stopping here.")
|
|
43
|
+
return
|
|
44
|
+
mail.send(
|
|
45
|
+
_("Inactivity of your {site} account").format(site=current_app.config["SITE_TITLE"]),
|
|
46
|
+
user,
|
|
47
|
+
"account_inactivity",
|
|
48
|
+
user=user,
|
|
49
|
+
)
|
|
50
|
+
logging.debug(f"Notified {user.email} of account inactivity")
|
|
51
|
+
user.inactive_deletion_notified_at = datetime.utcnow()
|
|
52
|
+
user.save()
|
|
53
|
+
|
|
54
|
+
logging.info(f"Notified {users_to_notify.count()} inactive users")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@job("delete-inactive-users")
|
|
58
|
+
def delete_inactive_users(self):
|
|
59
|
+
if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"]:
|
|
60
|
+
logging.warning(
|
|
61
|
+
"YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION setting is not set, no deletion planned"
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Clear inactive_deletion_notified_at field if user has logged in since notification
|
|
66
|
+
for user in User.objects(deleted=None, inactive_deletion_notified_at__exists=True):
|
|
67
|
+
if user.current_login_at > user.inactive_deletion_notified_at:
|
|
68
|
+
user.inactive_deletion_notified_at = None
|
|
69
|
+
user.save()
|
|
70
|
+
|
|
71
|
+
# Delete inactive users upon notification delay if user still hasn't logged in
|
|
72
|
+
deletion_comparison_date = datetime.utcnow() - timedelta(
|
|
73
|
+
days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"] * 365
|
|
74
|
+
)
|
|
75
|
+
notified_at = datetime.utcnow() - timedelta(
|
|
76
|
+
days=current_app.config["DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY"]
|
|
77
|
+
)
|
|
78
|
+
users_to_delete = User.objects(
|
|
79
|
+
deleted=None,
|
|
80
|
+
current_login_at__lte=deletion_comparison_date,
|
|
81
|
+
inactive_deletion_notified_at__lte=notified_at,
|
|
82
|
+
)
|
|
83
|
+
for user in users_to_delete:
|
|
84
|
+
copied_user = copy(user)
|
|
85
|
+
user.mark_as_deleted(notify=False, delete_comments=False)
|
|
86
|
+
logging.warning(f"Deleted user {copied_user.email} due to account inactivity")
|
|
87
|
+
mail.send(
|
|
88
|
+
_("Deletion of your inactive {site} account").format(
|
|
89
|
+
site=current_app.config["SITE_TITLE"]
|
|
90
|
+
),
|
|
91
|
+
copied_user,
|
|
92
|
+
"inactive_account_deleted",
|
|
93
|
+
)
|
|
94
|
+
logging.info(f"Deleted {users_to_delete.count()} inactive users")
|
|
@@ -18,7 +18,7 @@ class UserModelTest:
|
|
|
18
18
|
user = UserFactory()
|
|
19
19
|
other_user = UserFactory()
|
|
20
20
|
org = OrganizationFactory(editors=[user])
|
|
21
|
-
|
|
21
|
+
discussion = DiscussionFactory(
|
|
22
22
|
user=user,
|
|
23
23
|
subject=org,
|
|
24
24
|
discussion=[
|
|
@@ -26,14 +26,6 @@ class UserModelTest:
|
|
|
26
26
|
MessageDiscussionFactory(posted_by=user),
|
|
27
27
|
],
|
|
28
28
|
)
|
|
29
|
-
discussion_with_other = DiscussionFactory(
|
|
30
|
-
user=other_user,
|
|
31
|
-
subject=org,
|
|
32
|
-
discussion=[
|
|
33
|
-
MessageDiscussionFactory(posted_by=other_user),
|
|
34
|
-
MessageDiscussionFactory(posted_by=user),
|
|
35
|
-
],
|
|
36
|
-
)
|
|
37
29
|
user_follow_org = Follow.objects.create(follower=user, following=org)
|
|
38
30
|
user_followed = Follow.objects.create(follower=other_user, following=user)
|
|
39
31
|
|
|
@@ -42,15 +34,40 @@ class UserModelTest:
|
|
|
42
34
|
org.reload()
|
|
43
35
|
assert len(org.members) == 0
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
assert
|
|
37
|
+
# discussions are kept by default
|
|
38
|
+
discussion.reload()
|
|
39
|
+
assert len(discussion.discussion) == 2
|
|
40
|
+
assert discussion.discussion[1].content != "DELETED"
|
|
48
41
|
|
|
49
42
|
assert Follow.objects(id=user_follow_org.id).first() is None
|
|
50
43
|
assert Follow.objects(id=user_followed.id).first() is None
|
|
51
44
|
|
|
52
45
|
assert user.slug == "deleted"
|
|
53
46
|
|
|
47
|
+
def test_mark_as_deleted_with_comments_deletion(self):
|
|
48
|
+
user = UserFactory()
|
|
49
|
+
other_user = UserFactory()
|
|
50
|
+
discussion_only_user = DiscussionFactory(
|
|
51
|
+
user=user,
|
|
52
|
+
discussion=[
|
|
53
|
+
MessageDiscussionFactory(posted_by=user),
|
|
54
|
+
MessageDiscussionFactory(posted_by=user),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
discussion_with_other = DiscussionFactory(
|
|
58
|
+
user=other_user,
|
|
59
|
+
discussion=[
|
|
60
|
+
MessageDiscussionFactory(posted_by=other_user),
|
|
61
|
+
MessageDiscussionFactory(posted_by=user),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
user.mark_as_deleted(delete_comments=True)
|
|
66
|
+
|
|
67
|
+
assert Discussion.objects(id=discussion_only_user.id).first() is None
|
|
68
|
+
discussion_with_other.reload()
|
|
69
|
+
assert discussion_with_other.discussion[1].content == "DELETED"
|
|
70
|
+
|
|
54
71
|
def test_mark_as_deleted_slug_multiple(self):
|
|
55
72
|
user = UserFactory()
|
|
56
73
|
other_user = UserFactory()
|
udata/settings.py
CHANGED
|
@@ -115,6 +115,11 @@ class Defaults(object):
|
|
|
115
115
|
|
|
116
116
|
SECURITY_RETURN_GENERIC_RESPONSES = False
|
|
117
117
|
|
|
118
|
+
# Inactive users settings
|
|
119
|
+
YEARS_OF_INACTIVITY_BEFORE_DELETION = None
|
|
120
|
+
DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY = 30
|
|
121
|
+
MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS = 200
|
|
122
|
+
|
|
118
123
|
# Sentry configuration
|
|
119
124
|
SENTRY_DSN = None
|
|
120
125
|
SENTRY_TAGS = {}
|