udata 10.4.2.dev35266__py2.py3-none-any.whl → 10.4.2.dev35319__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.

@@ -6,6 +6,7 @@ log = logging.getLogger(__name__)
6
6
  def init_app(app):
7
7
  # Load all core actvitiess
8
8
  import udata.core.user.activities # noqa
9
+ import udata.core.dataservices.activities # noqa
9
10
  import udata.core.dataset.activities # noqa
10
11
  import udata.core.reuse.activities # noqa
11
12
  import udata.core.organization.activities # noqa
@@ -46,6 +46,7 @@ activity_fields = api.model(
46
46
  "label": fields.String(description="The label of the activity", required=True),
47
47
  "key": fields.String(description="The key of the activity", required=True),
48
48
  "icon": fields.String(description="The icon of the activity", required=True),
49
+ "changes": fields.List(fields.String, description="Changed attributes as list"),
49
50
  "extras": fields.Raw(description="Extras attributes as key-value pairs"),
50
51
  },
51
52
  )
@@ -3,6 +3,7 @@ from datetime import datetime
3
3
  from blinker import Signal
4
4
  from mongoengine.signals import post_save
5
5
 
6
+ from udata.api_fields import get_fields
6
7
  from udata.auth import current_user
7
8
  from udata.mongo import db
8
9
 
@@ -36,6 +37,7 @@ class Activity(db.Document, metaclass=EmitNewActivityMetaClass):
36
37
  organization = db.ReferenceField("Organization")
37
38
  related_to = db.ReferenceField(db.DomainModel, required=True)
38
39
  created_at = db.DateTimeField(default=datetime.utcnow, required=True)
40
+ changes = db.ListField(db.StringField())
39
41
 
40
42
  extras = db.ExtrasField()
41
43
 
@@ -65,11 +67,36 @@ class Activity(db.Document, metaclass=EmitNewActivityMetaClass):
65
67
  return cls.on_new.connect(func, sender=cls)
66
68
 
67
69
  @classmethod
68
- def emit(cls, related_to, organization=None, extras=None):
70
+ def emit(cls, related_to, organization=None, changed_fields=None, extras=None):
69
71
  new_activity.send(
70
72
  cls,
71
73
  related_to=related_to,
72
74
  actor=current_user._get_current_object(),
73
75
  organization=organization,
76
+ changes=changed_fields,
74
77
  extras=extras,
75
78
  )
79
+
80
+
81
+ class Auditable(object):
82
+ @classmethod
83
+ def post_save(cls, sender, document, **kwargs):
84
+ try:
85
+ auditable_fields = [
86
+ key for key, field, info in get_fields(cls) if info.get("auditable", True)
87
+ ]
88
+ except Exception:
89
+ # for backward compatibility, all fields are treated as auditable for classes not using field() function
90
+ auditable_fields = document._get_changed_fields()
91
+ changed_fields = [
92
+ field for field in document._get_changed_fields() if field in auditable_fields
93
+ ]
94
+ if "post_save" in kwargs.get("ignores", []):
95
+ return
96
+ cls.after_save.send(document)
97
+ if kwargs.get("created"):
98
+ cls.on_create.send(document)
99
+ elif len(changed_fields):
100
+ cls.on_update.send(document, changed_fields=changed_fields)
101
+ if getattr(document, "deleted_at", None) or getattr(document, "deleted", None):
102
+ cls.on_delete.send(document)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from collections.abc import Iterable
2
3
 
3
4
  from udata.models import Organization, User, db
4
5
  from udata.tasks import task
@@ -9,28 +10,36 @@ log = logging.getLogger(__name__)
9
10
 
10
11
 
11
12
  @new_activity.connect
12
- def delay_activity(cls, related_to, actor, organization=None, extras=None):
13
+ def delay_activity(cls, related_to, actor, organization=None, changes=None, extras=None):
13
14
  emit_activity.delay(
14
15
  cls.__name__,
15
16
  str(actor.id),
16
17
  related_to_cls=related_to.__class__.__name__,
17
18
  related_to_id=str(related_to.id),
18
19
  organization_id=str(organization.id) if organization else None,
20
+ changes=changes,
19
21
  extras=extras,
20
22
  )
21
23
 
22
24
 
23
25
  @task
24
26
  def emit_activity(
25
- classname, actor_id, related_to_cls, related_to_id, organization_id=None, extras=None
27
+ classname,
28
+ actor_id,
29
+ related_to_cls,
30
+ related_to_id,
31
+ organization_id=None,
32
+ changes=None,
33
+ extras=None,
26
34
  ):
27
35
  log.debug(
28
- "Emit new activity: %s %s %s %s %s %s",
36
+ "Emit new activity: %s %s %s %s %s %s %s",
29
37
  classname,
30
38
  actor_id,
31
39
  related_to_cls,
32
40
  related_to_id,
33
41
  organization_id,
42
+ ", ".join(changes) if changes and isinstance(changes, Iterable) else "",
34
43
  extras,
35
44
  )
36
45
  cls = db.resolve_model(classname)
@@ -40,4 +49,10 @@ def emit_activity(
40
49
  organization = Organization.objects.get(pk=organization_id)
41
50
  else:
42
51
  organization = None
43
- cls.objects.create(actor=actor, related_to=related_to, organization=organization, extras=extras)
52
+ cls.objects.create(
53
+ actor=actor,
54
+ related_to=related_to,
55
+ organization=organization,
56
+ changes=changes,
57
+ extras=extras,
58
+ )
@@ -0,0 +1,53 @@
1
+ from udata.auth import current_user
2
+ from udata.i18n import lazy_gettext as _
3
+ from udata.models import Activity, Dataservice, db
4
+
5
+ __all__ = (
6
+ "UserCreatedDataservice",
7
+ "UserUpdatedDataservice",
8
+ "UserDeletedDataservice",
9
+ "DataserviceRelatedActivity",
10
+ )
11
+
12
+
13
+ class DataserviceRelatedActivity(object):
14
+ related_to = db.ReferenceField("Dataservice")
15
+
16
+
17
+ class UserCreatedDataservice(DataserviceRelatedActivity, Activity):
18
+ key = "dataservice:created"
19
+ icon = "fa fa-plus"
20
+ badge_type = "success"
21
+ label = _("created a dataservice")
22
+
23
+
24
+ class UserUpdatedDataservice(DataserviceRelatedActivity, Activity):
25
+ key = "dataservice:updated"
26
+ icon = "fa fa-pencil"
27
+ label = _("updated a dataservice")
28
+
29
+
30
+ class UserDeletedDataservice(DataserviceRelatedActivity, Activity):
31
+ key = "dataservice:deleted"
32
+ icon = "fa fa-remove"
33
+ badge_type = "error"
34
+ label = _("deleted a dataservice")
35
+
36
+
37
+ @Dataservice.on_create.connect
38
+ def on_user_created_dataservice(dataservice):
39
+ if not dataservice.private and current_user and current_user.is_authenticated:
40
+ UserCreatedDataservice.emit(dataservice, dataservice.organization)
41
+
42
+
43
+ @Dataservice.on_update.connect
44
+ def on_user_updated_dataservice(dataservice, **kwargs):
45
+ changed_fields = kwargs.get("changed_fields", [])
46
+ if not dataservice.private and current_user and current_user.is_authenticated:
47
+ UserUpdatedDataservice.emit(dataservice, dataservice.organization, changed_fields)
48
+
49
+
50
+ @Dataservice.on_delete.connect
51
+ def on_user_deleted_dataservice(dataservice):
52
+ if not dataservice.private and current_user and current_user.is_authenticated:
53
+ UserDeletedDataservice.emit(dataservice, dataservice.organization)
@@ -8,6 +8,7 @@ from mongoengine.signals import post_save
8
8
  import udata.core.contact_point.api_fields as contact_api_fields
9
9
  import udata.core.dataset.api_fields as datasets_api_fields
10
10
  from udata.api_fields import field, function_field, generate_fields
11
+ from udata.core.activity.models import Auditable
11
12
  from udata.core.dataservices.constants import DATASERVICE_ACCESS_TYPES, DATASERVICE_FORMATS
12
13
  from udata.core.dataset.models import Dataset
13
14
  from udata.core.metrics.models import WithMetrics
@@ -105,7 +106,7 @@ class HarvestMetadata(db.EmbeddedDocument):
105
106
  {"key": "views", "value": "metrics.views"},
106
107
  ],
107
108
  )
108
- class Dataservice(WithMetrics, Owned, db.Document):
109
+ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
109
110
  meta = {
110
111
  "indexes": [
111
112
  "$title",
@@ -117,6 +118,11 @@ class Dataservice(WithMetrics, Owned, db.Document):
117
118
  "auto_create_index_on_save": True,
118
119
  }
119
120
 
121
+ after_save = Signal()
122
+ on_create = Signal()
123
+ on_update = Signal()
124
+ on_delete = Signal()
125
+
120
126
  verbose_name = _("dataservice")
121
127
 
122
128
  def __str__(self):
@@ -175,7 +181,10 @@ class Dataservice(WithMetrics, Owned, db.Document):
175
181
  description="Is the dataservice private to the owner or the organization",
176
182
  )
177
183
 
178
- extras = field(db.ExtrasField())
184
+ extras = field(
185
+ db.ExtrasField(),
186
+ auditable=False,
187
+ )
179
188
 
180
189
  contact_points = field(
181
190
  db.ListField(
@@ -201,8 +210,9 @@ class Dataservice(WithMetrics, Owned, db.Document):
201
210
  ),
202
211
  readonly=True,
203
212
  sortable="last_modified",
213
+ auditable=False,
204
214
  )
205
- deleted_at = field(db.DateTimeField())
215
+ deleted_at = field(db.DateTimeField(), auditable=False)
206
216
  archived_at = field(db.DateTimeField())
207
217
 
208
218
  datasets = field(
@@ -223,6 +233,7 @@ class Dataservice(WithMetrics, Owned, db.Document):
223
233
  harvest = field(
224
234
  db.EmbeddedDocumentField(HarvestMetadata),
225
235
  readonly=True,
236
+ auditable=False,
226
237
  )
227
238
 
228
239
  def url_for(self, *args, **kwargs):
@@ -250,26 +261,11 @@ class Dataservice(WithMetrics, Owned, db.Document):
250
261
 
251
262
  def count_discussions(self):
252
263
  self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
253
- self.save()
264
+ self.save(signal_kwargs={"ignores": ["post_save"]})
254
265
 
255
266
  def count_followers(self):
256
267
  self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
257
- self.save()
258
-
259
- on_create = Signal()
260
- on_update = Signal()
261
- on_delete = Signal()
262
-
263
- @classmethod
264
- def post_save(cls, sender, document, **kwargs):
265
- if "post_save" in kwargs.get("ignores", []):
266
- return
267
- if kwargs.get("created"):
268
- cls.on_create.send(document)
269
- else:
270
- cls.on_update.send(document)
271
- if document.deleted_at:
272
- cls.on_delete.send(document)
268
+ self.save(signal_kwargs={"ignores": ["post_save"]})
273
269
 
274
270
 
275
271
  post_save.connect(Dataservice.post_save, sender=Dataservice)
@@ -35,6 +35,52 @@ class UserDeletedDataset(DatasetRelatedActivity, Activity):
35
35
  label = _("deleted a dataset")
36
36
 
37
37
 
38
+ class UserAddedResourceToDataset(DatasetRelatedActivity, Activity):
39
+ key = "dataset:resource:added"
40
+ icon = "fa fa-plus"
41
+ label = _("added a resource to a dataset")
42
+
43
+
44
+ class UserUpdatedResource(DatasetRelatedActivity, Activity):
45
+ key = "dataset:resource:updated"
46
+ icon = "fa fa-pencil"
47
+ label = _("updated a resource")
48
+
49
+
50
+ class UserRemovedResourceFromDataset(DatasetRelatedActivity, Activity):
51
+ key = "dataset:resource:deleted"
52
+ icon = "fa fa-remove"
53
+ label = _("removed a resource from a dataset")
54
+
55
+
56
+ @Dataset.on_resource_added.connect
57
+ def on_user_added_resource_to_dataset(sender, document, **kwargs):
58
+ if not document.private and current_user and current_user.is_authenticated:
59
+ UserAddedResourceToDataset.emit(
60
+ document, document.organization, None, {"resource_id": str(kwargs["resource_id"])}
61
+ )
62
+
63
+
64
+ @Dataset.on_resource_updated.connect
65
+ def on_user_updated_resource(sender, document, **kwargs):
66
+ changed_fields = kwargs.get("changed_fields", [])
67
+ if not document.private and current_user and current_user.is_authenticated:
68
+ UserUpdatedResource.emit(
69
+ document,
70
+ document.organization,
71
+ changed_fields,
72
+ {"resource_id": str(kwargs["resource_id"])},
73
+ )
74
+
75
+
76
+ @Dataset.on_resource_removed.connect
77
+ def on_user_removed_resource_from_dataset(sender, document, **kwargs):
78
+ if not document.private and current_user and current_user.is_authenticated:
79
+ UserRemovedResourceFromDataset.emit(
80
+ document, document.organization, None, {"resource_id": str(kwargs["resource_id"])}
81
+ )
82
+
83
+
38
84
  @Dataset.on_create.connect
39
85
  def on_user_created_dataset(dataset):
40
86
  if not dataset.private and current_user and current_user.is_authenticated:
@@ -42,9 +88,10 @@ def on_user_created_dataset(dataset):
42
88
 
43
89
 
44
90
  @Dataset.on_update.connect
45
- def on_user_updated_dataset(dataset):
91
+ def on_user_updated_dataset(dataset, **kwargs):
92
+ changed_fields = kwargs.get("changed_fields", [])
46
93
  if not dataset.private and current_user and current_user.is_authenticated:
47
- UserUpdatedDataset.emit(dataset, dataset.organization)
94
+ UserUpdatedDataset.emit(dataset, dataset.organization, changed_fields)
48
95
 
49
96
 
50
97
  @Dataset.on_delete.connect
@@ -19,6 +19,7 @@ from werkzeug.utils import cached_property
19
19
  from udata.api_fields import field
20
20
  from udata.app import cache
21
21
  from udata.core import storages
22
+ from udata.core.activity.models import Auditable
22
23
  from udata.core.owned import Owned, OwnedQuerySet
23
24
  from udata.frontend.markdown import mdstrip
24
25
  from udata.i18n import lazy_gettext as _
@@ -540,45 +541,57 @@ class DatasetBadgeMixin(BadgeMixin):
540
541
  __badges__ = BADGES
541
542
 
542
543
 
543
- class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
544
- title = db.StringField(required=True)
545
- acronym = db.StringField(max_length=128)
544
+ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, db.Document):
545
+ title = field(db.StringField(required=True))
546
+ acronym = field(db.StringField(max_length=128))
546
547
  # /!\ do not set directly the slug when creating or updating a dataset
547
548
  # this will break the search indexation
548
- slug = db.SlugField(
549
- max_length=255, required=True, populate_from="title", update=True, follow=True
549
+ slug = field(
550
+ db.SlugField(
551
+ max_length=255, required=True, populate_from="title", update=True, follow=True
552
+ ),
553
+ auditable=False,
550
554
  )
551
- description = db.StringField(required=True, default="")
552
- license = db.ReferenceField("License")
555
+ description = field(db.StringField(required=True, default=""))
556
+ license = field(db.ReferenceField("License"))
553
557
 
554
- tags = db.TagListField()
555
- resources = db.ListField(db.EmbeddedDocumentField(Resource))
558
+ tags = field(db.TagListField())
559
+ resources = field(db.ListField(db.EmbeddedDocumentField(Resource)), auditable=False)
556
560
 
557
- private = db.BooleanField(default=False)
558
- frequency = db.StringField(choices=list(UPDATE_FREQUENCIES.keys()))
559
- frequency_date = db.DateTimeField(verbose_name=_("Future date of update"))
560
- temporal_coverage = db.EmbeddedDocumentField(db.DateRange)
561
- spatial = db.EmbeddedDocumentField(SpatialCoverage)
562
- schema = db.EmbeddedDocumentField(Schema)
561
+ private = field(db.BooleanField(default=False))
562
+ frequency = field(db.StringField(choices=list(UPDATE_FREQUENCIES.keys())))
563
+ frequency_date = field(db.DateTimeField(verbose_name=_("Future date of update")))
564
+ temporal_coverage = field(db.EmbeddedDocumentField(db.DateRange))
565
+ spatial = field(db.EmbeddedDocumentField(SpatialCoverage))
566
+ schema = field(db.EmbeddedDocumentField(Schema))
563
567
 
564
- ext = db.MapField(db.GenericEmbeddedDocumentField())
565
- extras = db.ExtrasField()
566
- harvest = db.EmbeddedDocumentField(HarvestDatasetMetadata)
568
+ ext = field(db.MapField(db.GenericEmbeddedDocumentField()), auditable=False)
569
+ extras = field(db.ExtrasField(), auditable=False)
570
+ harvest = field(db.EmbeddedDocumentField(HarvestDatasetMetadata), auditable=False)
567
571
 
568
- quality_cached = db.DictField()
572
+ quality_cached = field(db.DictField(), auditable=False)
569
573
 
570
- featured = db.BooleanField(required=True, default=False)
574
+ featured = field(
575
+ db.BooleanField(required=True, default=False),
576
+ auditable=False,
577
+ )
571
578
 
572
- contact_points = db.ListField(db.ReferenceField("ContactPoint", reverse_delete_rule=db.PULL))
579
+ contact_points = field(
580
+ db.ListField(db.ReferenceField("ContactPoint", reverse_delete_rule=db.PULL))
581
+ )
573
582
 
574
- created_at_internal = DateTimeField(
575
- verbose_name=_("Creation date"), default=datetime.utcnow, required=True
583
+ created_at_internal = field(
584
+ DateTimeField(verbose_name=_("Creation date"), default=datetime.utcnow, required=True),
585
+ auditable=False,
576
586
  )
577
- last_modified_internal = DateTimeField(
578
- verbose_name=_("Last modification date"), default=datetime.utcnow, required=True
587
+ last_modified_internal = field(
588
+ DateTimeField(
589
+ verbose_name=_("Last modification date"), default=datetime.utcnow, required=True
590
+ ),
591
+ auditable=False,
579
592
  )
580
- deleted = db.DateTimeField()
581
- archived = db.DateTimeField()
593
+ deleted = field(db.DateTimeField(), auditable=False)
594
+ archived = field(db.DateTimeField())
582
595
 
583
596
  def __str__(self):
584
597
  return self.title or ""
@@ -654,18 +667,6 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
654
667
  def pre_save(cls, sender, document, **kwargs):
655
668
  cls.before_save.send(document)
656
669
 
657
- @classmethod
658
- def post_save(cls, sender, document, **kwargs):
659
- if "post_save" in kwargs.get("ignores", []):
660
- return
661
- cls.after_save.send(document)
662
- if kwargs.get("created"):
663
- cls.on_create.send(document)
664
- else:
665
- cls.on_update.send(document)
666
- if document.deleted:
667
- cls.on_delete.send(document)
668
-
669
670
  def clean(self):
670
671
  super(Dataset, self).clean()
671
672
  if self.frequency in LEGACY_FREQUENCIES:
@@ -1046,19 +1047,19 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
1046
1047
  from udata.models import Discussion
1047
1048
 
1048
1049
  self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
1049
- self.save()
1050
+ self.save(signal_kwargs={"ignores": ["post_save"]})
1050
1051
 
1051
1052
  def count_reuses(self):
1052
1053
  from udata.models import Reuse
1053
1054
 
1054
1055
  self.metrics["reuses"] = Reuse.objects(datasets=self).visible().count()
1055
- self.save()
1056
+ self.save(signal_kwargs={"ignores": ["post_save"]})
1056
1057
 
1057
1058
  def count_followers(self):
1058
1059
  from udata.models import Follow
1059
1060
 
1060
1061
  self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
1061
- self.save()
1062
+ self.save(signal_kwargs={"ignores": ["post_save"]})
1062
1063
 
1063
1064
 
1064
1065
  pre_init.connect(Dataset.pre_init, sender=Dataset)
@@ -8,6 +8,7 @@ class WithMetrics(object):
8
8
  metrics = field(
9
9
  db.DictField(),
10
10
  readonly=True,
11
+ auditable=False,
11
12
  )
12
13
 
13
14
  __metrics_keys__ = []
@@ -32,6 +32,7 @@ def on_user_created_organization(organization):
32
32
 
33
33
 
34
34
  @Organization.on_update.connect
35
- def on_user_updated_organization(organization):
35
+ def on_user_updated_organization(organization, **kwargs):
36
+ changed_fields = kwargs.get("changed_fields", [])
36
37
  if current_user and current_user.is_authenticated:
37
- UserUpdatedOrganization.emit(organization, organization)
38
+ UserUpdatedOrganization.emit(organization, organization, changed_fields)
@@ -42,9 +42,6 @@ org_ref_fields = api.inherit(
42
42
  },
43
43
  )
44
44
 
45
- # This import is not at the top of the file to avoid circular imports
46
- from udata.core.user.api_fields import user_ref_fields # noqa
47
-
48
45
 
49
46
  def check_can_access_user_private_info():
50
47
  # This endpoint is secure, only organization member has access.
@@ -64,14 +61,18 @@ def check_can_access_user_private_info():
64
61
  def member_email_with_visibility_check(email):
65
62
  if current_user_is_admin_or_self():
66
63
  return email
64
+ name, domain = email.split("@")
67
65
  if check_can_access_user_private_info():
68
66
  # Obfuscate email partially for other members
69
- name, domain = email.split("@")
70
67
  name = name[:2] + "*" * (len(name) - 2)
71
68
  return f"{name}@{domain}"
72
- return None
69
+ # Return only domain for other users
70
+ return f"***@{domain}"
73
71
 
74
72
 
73
+ # This import is not at the top of the file to avoid circular imports
74
+ from udata.core.user.api_fields import user_ref_fields # noqa
75
+
75
76
  member_user_with_email_fields = api.inherit(
76
77
  "MemberUserWithEmail",
77
78
  user_ref_fields,
@@ -6,6 +6,7 @@ from mongoengine.signals import post_save, pre_save
6
6
  from werkzeug.utils import cached_property
7
7
 
8
8
  from udata.api_fields import field
9
+ from udata.core.activity.models import Auditable
9
10
  from udata.core.badges.models import Badge, BadgeMixin, BadgesList
10
11
  from udata.core.metrics.models import WithMetrics
11
12
  from udata.core.storages import avatars, default_image_basename
@@ -110,29 +111,35 @@ class OrganizationBadgeMixin(BadgeMixin):
110
111
  __badges__ = BADGES
111
112
 
112
113
 
113
- class Organization(WithMetrics, OrganizationBadgeMixin, db.Datetimed, db.Document):
114
- name = db.StringField(required=True)
115
- acronym = db.StringField(max_length=128)
116
- slug = db.SlugField(
117
- max_length=255, required=True, populate_from="name", update=True, follow=True
114
+ class Organization(Auditable, WithMetrics, OrganizationBadgeMixin, db.Datetimed, db.Document):
115
+ name = field(db.StringField(required=True))
116
+ acronym = field(db.StringField(max_length=128))
117
+ slug = field(
118
+ db.SlugField(max_length=255, required=True, populate_from="name", update=True, follow=True),
119
+ auditable=False,
118
120
  )
119
- description = db.StringField(required=True)
120
- url = db.URLField()
121
- image_url = db.StringField()
122
- logo = db.ImageField(
123
- fs=avatars, basename=default_image_basename, max_size=LOGO_MAX_SIZE, thumbnails=LOGO_SIZES
121
+ description = field(db.StringField(required=True))
122
+ url = field(db.URLField())
123
+ image_url = field(db.StringField())
124
+ logo = field(
125
+ db.ImageField(
126
+ fs=avatars,
127
+ basename=default_image_basename,
128
+ max_size=LOGO_MAX_SIZE,
129
+ thumbnails=LOGO_SIZES,
130
+ )
124
131
  )
125
- business_number_id = db.StringField(max_length=ORG_BID_SIZE_LIMIT)
132
+ business_number_id = field(db.StringField(max_length=ORG_BID_SIZE_LIMIT))
126
133
 
127
- members = db.ListField(db.EmbeddedDocumentField(Member))
128
- teams = db.ListField(db.EmbeddedDocumentField(Team))
129
- requests = db.ListField(db.EmbeddedDocumentField(MembershipRequest))
134
+ members = field(db.ListField(db.EmbeddedDocumentField(Member)))
135
+ teams = field(db.ListField(db.EmbeddedDocumentField(Team)))
136
+ requests = field(db.ListField(db.EmbeddedDocumentField(MembershipRequest)))
130
137
 
131
- ext = db.MapField(db.GenericEmbeddedDocumentField())
132
- zone = db.StringField()
133
- extras = db.OrganizationExtrasField()
138
+ ext = field(db.MapField(db.GenericEmbeddedDocumentField()))
139
+ zone = field(db.StringField())
140
+ extras = field(db.OrganizationExtrasField(), auditable=False)
134
141
 
135
- deleted = db.DateTimeField()
142
+ deleted = field(db.DateTimeField())
136
143
 
137
144
  meta = {
138
145
  "indexes": [
@@ -168,19 +175,12 @@ class Organization(WithMetrics, OrganizationBadgeMixin, db.Datetimed, db.Documen
168
175
  on_update = Signal()
169
176
  before_delete = Signal()
170
177
  after_delete = Signal()
178
+ on_delete = Signal()
171
179
 
172
180
  @classmethod
173
181
  def pre_save(cls, sender, document, **kwargs):
174
182
  cls.before_save.send(document)
175
183
 
176
- @classmethod
177
- def post_save(cls, sender, document, **kwargs):
178
- cls.after_save.send(document)
179
- if kwargs.get("created"):
180
- cls.on_create.send(document)
181
- else:
182
- cls.on_update.send(document)
183
-
184
184
  def url_for(self, *args, **kwargs):
185
185
  return endpoint_for("organizations.show", "api.organization", org=self, *args, **kwargs)
186
186
 
@@ -296,31 +296,31 @@ class Organization(WithMetrics, OrganizationBadgeMixin, db.Datetimed, db.Documen
296
296
 
297
297
  def count_members(self):
298
298
  self.metrics["members"] = len(self.members)
299
- self.save()
299
+ self.save(signal_kwargs={"ignores": ["post_save"]})
300
300
 
301
301
  def count_datasets(self):
302
302
  from udata.models import Dataset
303
303
 
304
304
  self.metrics["datasets"] = Dataset.objects(organization=self).visible().count()
305
- self.save()
305
+ self.save(signal_kwargs={"ignores": ["post_save"]})
306
306
 
307
307
  def count_reuses(self):
308
308
  from udata.models import Reuse
309
309
 
310
310
  self.metrics["reuses"] = Reuse.objects(organization=self).visible().count()
311
- self.save()
311
+ self.save(signal_kwargs={"ignores": ["post_save"]})
312
312
 
313
313
  def count_dataservices(self):
314
314
  from udata.models import Dataservice
315
315
 
316
316
  self.metrics["dataservices"] = Dataservice.objects(organization=self).visible().count()
317
- self.save()
317
+ self.save(signal_kwargs={"ignores": ["post_save"]})
318
318
 
319
319
  def count_followers(self):
320
320
  from udata.models import Follow
321
321
 
322
322
  self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
323
- self.save()
323
+ self.save(signal_kwargs={"ignores": ["post_save"]})
324
324
 
325
325
 
326
326
  pre_save.connect(Organization.pre_save, sender=Organization)
udata/core/owned.py CHANGED
@@ -80,7 +80,7 @@ def check_organization_is_valid_for_current_user(organization, **_kwargs):
80
80
 
81
81
  class Owned(object):
82
82
  """
83
- A mixin to factorize owning behvaior between users and organizations.
83
+ A mixin to factorize owning behavior between users and organizations.
84
84
  """
85
85
 
86
86
  owner = field(
@@ -38,9 +38,10 @@ def on_user_created_reuse(reuse):
38
38
 
39
39
 
40
40
  @Reuse.on_update.connect
41
- def on_user_updated_reuse(reuse):
41
+ def on_user_updated_reuse(reuse, **kwargs):
42
+ changed_fields = kwargs.get("changed_fields", [])
42
43
  if not reuse.private and current_user and current_user.is_authenticated:
43
- UserUpdatedReuse.emit(reuse, reuse.organization)
44
+ UserUpdatedReuse.emit(reuse, reuse.organization, changed_fields)
44
45
 
45
46
 
46
47
  @Reuse.on_delete.connect
@@ -3,6 +3,7 @@ from mongoengine.signals import post_save, pre_save
3
3
  from werkzeug.utils import cached_property
4
4
 
5
5
  from udata.api_fields import field, function_field, generate_fields
6
+ from udata.core.activity.models import Auditable
6
7
  from udata.core.dataset.api_fields import dataset_fields
7
8
  from udata.core.owned import Owned, OwnedQuerySet
8
9
  from udata.core.reuse.api_fields import BIGGEST_IMAGE_SIZE
@@ -60,7 +61,7 @@ class ReuseBadgeMixin(BadgeMixin):
60
61
  additional_filters={"organization_badge": "organization.badges"},
61
62
  mask="*,datasets{id,title,uri,page}",
62
63
  )
63
- class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
64
+ class Reuse(db.Datetimed, Auditable, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
64
65
  title = field(
65
66
  db.StringField(required=True),
66
67
  sortable=True,
@@ -71,6 +72,7 @@ class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
71
72
  max_length=255, required=True, populate_from="title", update=True, follow=True
72
73
  ),
73
74
  readonly=True,
75
+ auditable=False,
74
76
  )
75
77
  description = field(
76
78
  db.StringField(required=True),
@@ -126,15 +128,17 @@ class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
126
128
  private = field(db.BooleanField(default=False), filterable={})
127
129
 
128
130
  ext = db.MapField(db.GenericEmbeddedDocumentField())
129
- extras = field(db.ExtrasField())
131
+ extras = field(db.ExtrasField(), auditable=False)
130
132
 
131
133
  featured = field(
132
134
  db.BooleanField(),
133
135
  filterable={},
134
136
  readonly=True,
137
+ auditable=False,
135
138
  )
136
139
  deleted = field(
137
140
  db.DateTimeField(),
141
+ auditable=False,
138
142
  )
139
143
  archived = field(
140
144
  db.DateTimeField(),
@@ -181,18 +185,6 @@ class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
181
185
  # Emit before_save
182
186
  cls.before_save.send(document)
183
187
 
184
- @classmethod
185
- def post_save(cls, sender, document, **kwargs):
186
- if "post_save" in kwargs.get("ignores", []):
187
- return
188
- cls.after_save.send(document)
189
- if kwargs.get("created"):
190
- cls.on_create.send(document)
191
- else:
192
- cls.on_update.send(document)
193
- if document.deleted:
194
- cls.on_delete.send(document)
195
-
196
188
  def url_for(self, *args, **kwargs):
197
189
  return endpoint_for("reuses.show", "api.reuse", reuse=self, *args, **kwargs)
198
190
 
@@ -289,13 +281,13 @@ class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
289
281
  from udata.models import Discussion
290
282
 
291
283
  self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
292
- self.save()
284
+ self.save(signal_kwargs={"ignores": ["post_save"]})
293
285
 
294
286
  def count_followers(self):
295
287
  from udata.models import Follow
296
288
 
297
289
  self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
298
- self.save()
290
+ self.save(signal_kwargs={"ignores": ["post_save"]})
299
291
 
300
292
 
301
293
  pre_save.connect(Reuse.pre_save, sender=Reuse)
@@ -1,5 +1,7 @@
1
1
  from flask_security import current_user
2
2
 
3
+ from udata.core.dataservices.activities import DataserviceRelatedActivity
4
+ from udata.core.dataservices.models import Dataservice
3
5
  from udata.core.dataset.activities import DatasetRelatedActivity
4
6
  from udata.core.discussions.signals import on_new_discussion, on_new_discussion_comment
5
7
  from udata.core.followers.signals import on_follow
@@ -40,6 +42,11 @@ class UserFollowedUser(FollowActivity, Activity):
40
42
  template = "activity/user.html"
41
43
 
42
44
 
45
+ class UserDiscussedDataservice(DiscussActivity, DataserviceRelatedActivity, Activity):
46
+ key = "dataservice:discussed"
47
+ label = _("discussed a dataservice")
48
+
49
+
43
50
  class UserDiscussedDataset(DiscussActivity, DatasetRelatedActivity, Activity):
44
51
  key = "dataset:discussed"
45
52
  label = _("discussed a dataset")
@@ -50,6 +57,11 @@ class UserDiscussedReuse(DiscussActivity, ReuseRelatedActivity, Activity):
50
57
  label = _("discussed a reuse")
51
58
 
52
59
 
60
+ class UserFollowedDataservice(FollowActivity, DataserviceRelatedActivity, Activity):
61
+ key = "dataservice:followed"
62
+ label = _("followed a dataservice")
63
+
64
+
53
65
  class UserFollowedDataset(FollowActivity, DatasetRelatedActivity, Activity):
54
66
  key = "dataset:followed"
55
67
  label = _("followed a dataset")
@@ -68,7 +80,9 @@ class UserFollowedOrganization(FollowActivity, OrgRelatedActivity, Activity):
68
80
  @on_follow.connect
69
81
  def write_activity_on_follow(follow, **kwargs):
70
82
  if current_user.is_authenticated:
71
- if isinstance(follow.following, Dataset):
83
+ if isinstance(follow.following, Dataservice):
84
+ UserFollowedDataservice.emit(follow.following)
85
+ elif isinstance(follow.following, Dataset):
72
86
  UserFollowedDataset.emit(follow.following)
73
87
  elif isinstance(follow.following, Reuse):
74
88
  UserFollowedReuse.emit(follow.following)
@@ -82,6 +96,8 @@ def write_activity_on_follow(follow, **kwargs):
82
96
  @on_new_discussion_comment.connect
83
97
  def write_activity_on_discuss(discussion, **kwargs):
84
98
  if current_user.is_authenticated:
99
+ if isinstance(discussion.subject, Dataservice):
100
+ UserDiscussedDataservice.emit(discussion.subject)
85
101
  if isinstance(discussion.subject, Dataset):
86
102
  UserDiscussedDataset.emit(discussion.subject)
87
103
  elif isinstance(discussion.subject, Reuse):
udata/core/user/api.py CHANGED
@@ -409,6 +409,7 @@ class SuggestUsersAPI(API):
409
409
  "first_name": user.first_name,
410
410
  "last_name": user.last_name,
411
411
  "avatar_url": user.avatar,
412
+ "email": user.email,
412
413
  "slug": user.slug,
413
414
  }
414
415
  for user in users.order_by(DEFAULT_SORTING).limit(args["size"])
@@ -30,7 +30,7 @@ user_ref_fields = api.inherit(
30
30
  },
31
31
  )
32
32
 
33
- from udata.core.organization.api_fields import org_ref_fields # noqa
33
+ from udata.core.organization.api_fields import member_email_with_visibility_check, org_ref_fields # noqa
34
34
 
35
35
  user_fields = api.model(
36
36
  "User",
@@ -126,6 +126,11 @@ user_suggestion_fields = api.model(
126
126
  "avatar_url": fields.ImageField(
127
127
  size=BIGGEST_AVATAR_SIZE, description="The user avatar URL", readonly=True
128
128
  ),
129
+ "email": fields.Raw(
130
+ attribute=lambda o: member_email_with_visibility_check(o["email"]),
131
+ description="The user email (only the domain for non-admin user)",
132
+ readonly=True,
133
+ ),
129
134
  "slug": fields.String(description="The user permalink string", readonly=True),
130
135
  },
131
136
  )
udata/core/user/models.py CHANGED
@@ -12,6 +12,7 @@ from mongoengine.signals import post_save, pre_save
12
12
  from werkzeug.utils import cached_property
13
13
 
14
14
  from udata import mail
15
+ from udata.api_fields import field
15
16
  from udata.core import storages
16
17
  from udata.core.discussions.models import Discussion
17
18
  from udata.core.storages import avatars, default_image_basename
@@ -42,49 +43,53 @@ class UserSettings(db.EmbeddedDocument):
42
43
 
43
44
 
44
45
  class User(WithMetrics, UserMixin, db.Document):
45
- slug = db.SlugField(max_length=255, required=True, populate_from="fullname")
46
- email = db.StringField(max_length=255, required=True, unique=True)
47
- password = db.StringField()
48
- active = db.BooleanField()
49
- fs_uniquifier = db.StringField(max_length=64, unique=True, sparse=True)
50
- roles = db.ListField(db.ReferenceField(Role), default=[])
46
+ slug = field(
47
+ db.SlugField(max_length=255, required=True, populate_from="fullname"), auditable=False
48
+ )
49
+ email = field(db.StringField(max_length=255, required=True, unique=True))
50
+ password = field(db.StringField())
51
+ active = field(db.BooleanField())
52
+ fs_uniquifier = field(db.StringField(max_length=64, unique=True, sparse=True))
53
+ roles = field(db.ListField(db.ReferenceField(Role), default=[]))
51
54
 
52
- first_name = db.StringField(max_length=255, required=True)
53
- last_name = db.StringField(max_length=255, required=True)
55
+ first_name = field(db.StringField(max_length=255, required=True))
56
+ last_name = field(db.StringField(max_length=255, required=True))
54
57
 
55
- avatar_url = db.URLField()
56
- avatar = db.ImageField(fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES)
57
- website = db.URLField()
58
- about = db.StringField()
58
+ avatar_url = field(db.URLField())
59
+ avatar = field(
60
+ db.ImageField(fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES)
61
+ )
62
+ website = field(db.URLField())
63
+ about = field(db.StringField())
59
64
 
60
- prefered_language = db.StringField()
65
+ prefered_language = field(db.StringField())
61
66
 
62
- apikey = db.StringField()
67
+ apikey = field(db.StringField())
63
68
 
64
- created_at = db.DateTimeField(default=datetime.utcnow, required=True)
69
+ created_at = field(db.DateTimeField(default=datetime.utcnow, required=True), auditable=False)
65
70
 
66
71
  # The field below is required for Flask-security
67
72
  # when SECURITY_CONFIRMABLE is True
68
- confirmed_at = db.DateTimeField()
73
+ confirmed_at = field(db.DateTimeField(), auditable=False)
69
74
 
70
- password_rotation_demanded = db.DateTimeField()
71
- password_rotation_performed = db.DateTimeField()
75
+ password_rotation_demanded = field(db.DateTimeField(), auditable=False)
76
+ password_rotation_performed = field(db.DateTimeField(), auditable=False)
72
77
 
73
78
  # The 5 fields below are required for Flask-security
74
79
  # when SECURITY_TRACKABLE is True
75
- last_login_at = db.DateTimeField()
76
- current_login_at = db.DateTimeField()
77
- last_login_ip = db.StringField()
78
- current_login_ip = db.StringField()
79
- login_count = db.IntField()
80
+ last_login_at = field(db.DateTimeField(), auditable=False)
81
+ current_login_at = field(db.DateTimeField(), auditable=False)
82
+ last_login_ip = field(db.StringField(), auditable=False)
83
+ current_login_ip = field(db.StringField(), auditable=False)
84
+ login_count = field(db.IntField(), auditable=False)
80
85
 
81
- deleted = db.DateTimeField()
82
- ext = db.MapField(db.GenericEmbeddedDocumentField())
83
- extras = db.ExtrasField()
86
+ deleted = field(db.DateTimeField())
87
+ ext = field(db.MapField(db.GenericEmbeddedDocumentField()))
88
+ extras = field(db.ExtrasField(), auditable=False)
84
89
 
85
90
  # Used to track notification for automatic inactive users deletion
86
91
  # when YEARS_OF_INACTIVITY_BEFORE_DELETION is set
87
- inactive_deletion_notified_at = db.DateTimeField()
92
+ inactive_deletion_notified_at = field(db.DateTimeField(), auditable=False)
88
93
 
89
94
  before_save = Signal()
90
95
  after_save = Signal()
@@ -209,6 +214,8 @@ class User(WithMetrics, UserMixin, db.Document):
209
214
 
210
215
  @classmethod
211
216
  def post_save(cls, sender, document, **kwargs):
217
+ if "post_save" in kwargs.get("ignores", []):
218
+ return
212
219
  cls.after_save.send(document)
213
220
  if kwargs.get("created"):
214
221
  cls.on_create.send(document)
@@ -294,31 +301,31 @@ class User(WithMetrics, UserMixin, db.Document):
294
301
  from udata.models import Dataset
295
302
 
296
303
  self.metrics["datasets"] = Dataset.objects(owner=self).visible().count()
297
- self.save()
304
+ self.save(signal_kwargs={"ignores": ["post_save"]})
298
305
 
299
306
  def count_reuses(self):
300
307
  from udata.models import Reuse
301
308
 
302
309
  self.metrics["reuses"] = Reuse.objects(owner=self).visible().count()
303
- self.save()
310
+ self.save(signal_kwargs={"ignores": ["post_save"]})
304
311
 
305
312
  def count_dataservices(self):
306
313
  from udata.core.dataservices.models import Dataservice
307
314
 
308
315
  self.metrics["dataservices"] = Dataservice.objects(owner=self).visible().count()
309
- self.save()
316
+ self.save(signal_kwargs={"ignores": ["post_save"]})
310
317
 
311
318
  def count_followers(self):
312
319
  from udata.models import Follow
313
320
 
314
321
  self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
315
- self.save()
322
+ self.save(signal_kwargs={"ignores": ["post_save"]})
316
323
 
317
324
  def count_following(self):
318
325
  from udata.models import Follow
319
326
 
320
327
  self.metrics["following"] = Follow.objects.following(self).count()
321
- self.save()
328
+ self.save(signal_kwargs={"ignores": ["post_save"]})
322
329
 
323
330
 
324
331
  datastore = MongoEngineUserDatastore(db, User, Role)
@@ -67,6 +67,7 @@ class Datetimed(object):
67
67
  ),
68
68
  sortable=True,
69
69
  readonly=True,
70
+ auditable=False,
70
71
  )
71
72
 
72
73
 
@@ -362,10 +362,10 @@ class MembershipAPITest:
362
362
  assert len(members) == 2
363
363
  assert members[0]["role"] == "admin"
364
364
  assert members[0]["since"] == "2024-04-14T00:00:00+00:00"
365
- assert members[0]["user"]["email"] is None
365
+ assert members[0]["user"]["email"] == "***@example.org"
366
366
 
367
367
  assert members[1]["role"] == "editor"
368
- assert members[1]["user"]["email"] is None
368
+ assert members[1]["user"]["email"] == "***@example.org"
369
369
 
370
370
  # Super admin of udata can see emails
371
371
  api.login(AdminFactory())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udata
3
- Version: 10.4.2.dev35266
3
+ Version: 10.4.2.dev35319
4
4
  Summary: Open data portal
5
5
  Home-page: https://github.com/opendatateam/udata
6
6
  Author: Opendata Team
@@ -141,7 +141,9 @@ It is collectively taken care of by members of the
141
141
 
142
142
  ## Current (in progress)
143
143
 
144
- - Do not crash if file doesn't exists during resource deletion [#3323](https://github.com/opendatateam/udata/pull/3323)
144
+ - Add activities to dataservices and resources, add Auditable class to refactor improve code [#3308](https://github.com/opendatateam/udata/pull/3308)
145
+ - Do not crash if file doesn't exists during resource deletion [#3323](https://github.com/opendatateam/udata/pull/3323)
146
+ - Show user domain in suggest [#3324](https://github.com/opendatateam/udata/pull/3324)
145
147
 
146
148
  ## 10.4.1 (2025-05-20)
147
149
 
@@ -55,12 +55,12 @@ udata/commands/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
55
55
  udata/commands/tests/test_fixtures.py,sha256=acFmE08S4B0GHJP4nN1dfRNcKjcsE88XusyW_HT12SE,5554
56
56
  udata/core/__init__.py,sha256=O7C9WWCXiLWnWPnPbFRszWhOmvRQiI4gD-5qkWvPGRo,385
57
57
  udata/core/csv.py,sha256=qeRtSQXPT5n5EoWZ3_XfnOTW7ITnEzzHctODPeX63Uk,8543
58
- udata/core/owned.py,sha256=LCnnKzx1h1oDc_YVbYudmcnIRwo7GKpOcRsZKxwZfUk,5561
59
- udata/core/activity/__init__.py,sha256=OaiFyq7HB4xL4SuMPD1N8IFNpntwx9ZayVzelciOieI,298
60
- udata/core/activity/api.py,sha256=uSWCFin5nBfrFyOZxbSulFiwWagnEWF996sy-rv-Dmw,3855
61
- udata/core/activity/models.py,sha256=Ln9YUsdCBMFmUlw5GA2gIJ50sMorMFzZGo8u8sy7pIE,2072
58
+ udata/core/owned.py,sha256=OQT7wdk7dAqGvWDiJRVkKJxerDc9_Io910nvLmfBAVI,5561
59
+ udata/core/activity/__init__.py,sha256=U4e1qgBwiz3Lc7lbhIji3p1WVGsUg5GftfDrxFZQu5Q,352
60
+ udata/core/activity/api.py,sha256=ECDnqDlOeWXtjumPZKTyOWHY8mMQr7KFQTF-tbO3jss,3944
61
+ udata/core/activity/models.py,sha256=ipViQMB1UMgxd9nolueanEgOB31s9pihTqkDdfo11u4,3214
62
62
  udata/core/activity/signals.py,sha256=Io2A43as3yR-DZ5R3wzM_bTpn528pxWsZDUFZ9xtj2Y,191
63
- udata/core/activity/tasks.py,sha256=lEnISnHXPMeeErWIhJbSYOlxnrc6yAKC-T28qTEWQn0,1235
63
+ udata/core/activity/tasks.py,sha256=F3PY12dnpT5Z8VuYfuOyDP6VPKPJmq1Sm4lSiPfmUCA,1498
64
64
  udata/core/badges/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  udata/core/badges/api.py,sha256=PkOx-bA9dFXIQZhUYg8r5OLBo1aNJiUJbKVCPOS7wm0,762
66
66
  udata/core/badges/commands.py,sha256=J6dKsoZmCbWM3HHOktPt-AHoMj-AhNPL0jxiV4yVlgw,1143
@@ -81,19 +81,20 @@ udata/core/contact_point/factories.py,sha256=YoW2PKKaemYO4lIy5MwpH36uXM_J3rE-Ihs
81
81
  udata/core/contact_point/forms.py,sha256=oBe1agSJFyx2QRgYzPRg2A7qVscaBTaKG4V-AyIwnF8,729
82
82
  udata/core/contact_point/models.py,sha256=Xqmqg7S13gcaKxiQT52WHeQEHTaUDDGIXInXyqNh4Po,854
83
83
  udata/core/dataservices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
+ udata/core/dataservices/activities.py,sha256=0gYJACjhHThQyiXcLD2gX858QjepXu8EUM3nVEp656w,1703
84
85
  udata/core/dataservices/api.py,sha256=7jOn3Ug63-5wjXP0d_zL0whdCQeHG_eQ1Y-wNhUuPLM,7388
85
86
  udata/core/dataservices/apiv2.py,sha256=XIqJq58uLtxtt52iKYo7Fl0ZXv9Sz72uA7JIGwP8Qos,995
86
87
  udata/core/dataservices/constants.py,sha256=LBCTQ44YvjTZfQVbxFiovPxapAknSJNOZKjMP8WeFy4,351
87
88
  udata/core/dataservices/csv.py,sha256=pcNIeGaCzBMMna3n3YqHjsoXzfLtg_ITtDmdKb9svDc,1053
88
89
  udata/core/dataservices/factories.py,sha256=LDk8vvG0zhW8J-ZX5LoJQDU13pqeIyjQ05muuMro_eA,876
89
- udata/core/dataservices/models.py,sha256=2HkVDBcseLdtJIF9jCdFVPMVYcHhlw3zRkd7iFHqymw,10002
90
+ udata/core/dataservices/models.py,sha256=xn4iq9Io9GojUY53U9tBx0ltwJTKICkeiFHltVWejaw,9930
90
91
  udata/core/dataservices/permissions.py,sha256=98zM_R4v2ZtRubflB7ajaVQz-DVc-pZBMgtKUYy34oI,169
91
92
  udata/core/dataservices/rdf.py,sha256=P-lSAKKy0Kbcfw_tGjl25DuqX77Sg4qZZYJtb9pzcOc,7847
92
93
  udata/core/dataservices/search.py,sha256=Wsr51jU14D9NrNP3ELTc8swwtFb-dakgc55cDrtt3No,4639
93
94
  udata/core/dataservices/tasks.py,sha256=d2tG1l6u8-eUKUYBOgnCsQLbLmLgJXU-DOzZWhhL6Qg,897
94
95
  udata/core/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
96
  udata/core/dataset/actions.py,sha256=mX6xox0PiMrbcAPZ3VZsI26rfM-ciYfEXxN6sqqImKA,1222
96
- udata/core/dataset/activities.py,sha256=v8k1jwhdx62Z2ARZq8Q-x86OWSsBK99hRloPl74OCgA,1502
97
+ udata/core/dataset/activities.py,sha256=FD5A16cCyPvMY5lEEtQX3ENlaA1pao5Et8FbbHPpLgg,3195
97
98
  udata/core/dataset/api.py,sha256=jElQZuguc514Eb0cWdquEfosP1yB79hEQ52SV_SvLx8,33282
98
99
  udata/core/dataset/api_fields.py,sha256=SLuzWoPdMLPX28WQ9DRGUPKS27vlltiFeiTo6jXa55Q,17549
99
100
  udata/core/dataset/apiv2.py,sha256=YOCNqQh7_OODcrzqJqdAPc0CoB9vG0DVcFiJNl9yBwQ,20749
@@ -104,7 +105,7 @@ udata/core/dataset/events.py,sha256=bSM0nFEX14r4JHc-bAM-7OOuD3JAxUIpw9GgXbOsUyw,
104
105
  udata/core/dataset/exceptions.py,sha256=uKiayLSpSzsnLvClObS6hOO0qXEqvURKN7_w8eimQNU,498
105
106
  udata/core/dataset/factories.py,sha256=fRDWDlybR_ud4pDs1-ntWuYHKtV9LMHeBOBp2SmTT6M,9006
106
107
  udata/core/dataset/forms.py,sha256=7KUxuFcEGT0MUe0cZCiZtsnZhvGgvEd68pe13NgeSMI,6292
107
- udata/core/dataset/models.py,sha256=5f1ZNIMXbD1LuXyR2uNcGM-wgR67EkzhxcXSwUjrfrQ,40922
108
+ udata/core/dataset/models.py,sha256=BiqwN1mLffc4_LqtQ6KcNK1BIh04cVKFhyOw3n3BIK8,41161
108
109
  udata/core/dataset/permissions.py,sha256=zXQ6kU-Ni3Pl5tDtat-ZPupug9InsNeCN7xRLc2Vcrc,1097
109
110
  udata/core/dataset/preview.py,sha256=IwCqiNTjjXbtA_SSKF52pwnzKKEz0GyYM95QNn2Dkog,2561
110
111
  udata/core/dataset/rdf.py,sha256=HkjzcWgq9AfPvUGMRI7-ufRrgnlfBmP8crbgRhg6Lz4,31789
@@ -138,13 +139,13 @@ udata/core/jobs/forms.py,sha256=B-B6jXHZsYV-PWAkD8DLoOlh6trv4l1hGZ4HOPm-PD4,1495
138
139
  udata/core/jobs/models.py,sha256=xK8T3FCmhtERNbZmh1Tq_ZTO6vojM172tTc0oplNoQ0,1358
139
140
  udata/core/metrics/__init__.py,sha256=CB0MhZqNe4gKLBO-6zoGisXK7FtavxWx_Kkkle-INrc,398
140
141
  udata/core/metrics/commands.py,sha256=mYozJGER55fHXaQQrlDig4clVFMHWS-8p34XnJGyehs,6003
141
- udata/core/metrics/models.py,sha256=17EDq6GnCI_splm0K_Y12vj5UKwsA6VglykjzQISp00,328
142
+ udata/core/metrics/models.py,sha256=Sv2Qhmqdug_atfDc_pyOJCb-8ACpvU-_0FFVPVOFDGk,353
142
143
  udata/core/metrics/signals.py,sha256=9mdJW__gR2GJT3huBr6HN2SDhKYJRgNbW9dnh48cAnU,176
143
144
  udata/core/metrics/tasks.py,sha256=_3L2g3blndBID66s_CauXEW2bDYathjd4HAAhYNVaOs,827
144
145
  udata/core/organization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
- udata/core/organization/activities.py,sha256=NY9pDzOVcdo2HZ-ax77lioiDGUZ8jpUguSLt_824saQ,1133
146
+ udata/core/organization/activities.py,sha256=Mw4-R8Q6G745IZnCDgrj7h2ax2crGYRhZtcewSK6_Ok,1213
146
147
  udata/core/organization/api.py,sha256=7mpPEBglamXWDZrfcp4MUosO3d-1yp4Ywkm06wujstA,20430
147
- udata/core/organization/api_fields.py,sha256=d_lgqjC0T-sPOqibxTLkW-to2t6fJX29NEQiKrdZHz4,7997
148
+ udata/core/organization/api_fields.py,sha256=y-YcBPBPRwDKMdrhIDKWJHe3FAti7qCTvbH5h8zLvOw,8045
148
149
  udata/core/organization/apiv2.py,sha256=fLMN5T7s8yF4u7c1WWHXMqQAlyH2OCSyAfmleiFlf7M,3037
149
150
  udata/core/organization/commands.py,sha256=DsRAtFDZvTciYNsUWumQWdn0jnNmKW-PwfIHUUZoBb8,1591
150
151
  udata/core/organization/constants.py,sha256=fncNtA-vFrRM22K1Wo6iYu9DQZjzknYxH6TIYfxM9kA,563
@@ -152,7 +153,7 @@ udata/core/organization/csv.py,sha256=YzbdD9Y333QY2j9_sxdmRjAlHg1aGV-oW-dvZdvajV
152
153
  udata/core/organization/factories.py,sha256=g8ubBcz79xbjvpunZ02IDOakFg1KE6cXjNkE9vFyFAc,624
153
154
  udata/core/organization/forms.py,sha256=tscDb1_yOpbTx3ahl8ttA7oDkX9jIyzLc4gOf6WbN3s,3552
154
155
  udata/core/organization/metrics.py,sha256=90romzr-FhnPKh-6UHBJ1Af2loDa4-8I1iZEgztA160,1062
155
- udata/core/organization/models.py,sha256=Nu7koTyCjdOtrPczI-UXw0OdKDaduSXYOYqYSIUPrHs,9362
156
+ udata/core/organization/models.py,sha256=QFRE8_4caiE34g5t2bNJ_GKk4WCVlobnr5_HZVf2pqw,9616
156
157
  udata/core/organization/notifications.py,sha256=i_36-l2y7fOGmnBmr5NDWmGGmrGRaCWbU-6XS4c2wQs,917
157
158
  udata/core/organization/permissions.py,sha256=GD-9TMtRppVCPaC1ysXYrONvGJV-ArzAOXm2XMKf9yo,1256
158
159
  udata/core/organization/rdf.py,sha256=TF2c85MHAu-TRiHNLxqV_Pw5z6sCgZrNszF9SYspQpk,1936
@@ -173,7 +174,7 @@ udata/core/reports/api.py,sha256=2xKsYC93V3md2ahkZs--O_0dTWQTicES5_9jbfER3hM,155
173
174
  udata/core/reports/constants.py,sha256=LRZSX3unyqZeB4yQjK3ws_hGbJcXYk4bu1Rhnhi5DEs,1235
174
175
  udata/core/reports/models.py,sha256=AsW5p2ZIdR4c6vNzglEN7MX03It-t9u7ktOsVZqvzSs,2702
175
176
  udata/core/reuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
- udata/core/reuse/activities.py,sha256=mAdHhqqpUF5zSh4e5AEo0J7alc3RflTbudDaKOzyTQw,1406
177
+ udata/core/reuse/activities.py,sha256=3eh3zS4eH_199rdwIW0vtrYdKAZ8W3qRbrt_CRLNZXY,1486
177
178
  udata/core/reuse/api.py,sha256=931cn6CGVcHv0XhSAXyjHkKy-9nOGHaHdE3uMU8v9mc,10928
178
179
  udata/core/reuse/api_fields.py,sha256=c61Gl56UjiBpXS0Nbvcoi_QHdUmhnBtqWm6nNHRYKyc,1232
179
180
  udata/core/reuse/apiv2.py,sha256=nZe-v8713aXKuv2B578NdxfrIckjbxhS3zUXAKSIKTI,835
@@ -181,7 +182,7 @@ udata/core/reuse/constants.py,sha256=JgDBrjOKSt9q0auv9rjzbGsch83H-Oi8YXAKeI5hO4o
181
182
  udata/core/reuse/csv.py,sha256=hCvKYzbL6kiUwzHkd9jrexdeACBj5k7XycKaLZesQeo,892
182
183
  udata/core/reuse/factories.py,sha256=GrQqYTIvwQrwkvJrbTr38-2faFW_PC99gn3yOVpgFec,850
183
184
  udata/core/reuse/metrics.py,sha256=sVh7BlW3OKRvFDHFyD4pPUV91jOOhj8qeWbBkLPn5Gg,176
184
- udata/core/reuse/models.py,sha256=Mb8us6MXaIHhKRVtdAE-9bPdMUm91EUeb_dBOLbI3FM,8669
185
+ udata/core/reuse/models.py,sha256=2W0KFQRSUXOfMl-jMWf6TvlgLW4bxoIQ_ceWYq_WdFM,8521
185
186
  udata/core/reuse/permissions.py,sha256=j-ancS7gvLl5vJu0TNYqpYD-2So-UzoDE4IHLxRoMGg,621
186
187
  udata/core/reuse/search.py,sha256=y1DwXYkBMBwuhn62CULkU1NNo89IYp0Ae7U01jcnjBY,3137
187
188
  udata/core/reuse/signals.py,sha256=nDrEUpYKN0AdYiEbrR0z3nzXzjaRcD8SAMutwIDsQPM,155
@@ -236,16 +237,16 @@ udata/core/topic/models.py,sha256=X-jScC_mMNdZp0hQ_SD-NBHsIPS8aYqpq99x6l4dKz4,20
236
237
  udata/core/topic/parsers.py,sha256=ugkBd-w8TewInqowNF2w36UPwKYMYluK4U-grkAu-wg,2411
237
238
  udata/core/topic/permissions.py,sha256=RtFPPlxuU_Bv7ip6LDO4AoPrKFnIOEs9cCMXaSSmEdk,118
238
239
  udata/core/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
239
- udata/core/user/activities.py,sha256=x-mSiwx7TzM2ci6KMkMiNx0sFs8J3LTJNbigtE8cA0s,2807
240
- udata/core/user/api.py,sha256=zqGTUgiMyv1sNs2HTvIJIcoqKfjcWqplH9iCTEI3Nic,13436
241
- udata/core/user/api_fields.py,sha256=MLuvzY-w6zJpWdNoKfaKRsIahLA56fB9M8pq2YCZIns,5764
240
+ udata/core/user/activities.py,sha256=RAotXwFERt6TfLHiNfV13e1AgKGhlGwzlPm-ihj1sHk,3493
241
+ udata/core/user/api.py,sha256=TO5iolYhb7wWSqBwrSHQOSYR-r_8Jrn4LtMF5BiQELk,13473
242
+ udata/core/user/api_fields.py,sha256=S4wONsS5p_Ys31HdBxS_hBreCFHEegjfagg5aprwkNA,6026
242
243
  udata/core/user/apiv2.py,sha256=4eNsvJjb4ChJQFrXtVbkOtAvXEcQcQpZf8vkEbriXRA,1125
243
244
  udata/core/user/commands.py,sha256=d33hjgUi8WLA8YFvoq__FJJ_E-fk8f4tWo1iuT8jJvM,3156
244
245
  udata/core/user/constants.py,sha256=aTluhTR2RGZ_cdG7-mkEoT5Ndbg8BNUwwzCOld0aLMY,77
245
246
  udata/core/user/factories.py,sha256=kkwaojciLzfuAOeRnL1E7XCcGPo8waAal_G2eeuVc0k,825
246
247
  udata/core/user/forms.py,sha256=yotqZozH9ViKuNI8SwdKocDEi7NXVs_XUMpdr_bIe5s,966
247
248
  udata/core/user/metrics.py,sha256=ZuCxYHu-770IsO-KPMhh-ZDkl2hYSVCuHDRvp-H1NfA,1192
248
- udata/core/user/models.py,sha256=cK6M5q-u78_2wuWpWGv4dZrG8LZxc64lW7Ced_CvRLA,10459
249
+ udata/core/user/models.py,sha256=vmcpA8oCP9Z3j8d85GdfeRpqGFEzBDTAB4g1wr7baWU,11187
249
250
  udata/core/user/permissions.py,sha256=Wbd8bLqSjqp9RHJ6ffLBj74L-LECcAhWazuw4Hq-2Gk,435
250
251
  udata/core/user/rdf.py,sha256=F6SDX65ECtVOjDtHXI9WMmq33pN7HWb5ntOqe3LQsbQ,834
251
252
  udata/core/user/tasks.py,sha256=VcK0zJq2it4CVmoAMIOgJ0c2IHCV6UBQPAB0qDABD88,3451
@@ -368,7 +369,7 @@ udata/migrations/__init__.py,sha256=RBCBDaTlLjuMs_Qzwji6Z6T4r7FCGXhESKoxQbT5qAA,
368
369
  udata/models/__init__.py,sha256=txbZwa-lRG3mq99eQ9E5YcFWiNUdjDVSyJJvlqUMFfs,1413
369
370
  udata/mongo/__init__.py,sha256=y4Rv-kq3o_kcEulcNpePLzocXPBNpx3Jd82G-VZPaMc,1421
370
371
  udata/mongo/badges_field.py,sha256=UmSaQkiOFtIb116GAT2B0OE6ypOrq8Jx7GdULEr05LU,985
371
- udata/mongo/datetime_fields.py,sha256=7ShJTNoJy_DZUlTILI32VZOwELZLBKGqMBGAciovrgw,2064
372
+ udata/mongo/datetime_fields.py,sha256=xACagQZu1OKPvpcznI-bMC1tJfAvo-VBUe7OOadnBdg,2089
372
373
  udata/mongo/document.py,sha256=pJJ5B-1EVbQUPMjRqgK51BHPY5eVnyaXgnBnzqwojtA,1777
373
374
  udata/mongo/engine.py,sha256=KdvaQnpw6un47CHpu3q9PYWwTtovVmsKZK_k1DmN48M,2552
374
375
  udata/mongo/errors.py,sha256=SpTMAc_aNIfGkqyXGCbTlIAmYxU86rGM_NtIYaB642c,472
@@ -630,7 +631,7 @@ udata/tests/api/test_datasets_api.py,sha256=Igu8NrOfDEtYKjlG7CrL_VpiFN2Tp6W-Kj4r
630
631
  udata/tests/api/test_fields.py,sha256=OW85Z5MES5HeWOpapeem8OvR1cIcrqW-xMWpdZO4LZ8,1033
631
632
  udata/tests/api/test_follow_api.py,sha256=XP6I96JUNT6xjGcQOF7pug_T_i67HzCiOGLaPdpfpEQ,4912
632
633
  udata/tests/api/test_me_api.py,sha256=YPd8zmR3zwJKtpSqz8nY1nOOMyXs66INeBwyhg5D0Us,13846
633
- udata/tests/api/test_organizations_api.py,sha256=-jV2By1qkcxtrm57jVKqKa8nvvCCohGUz63QT9uZgZQ,41209
634
+ udata/tests/api/test_organizations_api.py,sha256=u27G6Dg3SFmTMCMQXSQWEbsvSonsDelFSojUvt2i05s,41235
634
635
  udata/tests/api/test_reports_api.py,sha256=fCSz9NwMXBs6cxdXBVVI6y564AtovmZYw3xkgxQ9KE8,6217
635
636
  udata/tests/api/test_reuses_api.py,sha256=x7SG8tNmuPDAaK-t0E_Y02-_5g4o8t7dnVdfgE1Qu08,25218
636
637
  udata/tests/api/test_swagger.py,sha256=eE6La9qdTYTIUFevRVPJgtj17Jq_8uOlsDwzCNR0LL8,760
@@ -726,9 +727,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=ViV14tUmjSydHS0TWG_mFikKQfyUaT
726
727
  udata/translations/pt/LC_MESSAGES/udata.po,sha256=rzAD_MVoV54TmN3w1ECz3H2Ru5pM7hWMVH03SkY28Q8,47250
727
728
  udata/translations/sr/LC_MESSAGES/udata.mo,sha256=EHX1_D-Uglj38832G7BrA0QC5IuY3p8dKqi9T0DgPmE,29169
728
729
  udata/translations/sr/LC_MESSAGES/udata.po,sha256=3PMnbVhKVJh6Q8ABi1ZTZ8Dcf-sMjngLJZqLbonJoec,54225
729
- udata-10.4.2.dev35266.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
730
- udata-10.4.2.dev35266.dist-info/METADATA,sha256=G4V-wqR_tpcGXgvECSiRjzpXB9xUPvkUABDA6JwUmrw,146603
731
- udata-10.4.2.dev35266.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
732
- udata-10.4.2.dev35266.dist-info/entry_points.txt,sha256=ETvkR4r6G1duBsh_V_fGWENQy17GTFuobi95MYBAl1A,498
733
- udata-10.4.2.dev35266.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
734
- udata-10.4.2.dev35266.dist-info/RECORD,,
730
+ udata-10.4.2.dev35319.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
731
+ udata-10.4.2.dev35319.dist-info/METADATA,sha256=Au8KhpPvrAdVMP-YYk8a1wOly-4_2_zAhZ1SoALR0V8,146838
732
+ udata-10.4.2.dev35319.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
733
+ udata-10.4.2.dev35319.dist-info/entry_points.txt,sha256=ETvkR4r6G1duBsh_V_fGWENQy17GTFuobi95MYBAl1A,498
734
+ udata-10.4.2.dev35319.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
735
+ udata-10.4.2.dev35319.dist-info/RECORD,,