udata 10.1.3.dev34275__py2.py3-none-any.whl → 10.1.3.dev34303__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 (40) hide show
  1. udata/core/dataservices/models.py +1 -0
  2. udata/core/user/api.py +8 -1
  3. udata/core/user/models.py +16 -11
  4. udata/core/user/tasks.py +81 -2
  5. udata/core/user/tests/test_user_model.py +29 -12
  6. udata/harvest/actions.py +5 -0
  7. udata/harvest/backends/base.py +22 -2
  8. udata/harvest/models.py +19 -0
  9. udata/harvest/tests/test_actions.py +12 -0
  10. udata/harvest/tests/test_base_backend.py +74 -8
  11. udata/settings.py +5 -0
  12. udata/static/chunks/{11.822f6ccb39c92c796d13.js → 11.55ab79044cda0271b595.js} +3 -3
  13. udata/static/chunks/{11.822f6ccb39c92c796d13.js.map → 11.55ab79044cda0271b595.js.map} +1 -1
  14. udata/static/chunks/{13.d9c1735d14038b94c17e.js → 13.f29411b06be1883356a3.js} +2 -2
  15. udata/static/chunks/{13.d9c1735d14038b94c17e.js.map → 13.f29411b06be1883356a3.js.map} +1 -1
  16. udata/static/chunks/{17.81c57c0dedf812e43013.js → 17.3bd0340930d4a314ce9c.js} +2 -2
  17. udata/static/chunks/{17.81c57c0dedf812e43013.js.map → 17.3bd0340930d4a314ce9c.js.map} +1 -1
  18. udata/static/chunks/{19.ba0bb2baa40e899d440b.js → 19.3e0e8651d948e04b8cf2.js} +3 -3
  19. udata/static/chunks/{19.ba0bb2baa40e899d440b.js.map → 19.3e0e8651d948e04b8cf2.js.map} +1 -1
  20. udata/static/chunks/{5.0652a860afda96795a53.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
  21. udata/static/chunks/{5.0652a860afda96795a53.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
  22. udata/static/chunks/{6.92d7c2ec6d20005774ef.js → 6.d663709d877baa44a71e.js} +3 -3
  23. udata/static/chunks/{6.92d7c2ec6d20005774ef.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
  24. udata/static/chunks/{8.0f42630e6d8ff782928e.js → 8.494b003a94383b142c18.js} +2 -2
  25. udata/static/chunks/{8.0f42630e6d8ff782928e.js.map → 8.494b003a94383b142c18.js.map} +1 -1
  26. udata/static/common.js +1 -1
  27. udata/static/common.js.map +1 -1
  28. udata/templates/mail/account_inactivity.html +29 -0
  29. udata/templates/mail/account_inactivity.txt +22 -0
  30. udata/templates/mail/inactive_account_deleted.html +5 -0
  31. udata/templates/mail/inactive_account_deleted.txt +6 -0
  32. udata/tests/api/test_me_api.py +1 -1
  33. udata/tests/api/test_user_api.py +47 -8
  34. udata/tests/user/test_user_tasks.py +144 -0
  35. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/METADATA +5 -1
  36. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/RECORD +40 -35
  37. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/LICENSE +0 -0
  38. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/WHEEL +0 -0
  39. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/entry_points.txt +0 -0
  40. {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/top_level.txt +0 -0
@@ -94,6 +94,7 @@ class HarvestMetadata(db.EmbeddedDocument):
94
94
  )
95
95
  last_update = field(db.DateTimeField(), description="Date of the last harvesting")
96
96
  archived_at = field(db.DateTimeField())
97
+ archived_reason = field(db.StringField())
97
98
 
98
99
 
99
100
  @generate_fields(
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/harvest/actions.py CHANGED
@@ -7,6 +7,7 @@ from bson import ObjectId
7
7
  from flask import current_app
8
8
 
9
9
  from udata.auth import current_user
10
+ from udata.core.dataservices.models import Dataservice
10
11
  from udata.core.dataset.models import HarvestDatasetMetadata
11
12
  from udata.models import Dataset, Organization, PeriodicTask, User
12
13
  from udata.storage.s3 import delete_file
@@ -18,6 +19,7 @@ from .models import (
18
19
  VALIDATION_REFUSED,
19
20
  HarvestJob,
20
21
  HarvestSource,
22
+ archive_harvested_dataservice,
21
23
  archive_harvested_dataset,
22
24
  )
23
25
  from .tasks import harvest
@@ -161,6 +163,9 @@ def purge_sources():
161
163
  datasets = Dataset.objects.filter(harvest__source_id=str(source.id))
162
164
  for dataset in datasets:
163
165
  archive_harvested_dataset(dataset, reason="harvester-deleted", dryrun=False)
166
+ dataservices = Dataservice.objects.filter(harvest__source_id=str(source.id))
167
+ for dataservice in dataservices:
168
+ archive_harvested_dataservice(dataservice, reason="harvester-deleted", dryrun=False)
164
169
  source.delete()
165
170
  return count
166
171
 
@@ -20,6 +20,7 @@ from ..models import (
20
20
  HarvestItem,
21
21
  HarvestJob,
22
22
  HarvestLog,
23
+ archive_harvested_dataservice,
23
24
  archive_harvested_dataset,
24
25
  )
25
26
  from ..signals import after_harvest_job, before_harvest_job
@@ -342,6 +343,7 @@ class BaseBackend(object):
342
343
  harvest.last_update = datetime.utcnow()
343
344
 
344
345
  harvest.archived_at = None
346
+ harvest.archived_reason = None
345
347
 
346
348
  return harvest
347
349
 
@@ -370,9 +372,10 @@ class BaseBackend(object):
370
372
  "harvest__remote_id__nin": remote_ids,
371
373
  "harvest__last_update__lt": limit_date,
372
374
  }
373
- local_items_not_on_remote = Dataset.objects.filter(**q)
375
+ local_datasets_not_on_remote = Dataset.objects.filter(**q)
376
+ local_dataservices_not_on_remote = Dataservice.objects.filter(**q)
374
377
 
375
- for dataset in local_items_not_on_remote:
378
+ for dataset in local_datasets_not_on_remote:
376
379
  if not dataset.harvest.archived_at:
377
380
  archive_harvested_dataset(dataset, reason="not-on-remote", dryrun=self.dryrun)
378
381
  # add a HarvestItem to the job list (useful for report)
@@ -385,6 +388,23 @@ class BaseBackend(object):
385
388
 
386
389
  self.save_job()
387
390
 
391
+ for dataservice in local_dataservices_not_on_remote:
392
+ if not dataservice.harvest.archived_at:
393
+ archive_harvested_dataservice(
394
+ dataservice, reason="not-on-remote", dryrun=self.dryrun
395
+ )
396
+ # add a HarvestItem to the job list (useful for report)
397
+ # even when archiving has already been done (useful for debug)
398
+ self.job.items.append(
399
+ HarvestItem(
400
+ remote_id=str(dataservice.harvest.remote_id),
401
+ dataservice=dataservice,
402
+ status="archived",
403
+ )
404
+ )
405
+
406
+ self.save_job()
407
+
388
408
  def get_dataset(self, remote_id):
389
409
  """Get or create a dataset given its remote ID (and its source)
390
410
  We first try to match `source_id` to be source domain independent
udata/harvest/models.py CHANGED
@@ -6,6 +6,7 @@ from urllib.parse import urlparse
6
6
  from werkzeug.utils import cached_property
7
7
 
8
8
  from udata.core.dataservices.models import Dataservice
9
+ from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
9
10
  from udata.core.dataset.models import HarvestDatasetMetadata
10
11
  from udata.core.owned import Owned, OwnedQuerySet
11
12
  from udata.i18n import lazy_gettext as _
@@ -203,3 +204,21 @@ def archive_harvested_dataset(dataset, reason, dryrun=False):
203
204
  dataset.validate()
204
205
  else:
205
206
  dataset.save()
207
+
208
+
209
+ def archive_harvested_dataservice(dataservice, reason, dryrun=False):
210
+ """
211
+ Archive an harvested dataservice, setting extras accordingly.
212
+ If `dryrun` is True, the dataservice is not saved but validated only.
213
+ """
214
+ log.debug("Archiving dataservice %s", dataservice.id)
215
+ archival_date = datetime.utcnow()
216
+ dataservice.archived_at = archival_date
217
+ if not dataservice.harvest:
218
+ dataservice.harvest = HarvestDataserviceMetadata()
219
+ dataservice.harvest.archived_reason = reason
220
+ dataservice.harvest.archived_at = archival_date
221
+ if dryrun:
222
+ dataservice.validate()
223
+ else:
224
+ dataservice.save()
@@ -6,6 +6,8 @@ from tempfile import NamedTemporaryFile
6
6
  import pytest
7
7
  from mock import patch
8
8
 
9
+ from udata.core.dataservices.factories import DataserviceFactory
10
+ from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
9
11
  from udata.core.dataset.factories import DatasetFactory
10
12
  from udata.core.dataset.models import HarvestDatasetMetadata
11
13
  from udata.core.organization.factories import OrganizationFactory
@@ -396,17 +398,27 @@ class HarvestActionsTest:
396
398
  dataset_to_archive = DatasetFactory(
397
399
  harvest=HarvestDatasetMetadata(source_id=str(to_delete[0].id))
398
400
  )
401
+ dataservice_to_archive = DataserviceFactory(
402
+ harvest=HarvestDataserviceMetadata(source_id=str(to_delete[0].id))
403
+ )
399
404
 
400
405
  result = actions.purge_sources()
401
406
  dataset_to_archive.reload()
407
+ dataservice_to_archive.reload()
402
408
 
403
409
  assert result == len(to_delete)
404
410
  assert len(HarvestSource.objects) == len(to_keep)
405
411
  assert PeriodicTask.objects.filter(id=periodic_task.id).count() == 0
406
412
  assert HarvestJob.objects(id=harvest_job.id).count() == 0
413
+
407
414
  assert dataset_to_archive.harvest.archived == "harvester-deleted"
415
+ assert_equal_dates(dataset_to_archive.harvest.archived_at, now)
408
416
  assert_equal_dates(dataset_to_archive.archived, now)
409
417
 
418
+ assert dataservice_to_archive.harvest.archived_reason == "harvester-deleted"
419
+ assert_equal_dates(dataservice_to_archive.harvest.archived_at, now)
420
+ assert_equal_dates(dataservice_to_archive.archived_at, now)
421
+
410
422
  @pytest.mark.options(HARVEST_JOBS_RETENTION_DAYS=2)
411
423
  def test_purge_jobs(self):
412
424
  now = datetime.utcnow()
@@ -4,6 +4,8 @@ from urllib.parse import urlparse
4
4
  import pytest
5
5
  from voluptuous import Schema
6
6
 
7
+ from udata.core.dataservices.factories import DataserviceFactory
8
+ from udata.core.dataservices.models import Dataservice
7
9
  from udata.core.dataset import tasks
8
10
  from udata.core.dataset.factories import DatasetFactory
9
11
  from udata.harvest.models import HarvestItem
@@ -20,9 +22,9 @@ class Unknown:
20
22
  pass
21
23
 
22
24
 
23
- def gen_remote_IDs(num: int) -> list[str]:
25
+ def gen_remote_IDs(num: int, prefix: str = "") -> list[str]:
24
26
  """Generate remote IDs."""
25
- return [f"fake-{i}" for i in range(num)]
27
+ return [f"{prefix}fake-{i}" for i in range(num)]
26
28
 
27
29
 
28
30
  class FakeBackend(BaseBackend):
@@ -45,6 +47,11 @@ class FakeBackend(BaseBackend):
45
47
  if self.is_done():
46
48
  return
47
49
 
50
+ for remote_id in self.source.config.get("dataservice_remote_ids", []):
51
+ self.process_dataservice(remote_id)
52
+ if self.is_done():
53
+ return
54
+
48
55
  def inner_process_dataset(self, item: HarvestItem):
49
56
  dataset = self.get_dataset(item.remote_id)
50
57
 
@@ -55,6 +62,16 @@ class FakeBackend(BaseBackend):
55
62
  dataset.last_modified_internal = self.source.config["last_modified"]
56
63
  return dataset
57
64
 
65
+ def inner_process_dataservice(self, item: HarvestItem):
66
+ dataservice = self.get_dataservice(item.remote_id)
67
+
68
+ for key, value in DataserviceFactory.as_dict().items():
69
+ if getattr(dataservice, key) is None:
70
+ setattr(dataservice, key, value)
71
+ if self.source.config.get("last_modified"):
72
+ dataservice.last_modified_internal = self.source.config["last_modified"]
73
+ return dataservice
74
+
58
75
 
59
76
  class HarvestFilterTest:
60
77
  @pytest.mark.parametrize("type,expected", HarvestFilter.TYPES.items())
@@ -210,7 +227,13 @@ class BaseBackendTest:
210
227
 
211
228
  def test_autoarchive(self, app):
212
229
  nb_datasets = 3
213
- source = HarvestSourceFactory(config={"dataset_remote_ids": gen_remote_IDs(nb_datasets)})
230
+ nb_dataservices = 3
231
+ source = HarvestSourceFactory(
232
+ config={
233
+ "dataset_remote_ids": gen_remote_IDs(nb_datasets, "dataset-"),
234
+ "dataservice_remote_ids": gen_remote_IDs(nb_dataservices, "dataservice-"),
235
+ }
236
+ )
214
237
  backend = FakeBackend(source)
215
238
 
216
239
  # create a dangling dataset to be archived
@@ -220,7 +243,15 @@ class BaseBackendTest:
220
243
  harvest={
221
244
  "domain": source.domain,
222
245
  "source_id": str(source.id),
223
- "remote_id": "not-on-remote",
246
+ "remote_id": "dataset-not-on-remote",
247
+ "last_update": last_update,
248
+ }
249
+ )
250
+ dataservice_arch = DataserviceFactory(
251
+ harvest={
252
+ "domain": source.domain,
253
+ "source_id": str(source.id),
254
+ "remote_id": "dataservice-not-on-remote",
224
255
  "last_update": last_update,
225
256
  }
226
257
  )
@@ -232,7 +263,15 @@ class BaseBackendTest:
232
263
  harvest={
233
264
  "domain": source.domain,
234
265
  "source_id": str(source.id),
235
- "remote_id": "not-on-remote-two",
266
+ "remote_id": "dataset-not-on-remote-two",
267
+ "last_update": last_update,
268
+ }
269
+ )
270
+ dataservice_no_arch = DataserviceFactory(
271
+ harvest={
272
+ "domain": source.domain,
273
+ "source_id": str(source.id),
274
+ "remote_id": "dataservice-not-on-remote-two",
236
275
  "last_update": last_update,
237
276
  }
238
277
  )
@@ -240,13 +279,17 @@ class BaseBackendTest:
240
279
  job = backend.harvest()
241
280
 
242
281
  # all datasets except arch : 3 mocks + 1 manual (no_arch)
243
- assert len(job.items) == nb_datasets + 1
282
+ assert len(job.items) == (nb_datasets + 1) + (nb_dataservices + 1)
244
283
  # all datasets : 3 mocks + 2 manuals (arch and no_arch)
245
284
  assert Dataset.objects.count() == nb_datasets + 2
285
+ assert Dataservice.objects.count() == nb_dataservices + 2
246
286
 
247
287
  archived_items = [i for i in job.items if i.status == "archived"]
248
- assert len(archived_items) == 1
288
+ assert len(archived_items) == 2
249
289
  assert archived_items[0].dataset == dataset_arch
290
+ assert archived_items[0].dataservice is None
291
+ assert archived_items[1].dataset is None
292
+ assert archived_items[1].dataservice == dataservice_arch
250
293
 
251
294
  dataset_arch.reload()
252
295
  assert dataset_arch.archived is not None
@@ -258,18 +301,41 @@ class BaseBackendTest:
258
301
  assert "archived" not in dataset_no_arch.harvest
259
302
  assert "archived_at" not in dataset_no_arch.harvest
260
303
 
304
+ dataservice_arch.reload()
305
+ assert dataservice_arch.archived_at is not None
306
+ assert "archived_reason" in dataservice_arch.harvest
307
+ assert "archived_at" in dataservice_arch.harvest
308
+
309
+ dataservice_no_arch.reload()
310
+ assert dataservice_no_arch.archived_at is None
311
+ assert "archived_reason" not in dataservice_no_arch.harvest
312
+ assert "archived_at" not in dataservice_no_arch.harvest
313
+
261
314
  # test unarchive: archive manually then relaunch harvest
262
- dataset = Dataset.objects.get(**{"harvest__remote_id": "fake-1"})
315
+ dataset = Dataset.objects.get(**{"harvest__remote_id": "dataset-fake-1"})
263
316
  dataset.archived = datetime.utcnow()
264
317
  dataset.harvest.archived = "not-on-remote"
265
318
  dataset.harvest.archived_at = datetime.utcnow()
266
319
  dataset.save()
320
+
321
+ dataservice = Dataservice.objects.get(**{"harvest__remote_id": "dataservice-fake-1"})
322
+ dataservice.archived_at = datetime.utcnow()
323
+ dataservice.harvest.archived_reason = "not-on-remote"
324
+ dataservice.harvest.archived_at = datetime.utcnow()
325
+ dataservice.save()
326
+
267
327
  backend.harvest()
328
+
268
329
  dataset.reload()
269
330
  assert dataset.archived is None
270
331
  assert "archived" not in dataset.harvest
271
332
  assert "archived_at" not in dataset.harvest
272
333
 
334
+ dataservice.reload()
335
+ assert dataservice.archived_at is None
336
+ assert "archived_reason" not in dataservice.harvest
337
+ assert "archived_at" not in dataservice.harvest
338
+
273
339
  def test_harvest_datasets_get_deleted(self):
274
340
  nb_datasets = 3
275
341
  source = HarvestSourceFactory(config={"dataset_remote_ids": gen_remote_IDs(nb_datasets)})
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 = {}