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.
- udata/__init__.py +1 -1
- udata/core/activity/__init__.py +2 -0
- udata/core/activity/api.py +10 -2
- udata/core/activity/models.py +28 -1
- udata/core/activity/tasks.py +19 -4
- udata/core/dataservices/activities.py +53 -0
- udata/core/dataservices/api.py +43 -0
- udata/core/dataservices/models.py +16 -20
- udata/core/dataset/activities.py +52 -5
- udata/core/dataset/api.py +44 -0
- udata/core/dataset/models.py +49 -43
- udata/core/dataset/rdf.py +1 -1
- udata/core/metrics/commands.py +1 -0
- udata/core/metrics/helpers.py +102 -0
- udata/core/metrics/models.py +1 -0
- udata/core/metrics/tasks.py +1 -0
- udata/core/organization/activities.py +3 -2
- udata/core/organization/api.py +11 -0
- udata/core/organization/api_fields.py +6 -5
- udata/core/organization/models.py +31 -31
- udata/core/owned.py +1 -1
- udata/core/post/api.py +34 -0
- udata/core/reuse/activities.py +6 -5
- udata/core/reuse/api.py +42 -1
- udata/core/reuse/models.py +8 -16
- udata/core/site/models.py +33 -0
- udata/core/topic/activities.py +36 -0
- udata/core/topic/models.py +23 -15
- udata/core/user/activities.py +17 -6
- udata/core/user/api.py +1 -0
- udata/core/user/api_fields.py +6 -1
- udata/core/user/models.py +39 -32
- udata/migrations/2025-05-22-purge-duplicate-activities.py +101 -0
- udata/mongo/datetime_fields.py +1 -0
- udata/settings.py +4 -0
- udata/static/chunks/{10.471164b2a9fe15614797.js → 10.8ca60413647062717b1e.js} +3 -3
- udata/static/chunks/{10.471164b2a9fe15614797.js.map → 10.8ca60413647062717b1e.js.map} +1 -1
- udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.b6f741fcc366abfad9c4.js} +3 -3
- udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
- udata/static/chunks/{13.f29411b06be1883356a3.js → 13.2d06442dd9a05d9777b5.js} +2 -2
- udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
- udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
- udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
- udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
- udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
- udata/static/chunks/{8.54e44b102164ae5e7a67.js → 8.778091d55cd8ea39af6b.js} +2 -2
- udata/static/chunks/{8.54e44b102164ae5e7a67.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
- udata/static/chunks/{9.07515e5187f475bce828.js → 9.033d7e190ca9e226a5d0.js} +3 -3
- udata/static/chunks/{9.07515e5187f475bce828.js.map → 9.033d7e190ca9e226a5d0.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_activities_api.py +29 -1
- udata/tests/api/test_dataservices_api.py +53 -0
- udata/tests/api/test_datasets_api.py +61 -0
- udata/tests/api/test_organizations_api.py +27 -2
- udata/tests/api/test_reuses_api.py +54 -0
- udata/tests/dataset/test_dataset_model.py +49 -0
- udata/tests/test_topics.py +19 -0
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/METADATA +16 -2
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/RECORD +64 -60
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/LICENSE +0 -0
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/WHEEL +0 -0
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/entry_points.txt +0 -0
- {udata-10.4.1.dev35211.dist-info → udata-10.4.2.dist-info}/top_level.txt +0 -0
udata/__init__.py
CHANGED
udata/core/activity/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ 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
|
|
13
|
+
import udata.core.topic.activities # noqa
|
udata/core/activity/api.py
CHANGED
|
@@ -4,6 +4,7 @@ from bson import ObjectId
|
|
|
4
4
|
from mongoengine.errors import DoesNotExist
|
|
5
5
|
|
|
6
6
|
from udata.api import API, api, fields
|
|
7
|
+
from udata.auth import current_user
|
|
7
8
|
from udata.core.organization.api_fields import org_ref_fields
|
|
8
9
|
from udata.core.user.api_fields import user_ref_fields
|
|
9
10
|
from udata.models import Activity, db
|
|
@@ -46,6 +47,7 @@ activity_fields = api.model(
|
|
|
46
47
|
"label": fields.String(description="The label of the activity", required=True),
|
|
47
48
|
"key": fields.String(description="The key of the activity", required=True),
|
|
48
49
|
"icon": fields.String(description="The icon of the activity", required=True),
|
|
50
|
+
"changes": fields.List(fields.String, description="Changed attributes as list"),
|
|
49
51
|
"extras": fields.Raw(description="Extras attributes as key-value pairs"),
|
|
50
52
|
},
|
|
51
53
|
)
|
|
@@ -76,7 +78,7 @@ class SiteActivityAPI(API):
|
|
|
76
78
|
@api.expect(activity_parser)
|
|
77
79
|
@api.marshal_with(activity_page_fields)
|
|
78
80
|
def get(self):
|
|
79
|
-
"""Fetch site activity, optionally filtered by user
|
|
81
|
+
"""Fetch site activity, optionally filtered by user or org."""
|
|
80
82
|
args = activity_parser.parse_args()
|
|
81
83
|
qs = Activity.objects
|
|
82
84
|
|
|
@@ -95,10 +97,11 @@ class SiteActivityAPI(API):
|
|
|
95
97
|
qs = qs.order_by("-created_at")
|
|
96
98
|
qs = qs.paginate(args["page"], args["page_size"])
|
|
97
99
|
|
|
98
|
-
# Filter out DBRefs
|
|
100
|
+
# - Filter out DBRefs
|
|
99
101
|
# Always return a result even not complete
|
|
100
102
|
# But log the error (ie. visible in sentry, silent for user)
|
|
101
103
|
# Can happen when someone manually delete an object in DB (ie. without proper purge)
|
|
104
|
+
# - Filter out private items (except for sysadmin users)
|
|
102
105
|
safe_items = []
|
|
103
106
|
for item in qs.queryset.items:
|
|
104
107
|
try:
|
|
@@ -106,6 +109,11 @@ class SiteActivityAPI(API):
|
|
|
106
109
|
except DoesNotExist as e:
|
|
107
110
|
log.error(e, exc_info=True)
|
|
108
111
|
else:
|
|
112
|
+
if hasattr(item.related_to, "private") and (
|
|
113
|
+
current_user.is_anonymous or not current_user.sysadmin
|
|
114
|
+
):
|
|
115
|
+
if item.related_to.private:
|
|
116
|
+
continue
|
|
109
117
|
safe_items.append(item)
|
|
110
118
|
qs.queryset.items = safe_items
|
|
111
119
|
|
udata/core/activity/models.py
CHANGED
|
@@ -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)
|
udata/core/activity/tasks.py
CHANGED
|
@@ -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,
|
|
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(
|
|
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 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 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 current_user and current_user.is_authenticated:
|
|
53
|
+
UserDeletedDataservice.emit(dataservice, dataservice.organization)
|
udata/core/dataservices/api.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from typing import List
|
|
2
3
|
|
|
3
4
|
import mongoengine
|
|
4
5
|
from bson import ObjectId
|
|
6
|
+
from feedgenerator.django.utils.feedgenerator import Atom1Feed
|
|
5
7
|
from flask import make_response, redirect, request, url_for
|
|
6
8
|
from flask_login import current_user
|
|
7
9
|
|
|
@@ -10,6 +12,9 @@ from udata.api_fields import patch
|
|
|
10
12
|
from udata.core.dataservices.permissions import OwnablePermission
|
|
11
13
|
from udata.core.dataset.models import Dataset
|
|
12
14
|
from udata.core.followers.api import FollowAPI
|
|
15
|
+
from udata.core.site.models import current_site
|
|
16
|
+
from udata.frontend.markdown import md
|
|
17
|
+
from udata.i18n import gettext as _
|
|
13
18
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
14
19
|
|
|
15
20
|
from .models import Dataservice
|
|
@@ -49,6 +54,44 @@ class DataservicesAPI(API):
|
|
|
49
54
|
return dataservice, 201
|
|
50
55
|
|
|
51
56
|
|
|
57
|
+
@ns.route("/recent.atom", endpoint="recent_dataservices_atom_feed")
|
|
58
|
+
class DataservicesAtomFeedAPI(API):
|
|
59
|
+
@api.doc("recent_dataservices_atom_feed")
|
|
60
|
+
def get(self):
|
|
61
|
+
feed = Atom1Feed(
|
|
62
|
+
_("Latest APIs"), description=None, feed_url=request.url, link=request.url_root
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
dataservices: List[Dataservice] = (
|
|
66
|
+
Dataservice.objects.visible()
|
|
67
|
+
.order_by("-created_at_internal")
|
|
68
|
+
.limit(current_site.feed_size)
|
|
69
|
+
)
|
|
70
|
+
for dataservice in dataservices:
|
|
71
|
+
author_name = None
|
|
72
|
+
author_uri = None
|
|
73
|
+
if dataservice.organization:
|
|
74
|
+
author_name = dataservice.organization.name
|
|
75
|
+
author_uri = dataservice.organization.external_url
|
|
76
|
+
elif dataservice.owner:
|
|
77
|
+
author_name = dataservice.owner.fullname
|
|
78
|
+
author_uri = dataservice.owner.external_url
|
|
79
|
+
feed.add_item(
|
|
80
|
+
dataservice.title,
|
|
81
|
+
unique_id=dataservice.id,
|
|
82
|
+
description=dataservice.description,
|
|
83
|
+
content=md(dataservice.description),
|
|
84
|
+
author_name=author_name,
|
|
85
|
+
author_link=author_uri,
|
|
86
|
+
link=dataservice.url_for(external=True),
|
|
87
|
+
updateddate=dataservice.metadata_modified_at,
|
|
88
|
+
pubdate=dataservice.created_at,
|
|
89
|
+
)
|
|
90
|
+
response = make_response(feed.writeString("utf-8"))
|
|
91
|
+
response.headers["Content-Type"] = "application/atom+xml"
|
|
92
|
+
return response
|
|
93
|
+
|
|
94
|
+
|
|
52
95
|
@ns.route("/<dataservice:dataservice>/", endpoint="dataservice")
|
|
53
96
|
class DataserviceAPI(API):
|
|
54
97
|
@api.doc("get_dataservice")
|
|
@@ -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(
|
|
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)
|
udata/core/dataset/activities.py
CHANGED
|
@@ -35,19 +35,66 @@ 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 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 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 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
|
-
if
|
|
86
|
+
if current_user and current_user.is_authenticated:
|
|
41
87
|
UserCreatedDataset.emit(dataset, dataset.organization)
|
|
42
88
|
|
|
43
89
|
|
|
44
90
|
@Dataset.on_update.connect
|
|
45
|
-
def on_user_updated_dataset(dataset):
|
|
46
|
-
|
|
47
|
-
|
|
91
|
+
def on_user_updated_dataset(dataset, **kwargs):
|
|
92
|
+
changed_fields = kwargs.get("changed_fields", [])
|
|
93
|
+
if current_user and current_user.is_authenticated:
|
|
94
|
+
UserUpdatedDataset.emit(dataset, dataset.organization, changed_fields)
|
|
48
95
|
|
|
49
96
|
|
|
50
97
|
@Dataset.on_delete.connect
|
|
51
98
|
def on_user_deleted_dataset(dataset):
|
|
52
|
-
if
|
|
99
|
+
if current_user and current_user.is_authenticated:
|
|
53
100
|
UserDeletedDataset.emit(dataset, dataset.organization)
|
udata/core/dataset/api.py
CHANGED
|
@@ -20,9 +20,11 @@ These changes might lead to backward compatibility breakage meaning:
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
from datetime import datetime
|
|
23
|
+
from typing import List
|
|
23
24
|
|
|
24
25
|
import mongoengine
|
|
25
26
|
from bson.objectid import ObjectId
|
|
27
|
+
from feedgenerator.django.utils.feedgenerator import Atom1Feed
|
|
26
28
|
from flask import abort, current_app, make_response, redirect, request, url_for
|
|
27
29
|
from flask_restx.inputs import boolean
|
|
28
30
|
from flask_security import current_user
|
|
@@ -39,8 +41,11 @@ from udata.core.dataset.models import CHECKSUM_TYPES
|
|
|
39
41
|
from udata.core.followers.api import FollowAPI
|
|
40
42
|
from udata.core.organization.models import Organization
|
|
41
43
|
from udata.core.reuse.models import Reuse
|
|
44
|
+
from udata.core.site.models import current_site
|
|
42
45
|
from udata.core.storages.api import handle_upload, upload_parser
|
|
43
46
|
from udata.core.topic.models import Topic
|
|
47
|
+
from udata.frontend.markdown import md
|
|
48
|
+
from udata.i18n import gettext as _
|
|
44
49
|
from udata.linkchecker.checker import check_resource
|
|
45
50
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
46
51
|
from udata.utils import get_by
|
|
@@ -292,6 +297,45 @@ class DatasetListAPI(API):
|
|
|
292
297
|
return dataset, 201
|
|
293
298
|
|
|
294
299
|
|
|
300
|
+
@ns.route("/recent.atom", endpoint="recent_datasets_atom_feed")
|
|
301
|
+
class DatasetsAtomFeedAPI(API):
|
|
302
|
+
@api.doc("recent_datasets_atom_feed")
|
|
303
|
+
def get(self):
|
|
304
|
+
feed = Atom1Feed(
|
|
305
|
+
_("Latest datasets"),
|
|
306
|
+
description=None,
|
|
307
|
+
feed_url=request.url,
|
|
308
|
+
link=request.url_root,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
datasets: List[Dataset] = (
|
|
312
|
+
Dataset.objects.visible().order_by("-created_at_internal").limit(current_site.feed_size)
|
|
313
|
+
)
|
|
314
|
+
for dataset in datasets:
|
|
315
|
+
author_name = None
|
|
316
|
+
author_uri = None
|
|
317
|
+
if dataset.organization:
|
|
318
|
+
author_name = dataset.organization.name
|
|
319
|
+
author_uri = dataset.organization.external_url
|
|
320
|
+
elif dataset.owner:
|
|
321
|
+
author_name = dataset.owner.fullname
|
|
322
|
+
author_uri = dataset.owner.external_url
|
|
323
|
+
feed.add_item(
|
|
324
|
+
dataset.title,
|
|
325
|
+
unique_id=dataset.id,
|
|
326
|
+
description=dataset.description,
|
|
327
|
+
content=md(dataset.description),
|
|
328
|
+
author_name=author_name,
|
|
329
|
+
author_link=author_uri,
|
|
330
|
+
link=dataset.external_url,
|
|
331
|
+
updateddate=dataset.last_modified,
|
|
332
|
+
pubdate=dataset.created_at,
|
|
333
|
+
)
|
|
334
|
+
response = make_response(feed.writeString("utf-8"))
|
|
335
|
+
response.headers["Content-Type"] = "application/atom+xml"
|
|
336
|
+
return response
|
|
337
|
+
|
|
338
|
+
|
|
295
339
|
@ns.route("/<dataset:dataset>/", endpoint="dataset", doc=common_doc)
|
|
296
340
|
@api.response(404, "Dataset not found")
|
|
297
341
|
@api.response(410, "Dataset has been deleted")
|
udata/core/dataset/models.py
CHANGED
|
@@ -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 =
|
|
549
|
-
|
|
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 =
|
|
574
|
+
featured = field(
|
|
575
|
+
db.BooleanField(required=True, default=False),
|
|
576
|
+
auditable=False,
|
|
577
|
+
)
|
|
571
578
|
|
|
572
|
-
contact_points =
|
|
579
|
+
contact_points = field(
|
|
580
|
+
db.ListField(db.ReferenceField("ContactPoint", reverse_delete_rule=db.PULL))
|
|
581
|
+
)
|
|
573
582
|
|
|
574
|
-
created_at_internal =
|
|
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 =
|
|
578
|
-
|
|
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:
|
|
@@ -973,7 +974,12 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
973
974
|
def remove_resource(self, resource):
|
|
974
975
|
# Deletes resource's file from file storage
|
|
975
976
|
if resource.fs_filename is not None:
|
|
976
|
-
|
|
977
|
+
try:
|
|
978
|
+
storages.resources.delete(resource.fs_filename)
|
|
979
|
+
except FileNotFoundError as e:
|
|
980
|
+
log.error(
|
|
981
|
+
f"File not found while deleting resource #{resource.id} in dataset {self.id}: {e}"
|
|
982
|
+
)
|
|
977
983
|
|
|
978
984
|
self.resources.remove(resource)
|
|
979
985
|
self.on_resource_removed.send(self.__class__, document=self, resource_id=resource.id)
|
|
@@ -1041,19 +1047,19 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
1041
1047
|
from udata.models import Discussion
|
|
1042
1048
|
|
|
1043
1049
|
self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
|
|
1044
|
-
self.save()
|
|
1050
|
+
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
1045
1051
|
|
|
1046
1052
|
def count_reuses(self):
|
|
1047
1053
|
from udata.models import Reuse
|
|
1048
1054
|
|
|
1049
1055
|
self.metrics["reuses"] = Reuse.objects(datasets=self).visible().count()
|
|
1050
|
-
self.save()
|
|
1056
|
+
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
1051
1057
|
|
|
1052
1058
|
def count_followers(self):
|
|
1053
1059
|
from udata.models import Follow
|
|
1054
1060
|
|
|
1055
1061
|
self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
|
|
1056
|
-
self.save()
|
|
1062
|
+
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
1057
1063
|
|
|
1058
1064
|
|
|
1059
1065
|
pre_init.connect(Dataset.pre_init, sender=Dataset)
|