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.

Files changed (50) 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/csv.py +0 -1
  12. udata/core/dataset/models.py +49 -47
  13. udata/core/dataset/rdf.py +1 -1
  14. udata/core/metrics/commands.py +1 -0
  15. udata/core/metrics/helpers.py +102 -0
  16. udata/core/metrics/models.py +1 -0
  17. udata/core/metrics/tasks.py +1 -0
  18. udata/core/organization/activities.py +3 -2
  19. udata/core/organization/api.py +11 -0
  20. udata/core/organization/api_fields.py +6 -5
  21. udata/core/organization/models.py +31 -31
  22. udata/core/owned.py +1 -1
  23. udata/core/post/api.py +34 -0
  24. udata/core/reuse/activities.py +6 -5
  25. udata/core/reuse/api.py +42 -1
  26. udata/core/reuse/models.py +8 -16
  27. udata/core/site/models.py +33 -0
  28. udata/core/topic/activities.py +36 -0
  29. udata/core/topic/models.py +23 -15
  30. udata/core/user/activities.py +17 -6
  31. udata/core/user/api.py +1 -0
  32. udata/core/user/api_fields.py +6 -1
  33. udata/core/user/models.py +39 -32
  34. udata/migrations/2025-05-22-purge-duplicate-activities.py +101 -0
  35. udata/mongo/datetime_fields.py +1 -0
  36. udata/settings.py +4 -0
  37. udata/tests/api/test_activities_api.py +29 -1
  38. udata/tests/api/test_dataservices_api.py +53 -0
  39. udata/tests/api/test_datasets_api.py +61 -0
  40. udata/tests/api/test_organizations_api.py +27 -2
  41. udata/tests/api/test_reuses_api.py +54 -0
  42. udata/tests/dataset/test_csv_adapter.py +6 -3
  43. udata/tests/dataset/test_dataset_model.py +49 -0
  44. udata/tests/test_topics.py +19 -0
  45. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/METADATA +17 -2
  46. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/RECORD +50 -46
  47. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/LICENSE +0 -0
  48. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/WHEEL +0 -0
  49. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/entry_points.txt +0 -0
  50. {udata-10.4.1.dev35201.dist-info → udata-10.4.2.dist-info}/top_level.txt +0 -0
udata/__init__.py CHANGED
@@ -4,5 +4,5 @@
4
4
  udata
5
5
  """
6
6
 
7
- __version__ = "10.4.1.dev"
7
+ __version__ = "10.4.2"
8
8
  __description__ = "Open data portal"
@@ -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
@@ -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 of org."""
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
 
@@ -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 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)
@@ -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(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,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 not dataset.private and current_user and current_user.is_authenticated:
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
- if not dataset.private and current_user and current_user.is_authenticated:
47
- UserUpdatedDataset.emit(dataset, dataset.organization)
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 not dataset.private and current_user and current_user.is_authenticated:
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),