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.
- udata/core/dataservices/models.py +1 -0
- 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/harvest/actions.py +5 -0
- udata/harvest/backends/base.py +22 -2
- udata/harvest/models.py +19 -0
- udata/harvest/tests/test_actions.py +12 -0
- udata/harvest/tests/test_base_backend.py +74 -8
- udata/settings.py +5 -0
- udata/static/chunks/{11.822f6ccb39c92c796d13.js → 11.55ab79044cda0271b595.js} +3 -3
- udata/static/chunks/{11.822f6ccb39c92c796d13.js.map → 11.55ab79044cda0271b595.js.map} +1 -1
- udata/static/chunks/{13.d9c1735d14038b94c17e.js → 13.f29411b06be1883356a3.js} +2 -2
- udata/static/chunks/{13.d9c1735d14038b94c17e.js.map → 13.f29411b06be1883356a3.js.map} +1 -1
- udata/static/chunks/{17.81c57c0dedf812e43013.js → 17.3bd0340930d4a314ce9c.js} +2 -2
- udata/static/chunks/{17.81c57c0dedf812e43013.js.map → 17.3bd0340930d4a314ce9c.js.map} +1 -1
- udata/static/chunks/{19.ba0bb2baa40e899d440b.js → 19.3e0e8651d948e04b8cf2.js} +3 -3
- udata/static/chunks/{19.ba0bb2baa40e899d440b.js.map → 19.3e0e8651d948e04b8cf2.js.map} +1 -1
- udata/static/chunks/{5.0652a860afda96795a53.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
- udata/static/chunks/{5.0652a860afda96795a53.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
- udata/static/chunks/{6.92d7c2ec6d20005774ef.js → 6.d663709d877baa44a71e.js} +3 -3
- udata/static/chunks/{6.92d7c2ec6d20005774ef.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
- udata/static/chunks/{8.0f42630e6d8ff782928e.js → 8.494b003a94383b142c18.js} +2 -2
- udata/static/chunks/{8.0f42630e6d8ff782928e.js.map → 8.494b003a94383b142c18.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-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/METADATA +5 -1
- {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/RECORD +40 -35
- {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/LICENSE +0 -0
- {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/WHEEL +0 -0
- {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.dist-info}/entry_points.txt +0 -0
- {udata-10.1.3.dev34275.dist-info → udata-10.1.3.dev34303.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/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
|
|
udata/harvest/backends/base.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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) ==
|
|
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 = {}
|