udata 10.4.1.dev35211__py2.py3-none-any.whl → 10.4.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of udata might be problematic. Click here for more details.

Files changed (64) hide show
  1. udata/__init__.py +1 -1
  2. udata/core/activity/__init__.py +2 -0
  3. udata/core/activity/api.py +10 -2
  4. udata/core/activity/models.py +28 -1
  5. udata/core/activity/tasks.py +19 -4
  6. udata/core/dataservices/activities.py +53 -0
  7. udata/core/dataservices/api.py +43 -0
  8. udata/core/dataservices/models.py +16 -20
  9. udata/core/dataset/activities.py +52 -5
  10. udata/core/dataset/api.py +44 -0
  11. udata/core/dataset/models.py +49 -43
  12. udata/core/dataset/rdf.py +1 -1
  13. udata/core/metrics/commands.py +1 -0
  14. udata/core/metrics/helpers.py +102 -0
  15. udata/core/metrics/models.py +1 -0
  16. udata/core/metrics/tasks.py +1 -0
  17. udata/core/organization/activities.py +3 -2
  18. udata/core/organization/api.py +11 -0
  19. udata/core/organization/api_fields.py +6 -5
  20. udata/core/organization/models.py +31 -31
  21. udata/core/owned.py +1 -1
  22. udata/core/post/api.py +34 -0
  23. udata/core/reuse/activities.py +6 -5
  24. udata/core/reuse/api.py +42 -1
  25. udata/core/reuse/models.py +8 -16
  26. udata/core/site/models.py +33 -0
  27. udata/core/topic/activities.py +36 -0
  28. udata/core/topic/models.py +23 -15
  29. udata/core/user/activities.py +17 -6
  30. udata/core/user/api.py +1 -0
  31. udata/core/user/api_fields.py +6 -1
  32. udata/core/user/models.py +39 -32
  33. udata/migrations/2025-05-22-purge-duplicate-activities.py +101 -0
  34. udata/mongo/datetime_fields.py +1 -0
  35. udata/settings.py +4 -0
  36. udata/static/chunks/{10.471164b2a9fe15614797.js → 10.8ca60413647062717b1e.js} +3 -3
  37. udata/static/chunks/{10.471164b2a9fe15614797.js.map → 10.8ca60413647062717b1e.js.map} +1 -1
  38. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.b6f741fcc366abfad9c4.js} +3 -3
  39. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
  40. udata/static/chunks/{13.f29411b06be1883356a3.js → 13.2d06442dd9a05d9777b5.js} +2 -2
  41. udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
  42. udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
  43. udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
  44. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
  45. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
  46. udata/static/chunks/{8.54e44b102164ae5e7a67.js → 8.778091d55cd8ea39af6b.js} +2 -2
  47. udata/static/chunks/{8.54e44b102164ae5e7a67.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
  48. udata/static/chunks/{9.07515e5187f475bce828.js → 9.033d7e190ca9e226a5d0.js} +3 -3
  49. udata/static/chunks/{9.07515e5187f475bce828.js.map → 9.033d7e190ca9e226a5d0.js.map} +1 -1
  50. udata/static/common.js +1 -1
  51. udata/static/common.js.map +1 -1
  52. udata/tests/api/test_activities_api.py +29 -1
  53. udata/tests/api/test_dataservices_api.py +53 -0
  54. udata/tests/api/test_datasets_api.py +61 -0
  55. udata/tests/api/test_organizations_api.py +27 -2
  56. udata/tests/api/test_reuses_api.py +54 -0
  57. udata/tests/dataset/test_dataset_model.py +49 -0
  58. udata/tests/test_topics.py +19 -0
  59. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/METADATA +16 -2
  60. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/RECORD +64 -60
  61. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/LICENSE +0 -0
  62. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/WHEEL +0 -0
  63. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/entry_points.txt +0 -0
  64. {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,9 @@
1
+ from blinker import Signal
1
2
  from flask import url_for
2
- from mongoengine.signals import pre_save
3
+ from mongoengine.signals import post_save, pre_save
3
4
 
5
+ from udata.api_fields import field
6
+ from udata.core.activity.models import Auditable
4
7
  from udata.core.owned import Owned, OwnedQuerySet
5
8
  from udata.models import SpatialCoverage, db
6
9
  from udata.search import reindex
@@ -8,24 +11,24 @@ from udata.search import reindex
8
11
  __all__ = ("Topic",)
9
12
 
10
13
 
11
- class Topic(db.Document, Owned, db.Datetimed):
12
- name = db.StringField(required=True)
13
- slug = db.SlugField(
14
- max_length=255, required=True, populate_from="name", update=True, follow=True
14
+ class Topic(db.Datetimed, Auditable, db.Document, Owned):
15
+ name = field(db.StringField(required=True))
16
+ slug = field(
17
+ db.SlugField(max_length=255, required=True, populate_from="name", update=True, follow=True),
18
+ auditable=False,
15
19
  )
16
- description = db.StringField()
17
- tags = db.ListField(db.StringField())
18
- color = db.IntField()
20
+ description = field(db.StringField())
21
+ tags = field(db.ListField(db.StringField()))
22
+ color = field(db.IntField())
19
23
 
20
- tags = db.ListField(db.StringField())
21
- datasets = db.ListField(db.LazyReferenceField("Dataset", reverse_delete_rule=db.PULL))
22
- reuses = db.ListField(db.LazyReferenceField("Reuse", reverse_delete_rule=db.PULL))
24
+ datasets = field(db.ListField(db.LazyReferenceField("Dataset", reverse_delete_rule=db.PULL)))
25
+ reuses = field(db.ListField(db.LazyReferenceField("Reuse", reverse_delete_rule=db.PULL)))
23
26
 
24
- featured = db.BooleanField(default=False)
25
- private = db.BooleanField()
26
- extras = db.ExtrasField()
27
+ featured = field(db.BooleanField(default=False), auditable=False)
28
+ private = field(db.BooleanField())
29
+ extras = field(db.ExtrasField(), auditable=False)
27
30
 
28
- spatial = db.EmbeddedDocumentField(SpatialCoverage)
31
+ spatial = field(db.EmbeddedDocumentField(SpatialCoverage))
29
32
 
30
33
  meta = {
31
34
  "indexes": ["$name", "created_at", "slug"] + Owned.meta["indexes"],
@@ -34,6 +37,10 @@ class Topic(db.Document, Owned, db.Datetimed):
34
37
  "queryset_class": OwnedQuerySet,
35
38
  }
36
39
 
40
+ after_save = Signal()
41
+ on_create = Signal()
42
+ on_update = Signal()
43
+
37
44
  def __str__(self):
38
45
  return self.name
39
46
 
@@ -60,3 +67,4 @@ class Topic(db.Document, Owned, db.Datetimed):
60
67
 
61
68
 
62
69
  pre_save.connect(Topic.pre_save, sender=Topic)
70
+ post_save.connect(Topic.post_save, sender=Topic)
@@ -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
@@ -28,11 +30,6 @@ class DiscussActivity(object):
28
30
  badge_type = "warning"
29
31
 
30
32
 
31
- class UserStarredOrganization(FollowActivity, OrgRelatedActivity, Activity):
32
- key = "organization:followed"
33
- label = _("followed an organization")
34
-
35
-
36
33
  class UserFollowedUser(FollowActivity, Activity):
37
34
  key = "user:followed"
38
35
  label = _("followed a user")
@@ -40,6 +37,11 @@ class UserFollowedUser(FollowActivity, Activity):
40
37
  template = "activity/user.html"
41
38
 
42
39
 
40
+ class UserDiscussedDataservice(DiscussActivity, DataserviceRelatedActivity, Activity):
41
+ key = "dataservice:discussed"
42
+ label = _("discussed a dataservice")
43
+
44
+
43
45
  class UserDiscussedDataset(DiscussActivity, DatasetRelatedActivity, Activity):
44
46
  key = "dataset:discussed"
45
47
  label = _("discussed a dataset")
@@ -50,6 +52,11 @@ class UserDiscussedReuse(DiscussActivity, ReuseRelatedActivity, Activity):
50
52
  label = _("discussed a reuse")
51
53
 
52
54
 
55
+ class UserFollowedDataservice(FollowActivity, DataserviceRelatedActivity, Activity):
56
+ key = "dataservice:followed"
57
+ label = _("followed a dataservice")
58
+
59
+
53
60
  class UserFollowedDataset(FollowActivity, DatasetRelatedActivity, Activity):
54
61
  key = "dataset:followed"
55
62
  label = _("followed a dataset")
@@ -68,7 +75,9 @@ class UserFollowedOrganization(FollowActivity, OrgRelatedActivity, Activity):
68
75
  @on_follow.connect
69
76
  def write_activity_on_follow(follow, **kwargs):
70
77
  if current_user.is_authenticated:
71
- if isinstance(follow.following, Dataset):
78
+ if isinstance(follow.following, Dataservice):
79
+ UserFollowedDataservice.emit(follow.following)
80
+ elif isinstance(follow.following, Dataset):
72
81
  UserFollowedDataset.emit(follow.following)
73
82
  elif isinstance(follow.following, Reuse):
74
83
  UserFollowedReuse.emit(follow.following)
@@ -82,6 +91,8 @@ def write_activity_on_follow(follow, **kwargs):
82
91
  @on_new_discussion_comment.connect
83
92
  def write_activity_on_discuss(discussion, **kwargs):
84
93
  if current_user.is_authenticated:
94
+ if isinstance(discussion.subject, Dataservice):
95
+ UserDiscussedDataservice.emit(discussion.subject)
85
96
  if isinstance(discussion.subject, Dataset):
86
97
  UserDiscussedDataset.emit(discussion.subject)
87
98
  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)
@@ -0,0 +1,101 @@
1
+ """
2
+ This migration updates Topic.featured to False when it is None.
3
+ """
4
+
5
+ import logging
6
+ from datetime import datetime, timedelta
7
+
8
+ from mongoengine.connection import get_db
9
+
10
+ from udata.core.dataset.activities import UserCreatedDataset, UserDeletedDataset, UserUpdatedDataset
11
+ from udata.core.organization.activities import UserUpdatedOrganization
12
+ from udata.core.reuse.activities import UserCreatedReuse, UserDeletedReuse, UserUpdatedReuse
13
+ from udata.core.user.activities import (
14
+ UserDiscussedDataset,
15
+ UserDiscussedReuse,
16
+ UserFollowedDataset,
17
+ UserFollowedOrganization,
18
+ UserFollowedReuse,
19
+ )
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ def migrate(db):
25
+ # Remove legacy fields (`as_organization`, `kwargs`) from old activities
26
+ result = get_db().activity.update_many({}, {"$unset": {"as_organization": ""}})
27
+ log.info(
28
+ f"Legacy field `as_organization` removed from {result.modified_count} activity objects"
29
+ )
30
+
31
+ result = get_db().activity.update_many({}, {"$unset": {"kwargs": ""}})
32
+ log.info(f"Legacy field `kwargs` removed from {result.modified_count} activity objects")
33
+
34
+ # Clean duplicate activities in case of discussion or following
35
+ # - remove the "updated" activity on the discussed/followed object
36
+ # - remove the activity on the organization
37
+ # The heuristic is to look for specific activities by the same actor on the targeted object
38
+ # within a -1 +1 second timespan
39
+ for action_related_activity, object_updated_activity in [
40
+ (UserDiscussedDataset, UserUpdatedDataset),
41
+ (UserDiscussedReuse, UserUpdatedReuse),
42
+ (UserFollowedDataset, UserUpdatedDataset),
43
+ (UserFollowedReuse, UserUpdatedReuse),
44
+ ]:
45
+ org_activity_count = 0
46
+ object_activity_count = 0
47
+ activities = (
48
+ action_related_activity.objects()
49
+ .no_dereference() # We use no_dereference in query to prevent DBref DoesNotExist errors
50
+ .no_cache()
51
+ .timeout(False)
52
+ )
53
+ log.info(
54
+ f"{datetime.utcnow()}: Processing {activities.count()} {action_related_activity} activities..."
55
+ )
56
+ for act in activities:
57
+ object_activity_count += object_updated_activity.objects(
58
+ actor=act.actor.id,
59
+ related_to=act.related_to.id,
60
+ created_at__gte=act.created_at - timedelta(seconds=1),
61
+ created_at__lte=act.created_at + timedelta(seconds=1),
62
+ ).delete()
63
+ if act.organization:
64
+ org_activity_count += UserUpdatedOrganization.objects(
65
+ actor=act.actor.id,
66
+ related_to=act.organization,
67
+ created_at__gte=act.created_at - timedelta(seconds=1),
68
+ created_at__lte=act.created_at + timedelta(seconds=1),
69
+ ).delete()
70
+ log.info(
71
+ f"{datetime.utcnow()}: Deleted {object_activity_count} {object_updated_activity} and {org_activity_count} UserUpdatedOrganization activities"
72
+ )
73
+
74
+ # Clean duplicated UserUpdatedOrganization activities on organization for any object related activity
75
+ for object_related_activity in [
76
+ UserCreatedDataset,
77
+ UserUpdatedDataset,
78
+ UserDeletedDataset,
79
+ UserCreatedReuse,
80
+ UserUpdatedReuse,
81
+ UserDeletedReuse,
82
+ UserFollowedOrganization,
83
+ ]:
84
+ count = 0
85
+ activities = (
86
+ object_related_activity.objects(organization__exists=True)
87
+ .no_dereference() # We use no_dereference in query to prevent DBref DoesNotExist errors
88
+ .no_cache()
89
+ .timeout(False)
90
+ )
91
+ log.info(
92
+ f"{datetime.utcnow()}: Processing {activities.count()} {object_related_activity} activities..."
93
+ )
94
+ for act in activities:
95
+ count += UserUpdatedOrganization.objects(
96
+ actor=act.actor.id,
97
+ related_to=act.organization,
98
+ created_at__gte=act.created_at - timedelta(seconds=1),
99
+ created_at__lte=act.created_at + timedelta(seconds=1),
100
+ ).delete()
101
+ log.info(f"{datetime.utcnow()}: Deleted {count} UserUpdatedOrganization activities")
@@ -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
 
udata/settings.py CHANGED
@@ -574,6 +574,10 @@ class Defaults(object):
574
574
  ###########################################################################
575
575
  MAX_RESOURCES_IN_JSON_LD = 20
576
576
 
577
+ # Metrics settings
578
+ ###########################################################################
579
+ METRICS_API = None
580
+
577
581
 
578
582
  class Testing(object):
579
583
  """Sane values for testing. Should be applied as override"""