udata 10.9.1.dev0__py3-none-any.whl → 11.0.2.dev8__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 (25) hide show
  1. udata/core/activity/models.py +25 -3
  2. udata/core/dataservices/activities.py +5 -3
  3. udata/core/dataset/activities.py +8 -6
  4. udata/harvest/backends/base.py +16 -2
  5. udata/harvest/tests/test_actions.py +22 -1
  6. udata/settings.py +8 -0
  7. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.0f04e49a40a0a381bcce.js} +3 -3
  8. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.0f04e49a40a0a381bcce.js.map} +1 -1
  9. udata/static/chunks/{13.f29411b06be1883356a3.js → 13.39e106d56f794ebd06a0.js} +2 -2
  10. udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.39e106d56f794ebd06a0.js.map} +1 -1
  11. udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.70cbb4a91b002338007e.js} +2 -2
  12. udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.70cbb4a91b002338007e.js.map} +1 -1
  13. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.df16abde17a42033a7f8.js} +3 -3
  14. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
  15. udata/static/chunks/{8.54e44b102164ae5e7a67.js → 8.0f42630e6d8ff782928e.js} +2 -2
  16. udata/static/chunks/{8.54e44b102164ae5e7a67.js.map → 8.0f42630e6d8ff782928e.js.map} +1 -1
  17. udata/static/common.js +1 -1
  18. udata/static/common.js.map +1 -1
  19. udata/tests/test_activity.py +142 -1
  20. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/METADATA +2 -2
  21. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/RECORD +25 -25
  22. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/WHEEL +0 -0
  23. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/entry_points.txt +0 -0
  24. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/licenses/LICENSE +0 -0
  25. {udata-10.9.1.dev0.dist-info → udata-11.0.2.dev8.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,8 @@
1
+ import logging
1
2
  from datetime import datetime
2
3
 
3
4
  from blinker import Signal
5
+ from flask import g
4
6
  from mongoengine.errors import DoesNotExist
5
7
  from mongoengine.signals import post_save
6
8
 
@@ -13,6 +15,8 @@ from .signals import new_activity
13
15
 
14
16
  __all__ = ("Activity",)
15
17
 
18
+ log = logging.getLogger(__name__)
19
+
16
20
 
17
21
  _registered_activities = {}
18
22
 
@@ -70,10 +74,15 @@ class Activity(db.Document, metaclass=EmitNewActivityMetaClass):
70
74
 
71
75
  @classmethod
72
76
  def emit(cls, related_to, organization=None, changed_fields=None, extras=None):
77
+ if hasattr(g, "harvest_activity_user"):
78
+ # We're in the context of a harvest action with a harvest activity user to use as actor
79
+ actor = g.harvest_activity_user
80
+ else:
81
+ actor = current_user._get_current_object()
73
82
  new_activity.send(
74
83
  cls,
75
84
  related_to=related_to,
76
- actor=current_user._get_current_object(),
85
+ actor=actor,
77
86
  organization=organization,
78
87
  changes=changed_fields,
79
88
  extras=extras,
@@ -110,7 +119,9 @@ class Auditable(object):
110
119
  # for backward compatibility, all fields are treated as auditable for classes not using field() function
111
120
  auditable_fields = document._get_changed_fields()
112
121
  changed_fields = [
113
- field for field in document._get_changed_fields() if field in auditable_fields
122
+ field
123
+ for field in document._get_changed_fields()
124
+ if field.split(".")[0] in auditable_fields
114
125
  ]
115
126
  if "post_save" in kwargs.get("ignores", []):
116
127
  return
@@ -119,6 +130,17 @@ class Auditable(object):
119
130
  cls.on_create.send(document)
120
131
  elif len(changed_fields):
121
132
  previous = getattr(document, "_previous_changed_fields", None)
122
- cls.on_update.send(document, changed_fields=changed_fields, previous=previous)
133
+ # make sure that changed fields have actually changed when comparing the document
134
+ # once it has been reloaded. It may have been cleaned or normalized when saved to mongo.
135
+ # We compare them one by one with the previous value stored in _previous_changed_fields.
136
+ # See https://github.com/opendatateam/udata/pull/3412 for more context.
137
+ document.reload()
138
+ changed_fields = [
139
+ field
140
+ for field in changed_fields
141
+ if previous[field] != get_field_value_from_path(document, field)
142
+ ]
143
+ if changed_fields:
144
+ cls.on_update.send(document, changed_fields=changed_fields, previous=previous)
123
145
  if getattr(document, "deleted_at", None) or getattr(document, "deleted", None):
124
146
  cls.on_delete.send(document)
@@ -1,3 +1,5 @@
1
+ from flask import g
2
+
1
3
  from udata.auth import current_user
2
4
  from udata.i18n import lazy_gettext as _
3
5
  from udata.models import Activity, Dataservice, db
@@ -36,18 +38,18 @@ class UserDeletedDataservice(DataserviceRelatedActivity, Activity):
36
38
 
37
39
  @Dataservice.on_create.connect
38
40
  def on_user_created_dataservice(dataservice):
39
- if current_user and current_user.is_authenticated:
41
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
40
42
  UserCreatedDataservice.emit(dataservice, dataservice.organization)
41
43
 
42
44
 
43
45
  @Dataservice.on_update.connect
44
46
  def on_user_updated_dataservice(dataservice, **kwargs):
45
47
  changed_fields = kwargs.get("changed_fields", [])
46
- if current_user and current_user.is_authenticated:
48
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
47
49
  UserUpdatedDataservice.emit(dataservice, dataservice.organization, changed_fields)
48
50
 
49
51
 
50
52
  @Dataservice.on_delete.connect
51
53
  def on_user_deleted_dataservice(dataservice):
52
- if current_user and current_user.is_authenticated:
54
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
53
55
  UserDeletedDataservice.emit(dataservice, dataservice.organization)
@@ -1,3 +1,5 @@
1
+ from flask import g
2
+
1
3
  from udata.auth import current_user
2
4
  from udata.i18n import lazy_gettext as _
3
5
  from udata.models import Activity, Dataset, db
@@ -55,7 +57,7 @@ class UserRemovedResourceFromDataset(DatasetRelatedActivity, Activity):
55
57
 
56
58
  @Dataset.on_resource_added.connect
57
59
  def on_user_added_resource_to_dataset(sender, document, **kwargs):
58
- if current_user and current_user.is_authenticated:
60
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
59
61
  UserAddedResourceToDataset.emit(
60
62
  document, document.organization, None, {"resource_id": str(kwargs["resource_id"])}
61
63
  )
@@ -64,7 +66,7 @@ def on_user_added_resource_to_dataset(sender, document, **kwargs):
64
66
  @Dataset.on_resource_updated.connect
65
67
  def on_user_updated_resource(sender, document, **kwargs):
66
68
  changed_fields = kwargs.get("changed_fields", [])
67
- if current_user and current_user.is_authenticated:
69
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
68
70
  UserUpdatedResource.emit(
69
71
  document,
70
72
  document.organization,
@@ -75,7 +77,7 @@ def on_user_updated_resource(sender, document, **kwargs):
75
77
 
76
78
  @Dataset.on_resource_removed.connect
77
79
  def on_user_removed_resource_from_dataset(sender, document, **kwargs):
78
- if current_user and current_user.is_authenticated:
80
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
79
81
  UserRemovedResourceFromDataset.emit(
80
82
  document, document.organization, None, {"resource_id": str(kwargs["resource_id"])}
81
83
  )
@@ -83,18 +85,18 @@ def on_user_removed_resource_from_dataset(sender, document, **kwargs):
83
85
 
84
86
  @Dataset.on_create.connect
85
87
  def on_user_created_dataset(dataset):
86
- if current_user and current_user.is_authenticated:
88
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
87
89
  UserCreatedDataset.emit(dataset, dataset.organization)
88
90
 
89
91
 
90
92
  @Dataset.on_update.connect
91
93
  def on_user_updated_dataset(dataset, **kwargs):
92
94
  changed_fields = kwargs.get("changed_fields", [])
93
- if current_user and current_user.is_authenticated:
95
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
94
96
  UserUpdatedDataset.emit(dataset, dataset.organization, changed_fields)
95
97
 
96
98
 
97
99
  @Dataset.on_delete.connect
98
100
  def on_user_deleted_dataset(dataset):
99
- if current_user and current_user.is_authenticated:
101
+ if (current_user and current_user.is_authenticated) or hasattr(g, "harvest_activity_user"):
100
102
  UserDeletedDataset.emit(dataset, dataset.organization)
@@ -4,14 +4,14 @@ from datetime import date, datetime, timedelta
4
4
  from uuid import UUID
5
5
 
6
6
  import requests
7
- from flask import current_app
7
+ from flask import current_app, g
8
8
  from voluptuous import MultipleInvalid, RequiredFieldInvalid
9
9
 
10
10
  import udata.uris as uris
11
11
  from udata.core.dataservices.models import Dataservice
12
12
  from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
13
13
  from udata.core.dataset.models import HarvestDatasetMetadata
14
- from udata.models import Dataset
14
+ from udata.models import Dataset, User
15
15
  from udata.utils import safe_unicode
16
16
 
17
17
  from ..exceptions import HarvestException, HarvestSkipException, HarvestValidationError
@@ -168,6 +168,17 @@ class BaseBackend(object):
168
168
  self.job = factory(status="initialized", started=datetime.utcnow(), source=self.source)
169
169
 
170
170
  before_harvest_job.send(self)
171
+ # Set harvest_activity_user on global context during the run
172
+ if current_app.config["HARVEST_ACTIVITY_USER_ID"]:
173
+ try:
174
+ # Try to fetch the existing harvest activity user
175
+ g.harvest_activity_user = User.objects.get(
176
+ id=current_app.config["HARVEST_ACTIVITY_USER_ID"]
177
+ )
178
+ except User.DoesNotExist:
179
+ log.exception(
180
+ "HARVEST_ACTIVITY_USER_ID does not seem to match an existing user id."
181
+ )
171
182
 
172
183
  try:
173
184
  self.inner_harvest()
@@ -199,6 +210,9 @@ class BaseBackend(object):
199
210
  self.job.errors.append(error)
200
211
  finally:
201
212
  self.end_job()
213
+ # Clean harvest_activity_user on global context
214
+ if hasattr(g, "harvest_activity_user"):
215
+ delattr(g, "harvest_activity_user")
202
216
 
203
217
  return self.job
204
218
 
@@ -4,16 +4,19 @@ from datetime import datetime, timedelta
4
4
  from tempfile import NamedTemporaryFile
5
5
 
6
6
  import pytest
7
+ from flask import current_app
7
8
  from mock import patch
8
9
 
10
+ from udata.core.activity.models import new_activity
9
11
  from udata.core.dataservices.factories import DataserviceFactory
10
12
  from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
13
+ from udata.core.dataset.activities import UserCreatedDataset
11
14
  from udata.core.dataset.factories import DatasetFactory
12
15
  from udata.core.dataset.models import HarvestDatasetMetadata
13
16
  from udata.core.organization.factories import OrganizationFactory
14
17
  from udata.core.user.factories import UserFactory
15
18
  from udata.models import Dataset, PeriodicTask
16
- from udata.tests.helpers import assert_emit, assert_equal_dates
19
+ from udata.tests.helpers import assert_emit, assert_equal_dates, assert_not_emit
17
20
  from udata.utils import faker
18
21
 
19
22
  from .. import actions, signals
@@ -578,6 +581,24 @@ class ExecutionTestMixin(MockBackendsMixin):
578
581
  self.action(source)
579
582
  assert len(Dataset.objects) == 5
580
583
 
584
+ @pytest.mark.options(HARVEST_ACTIVITY_USER_ID="68b860182728e27218dd7c72")
585
+ def test_harvest_emit_activity(self):
586
+ # We need to init dataset activities module
587
+ import udata.core.dataset.activities # noqa
588
+
589
+ user = UserFactory(id=current_app.config["HARVEST_ACTIVITY_USER_ID"])
590
+ source = HarvestSourceFactory(backend="factory")
591
+ with assert_emit(Dataset.on_create, new_activity):
592
+ self.action(source)
593
+
594
+ # We have an activity for each dataset created by the source action
595
+ activities = UserCreatedDataset.objects(actor=user)
596
+ assert activities.count() == Dataset.objects().count()
597
+
598
+ # On a second run, we don't expect any signal sent (no creation, update or deletion)
599
+ with assert_not_emit(Dataset.on_create, Dataset.on_update, new_activity):
600
+ self.action(source)
601
+
581
602
 
582
603
  class HarvestLaunchTest(ExecutionTestMixin):
583
604
  def action(self, *args, **kwargs):
udata/settings.py CHANGED
@@ -261,6 +261,8 @@ class Defaults(object):
261
261
 
262
262
  DELAY_BEFORE_REMINDER_NOTIFICATION = 30 # Days
263
263
 
264
+ # Harvest settings
265
+ ###########################################################################
264
266
  HARVEST_ENABLE_MANUAL_RUN = False
265
267
 
266
268
  HARVEST_PREVIEW_MAX_ITEMS = 20
@@ -285,7 +287,12 @@ class Defaults(object):
285
287
 
286
288
  HARVEST_ISO19139_XSLT_URL = "https://raw.githubusercontent.com/SEMICeu/iso-19139-to-dcat-ap/refs/heads/geodcat-ap-2.0.0/iso-19139-to-dcat-ap.xsl"
287
289
 
290
+ # If set, harvest emit activities associated with this user as actor
291
+ # It should be a dedicated service account
292
+ HARVEST_ACTIVITY_USER_ID = None
293
+
288
294
  # S3 connection details
295
+ ###########################################################################
289
296
  S3_URL = None
290
297
  S3_ACCESS_KEY_ID = None
291
298
  S3_SECRET_ACCESS_KEY = None
@@ -626,6 +633,7 @@ class Testing(object):
626
633
  "check_deliverability": False
627
634
  } # Disables deliverability for email domain name
628
635
  PUBLISH_ON_RESOURCE_EVENTS = False
636
+ HARVEST_ACTIVITY_USER_ID = None
629
637
 
630
638
 
631
639
  class Debug(Defaults):