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.

Files changed (35) hide show
  1. udata/core/user/api.py +8 -1
  2. udata/core/user/models.py +16 -11
  3. udata/core/user/tasks.py +81 -2
  4. udata/core/user/tests/test_user_model.py +29 -12
  5. udata/settings.py +5 -0
  6. udata/static/chunks/{10.8ca60413647062717b1e.js → 10.471164b2a9fe15614797.js} +3 -3
  7. udata/static/chunks/{10.8ca60413647062717b1e.js.map → 10.471164b2a9fe15614797.js.map} +1 -1
  8. udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.51d706fb9521c16976bc.js} +3 -3
  9. udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.51d706fb9521c16976bc.js.map} +1 -1
  10. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.f29411b06be1883356a3.js} +2 -2
  11. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.f29411b06be1883356a3.js.map} +1 -1
  12. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.3bd0340930d4a314ce9c.js} +2 -2
  13. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.3bd0340930d4a314ce9c.js.map} +1 -1
  14. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.8da42e8359d72afc2618.js} +3 -3
  15. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.8da42e8359d72afc2618.js.map} +1 -1
  16. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
  17. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
  18. udata/static/chunks/{9.033d7e190ca9e226a5d0.js → 9.07515e5187f475bce828.js} +3 -3
  19. udata/static/chunks/{9.033d7e190ca9e226a5d0.js.map → 9.07515e5187f475bce828.js.map} +1 -1
  20. udata/static/common.js +1 -1
  21. udata/static/common.js.map +1 -1
  22. udata/templates/mail/account_inactivity.html +29 -0
  23. udata/templates/mail/account_inactivity.txt +22 -0
  24. udata/templates/mail/inactive_account_deleted.html +5 -0
  25. udata/templates/mail/inactive_account_deleted.txt +6 -0
  26. udata/tests/api/test_me_api.py +1 -1
  27. udata/tests/api/test_user_api.py +47 -8
  28. udata/tests/user/test_user_tasks.py +144 -0
  29. udata/translations/udata.pot +83 -54
  30. {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/METADATA +4 -1
  31. {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/RECORD +35 -30
  32. {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/LICENSE +0 -0
  33. {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/WHEEL +0 -0
  34. {udata-10.1.3.dev34283.dist-info → udata-10.1.3.dev34313.dist-info}/entry_points.txt +0 -0
  35. {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
- for discussion in Discussion.objects(discussion__posted_by=self):
269
- # Remove all discussions with current user as only participant
270
- if all(message.posted_by == self for message in discussion.discussion):
271
- discussion.delete()
272
- continue
273
-
274
- for message in discussion.discussion:
275
- if message.posted_by == self:
276
- message.content = "DELETED"
277
- discussion.save()
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
- discussion_only_user = DiscussionFactory(
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
- assert Discussion.objects(id=discussion_only_user.id).first() is None
46
- discussion_with_other.reload()
47
- assert discussion_with_other.discussion[1].content == "DELETED"
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 = {}