udata 10.4.1.dev35201__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/csv.py +0 -1
- udata/core/dataset/models.py +49 -47
- 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/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_csv_adapter.py +6 -3
- udata/tests/dataset/test_dataset_model.py +49 -0
- udata/tests/test_topics.py +19 -0
- {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/METADATA +17 -2
- {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/RECORD +50 -46
- {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/LICENSE +0 -0
- {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/WHEEL +0 -0
- {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/entry_points.txt +0 -0
- {udata-10.4.1.dev35201.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/csv.py
CHANGED
|
@@ -40,7 +40,6 @@ class DatasetCsvAdapter(csv.Adapter):
|
|
|
40
40
|
("resources_count", lambda o: len(o.resources)),
|
|
41
41
|
("main_resources_count", lambda o: len([r for r in o.resources if r.type == "main"])),
|
|
42
42
|
("resources_formats", lambda o: ",".join(set(r.format for r in o.resources if r.format))),
|
|
43
|
-
"downloads",
|
|
44
43
|
("harvest.backend", lambda r: r.harvest and r.harvest.backend),
|
|
45
44
|
("harvest.domain", lambda r: r.harvest and r.harvest.domain),
|
|
46
45
|
("harvest.created_at", lambda r: r.harvest and r.harvest.created_at),
|