udata 10.8.2.dev37076__py2.py3-none-any.whl → 10.8.3__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 (59) hide show
  1. udata/__init__.py +1 -1
  2. udata/core/activity/models.py +23 -1
  3. udata/core/dataset/api_fields.py +2 -0
  4. udata/core/dataset/apiv2.py +4 -0
  5. udata/core/dataset/constants.py +1 -0
  6. udata/core/dataset/csv.py +1 -0
  7. udata/core/dataset/forms.py +6 -0
  8. udata/core/dataset/metrics.py +34 -0
  9. udata/core/dataset/models.py +10 -0
  10. udata/core/dataset/tasks.py +0 -11
  11. udata/core/metrics/__init__.py +1 -0
  12. udata/core/metrics/commands.py +3 -0
  13. udata/core/organization/csv.py +9 -26
  14. udata/core/organization/metrics.py +2 -0
  15. udata/core/organization/models.py +14 -9
  16. udata/core/user/metrics.py +2 -0
  17. udata/harvest/backends/dcat.py +161 -165
  18. udata/harvest/tests/dcat/catalog.xml +1 -0
  19. udata/harvest/tests/test_dcat_backend.py +19 -6
  20. udata/settings.py +1 -1
  21. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.d9c1735d14038b94c17e.js} +2 -2
  22. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.d9c1735d14038b94c17e.js.map} +1 -1
  23. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.81c57c0dedf812e43013.js} +2 -2
  24. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.81c57c0dedf812e43013.js.map} +1 -1
  25. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.8d03c06efcac6884bebe.js} +3 -3
  26. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.8d03c06efcac6884bebe.js.map} +1 -1
  27. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.343ca020a2d38cec1a14.js} +3 -3
  28. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.343ca020a2d38cec1a14.js.map} +1 -1
  29. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.a3b07de9dd2ca2d24e85.js} +3 -3
  30. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.a3b07de9dd2ca2d24e85.js.map} +1 -1
  31. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.b966402f5d680d4bdf4a.js} +2 -2
  32. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.b966402f5d680d4bdf4a.js.map} +1 -1
  33. udata/static/common.js +1 -1
  34. udata/static/common.js.map +1 -1
  35. udata/tests/dataset/test_dataset_model.py +63 -1
  36. udata/tests/organization/test_csv_adapter.py +3 -15
  37. udata/tests/reuse/test_reuse_model.py +6 -4
  38. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  39. udata/translations/ar/LC_MESSAGES/udata.po +62 -54
  40. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  41. udata/translations/de/LC_MESSAGES/udata.po +62 -54
  42. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  43. udata/translations/es/LC_MESSAGES/udata.po +62 -54
  44. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  45. udata/translations/fr/LC_MESSAGES/udata.po +62 -54
  46. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  47. udata/translations/it/LC_MESSAGES/udata.po +62 -54
  48. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  49. udata/translations/pt/LC_MESSAGES/udata.po +62 -54
  50. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  51. udata/translations/sr/LC_MESSAGES/udata.po +62 -54
  52. udata/translations/udata.pot +63 -56
  53. udata/utils.py +16 -0
  54. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.dist-info}/METADATA +15 -3
  55. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.dist-info}/RECORD +59 -58
  56. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.dist-info}/LICENSE +0 -0
  57. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.dist-info}/WHEEL +0 -0
  58. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.dist-info}/entry_points.txt +0 -0
  59. {udata-10.8.2.dev37076.dist-info → udata-10.8.3.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.8.2.dev"
7
+ __version__ = "10.8.3"
8
8
  __description__ = "Open data portal"
@@ -1,11 +1,13 @@
1
1
  from datetime import datetime
2
2
 
3
3
  from blinker import Signal
4
+ from mongoengine.errors import DoesNotExist
4
5
  from mongoengine.signals import post_save
5
6
 
6
7
  from udata.api_fields import get_fields
7
8
  from udata.auth import current_user
8
9
  from udata.mongo import db
10
+ from udata.utils import get_field_value_from_path
9
11
 
10
12
  from .signals import new_activity
11
13
 
@@ -79,6 +81,25 @@ class Activity(db.Document, metaclass=EmitNewActivityMetaClass):
79
81
 
80
82
 
81
83
  class Auditable(object):
84
+ def clean(self, **kwargs):
85
+ super().clean()
86
+ """
87
+ Fetch original document changed fields values before the new one erase it.
88
+ """
89
+ changed_fields = self._get_changed_fields()
90
+ if changed_fields:
91
+ try:
92
+ # `only` does not support having nested list as expressed in changed fields, ex resources.0.title
93
+ # thus we only strip to direct attributes for simplicity
94
+ direct_attributes = set(field.split(".")[0] for field in changed_fields)
95
+ old_document = self.__class__.objects.only(*direct_attributes).get(pk=self.pk)
96
+ self._previous_changed_fields = {}
97
+ for field_path in changed_fields:
98
+ field_value = get_field_value_from_path(old_document, field_path)
99
+ self._previous_changed_fields[field_path] = field_value
100
+ except DoesNotExist:
101
+ pass
102
+
82
103
  @classmethod
83
104
  def post_save(cls, sender, document, **kwargs):
84
105
  try:
@@ -97,6 +118,7 @@ class Auditable(object):
97
118
  if kwargs.get("created"):
98
119
  cls.on_create.send(document)
99
120
  elif len(changed_fields):
100
- cls.on_update.send(document, changed_fields=changed_fields)
121
+ previous = getattr(document, "_previous_changed_fields", None)
122
+ cls.on_update.send(document, changed_fields=changed_fields, previous=previous)
101
123
  if getattr(document, "deleted_at", None) or getattr(document, "deleted", None):
102
124
  cls.on_delete.send(document)
@@ -266,6 +266,7 @@ DEFAULT_MASK = ",".join(
266
266
  "acronym",
267
267
  "slug",
268
268
  "description",
269
+ "description_short",
269
270
  "created_at",
270
271
  "last_modified",
271
272
  "deleted",
@@ -327,6 +328,7 @@ dataset_fields = api.model(
327
328
  "description": fields.Markdown(
328
329
  description="The dataset description in markdown", required=True
329
330
  ),
331
+ "description_short": fields.String(description="The dataset short description"),
330
332
  "created_at": fields.ISODateTime(
331
333
  description="This date is computed between harvested creation date if any and site's internal creation date",
332
334
  required=True,
@@ -44,6 +44,7 @@ DEFAULT_MASK_APIV2 = ",".join(
44
44
  "acronym",
45
45
  "slug",
46
46
  "description",
47
+ "description_short",
47
48
  "created_at",
48
49
  "last_modified",
49
50
  "deleted",
@@ -105,6 +106,9 @@ dataset_fields = apiv2.model(
105
106
  "description": fields.Markdown(
106
107
  description="The dataset description in markdown", required=True
107
108
  ),
109
+ "description_short": fields.String(
110
+ description="The dataset short description", required=False
111
+ ),
108
112
  "created_at": fields.ISODateTime(
109
113
  description="The dataset creation date", required=True, readonly=True
110
114
  ),
@@ -89,5 +89,6 @@ SCHEMA_CACHE_DURATION = 60 * 5 # In seconds
89
89
 
90
90
  TITLE_SIZE_LIMIT = 350
91
91
  DESCRIPTION_SIZE_LIMIT = 100000
92
+ DESCRIPTION_SHORT_SIZE_LIMIT = 200
92
93
 
93
94
  FULL_OBJECTS_HEADER = "X-Get-Datasets-Full-Objects"
udata/core/dataset/csv.py CHANGED
@@ -26,6 +26,7 @@ class DatasetCsvAdapter(csv.Adapter):
26
26
  ("owner_id", "owner.id"),
27
27
  # 'contact_point', # ?
28
28
  "description",
29
+ "description_short",
29
30
  "frequency",
30
31
  "license",
31
32
  "temporal_coverage.start",
@@ -7,6 +7,7 @@ from udata.mongo.errors import FieldValidationError
7
7
  from .constants import (
8
8
  CHECKSUM_TYPES,
9
9
  DEFAULT_FREQUENCY,
10
+ DESCRIPTION_SHORT_SIZE_LIMIT,
10
11
  DESCRIPTION_SIZE_LIMIT,
11
12
  LEGACY_FREQUENCIES,
12
13
  RESOURCE_FILETYPES,
@@ -151,6 +152,11 @@ class DatasetForm(ModelForm):
151
152
  [validators.DataRequired(), validators.Length(max=DESCRIPTION_SIZE_LIMIT)],
152
153
  description=_("The details about the dataset (collection process, specifics...)."),
153
154
  )
155
+ description_short = fields.StringField(
156
+ _("Short description"),
157
+ [validators.Length(max=DESCRIPTION_SHORT_SIZE_LIMIT)],
158
+ description=_("A short description of the dataset."),
159
+ )
154
160
  license = fields.ModelSelectField(_("License"), model=License, allow_blank=True)
155
161
  frequency = fields.SelectField(
156
162
  _("Update frequency"),
@@ -0,0 +1,34 @@
1
+ from udata.core.dataservices.models import Dataservice
2
+ from udata.core.reuse.models import Reuse
3
+
4
+ from .models import Dataset
5
+
6
+
7
+ @Reuse.on_create.connect
8
+ @Reuse.on_update.connect
9
+ @Reuse.on_delete.connect
10
+ def update_dataset_reuses_metric(reuse, **kwargs):
11
+ previous = kwargs.get("previous")
12
+ if previous and previous.get("datasets"):
13
+ datasets_delta = set(dat.id for dat in reuse.datasets).symmetric_difference(
14
+ set(dat.id for dat in previous["datasets"])
15
+ )
16
+ else:
17
+ datasets_delta = set(dat.id for dat in reuse.datasets)
18
+ for dataset in datasets_delta:
19
+ Dataset.get(dataset).count_reuses()
20
+
21
+
22
+ @Dataservice.on_create.connect
23
+ @Dataservice.on_update.connect
24
+ @Dataservice.on_delete.connect
25
+ def update_dataset_dataservices_metric(dataservice, **kwargs):
26
+ previous = kwargs.get("previous")
27
+ if previous and previous.get("datasets"):
28
+ datasets_delta = set(dat.id for dat in dataservice.datasets).symmetric_difference(
29
+ set(dat.id for dat in previous["datasets"])
30
+ )
31
+ else:
32
+ datasets_delta = set(dat.id for dat in dataservice.datasets)
33
+ for dataset in datasets_delta:
34
+ Dataset.get(dataset).count_dataservices()
@@ -35,6 +35,7 @@ from .constants import (
35
35
  CHECKSUM_TYPES,
36
36
  CLOSED_FORMATS,
37
37
  DEFAULT_LICENSE,
38
+ DESCRIPTION_SHORT_SIZE_LIMIT,
38
39
  LEGACY_FREQUENCIES,
39
40
  MAX_DISTANCE,
40
41
  PIVOTAL_DATA,
@@ -560,6 +561,7 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
560
561
  auditable=False,
561
562
  )
562
563
  description = field(db.StringField(required=True, default=""))
564
+ description_short = field(db.StringField(max_length=DESCRIPTION_SHORT_SIZE_LIMIT))
563
565
  license = field(db.ReferenceField("License"))
564
566
 
565
567
  tags = field(db.TagListField())
@@ -616,6 +618,7 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
616
618
  "discussions_open",
617
619
  "reuses",
618
620
  "reuses_by_months",
621
+ "dataservices",
619
622
  "followers",
620
623
  "followers_by_months",
621
624
  "views",
@@ -628,6 +631,7 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
628
631
  "created_at_internal",
629
632
  "last_modified_internal",
630
633
  "metrics.reuses",
634
+ "metrics.dataservices",
631
635
  "metrics.followers",
632
636
  "metrics.views",
633
637
  "slug",
@@ -1100,6 +1104,12 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
1100
1104
  self.metrics["reuses_by_months"] = get_stock_metrics(Reuse.objects(datasets=self).visible())
1101
1105
  self.save(signal_kwargs={"ignores": ["post_save"]})
1102
1106
 
1107
+ def count_dataservices(self):
1108
+ from udata.core.dataservices.models import Dataservice
1109
+
1110
+ self.metrics["dataservices"] = Dataservice.objects(datasets=self).visible().count()
1111
+ self.save(signal_kwargs={"ignores": ["post_save"]})
1112
+
1103
1113
  def count_followers(self):
1104
1114
  from udata.models import Follow
1105
1115
 
@@ -123,17 +123,6 @@ def send_frequency_reminder(self):
123
123
  print("Done")
124
124
 
125
125
 
126
- @job("update-datasets-reuses-metrics")
127
- def update_datasets_reuses_metrics(self):
128
- all_datasets = Dataset.objects.visible().timeout(False)
129
- for dataset in all_datasets:
130
- try:
131
- dataset.count_reuses()
132
- except Exception as e:
133
- log.error(f"Error for dataset {dataset} during reuses metrics update: {e}")
134
- continue
135
-
136
-
137
126
  def get_queryset(model_cls):
138
127
  # special case for resources
139
128
  if model_cls.__name__ == "Resource":
@@ -6,6 +6,7 @@ def init_app(app):
6
6
  import udata.core.user.metrics # noqa
7
7
  import udata.core.organization.metrics # noqa
8
8
  import udata.core.discussions.metrics # noqa
9
+ import udata.core.dataset.metrics # noqa
9
10
  import udata.core.reuse.metrics # noqa
10
11
  import udata.core.followers.metrics # noqa
11
12
 
@@ -74,6 +74,7 @@ def update(
74
74
  dataset.metrics.clear()
75
75
  dataset.count_discussions()
76
76
  dataset.count_reuses()
77
+ dataset.count_dataservices()
77
78
  dataset.count_followers()
78
79
  except Exception as e:
79
80
  log.info(f"Error during update: {e}")
@@ -119,6 +120,7 @@ def update(
119
120
  organization.metrics.clear()
120
121
  organization.count_datasets()
121
122
  organization.count_reuses()
123
+ organization.count_dataservices()
122
124
  organization.count_followers()
123
125
  organization.count_members()
124
126
  except Exception as e:
@@ -135,6 +137,7 @@ def update(
135
137
  user.metrics.clear()
136
138
  user.count_datasets()
137
139
  user.count_reuses()
140
+ user.count_dataservices()
138
141
  user.count_followers()
139
142
  user.count_following()
140
143
  except Exception as e:
@@ -4,6 +4,13 @@ from udata.core.dataset.models import Dataset
4
4
  from .models import Organization
5
5
 
6
6
 
7
+ def get_resource_download_count(organization: Organization) -> int:
8
+ return sum(
9
+ dat.metrics.get("resources_downloads", 0) or 0
10
+ for dat in Dataset.objects(organization=organization).only("metrics").visible()
11
+ )
12
+
13
+
7
14
  @csv.adapter(Organization)
8
15
  class OrganizationCsvAdapter(csv.Adapter):
9
16
  downloads_counts = None
@@ -21,32 +28,8 @@ class OrganizationCsvAdapter(csv.Adapter):
21
28
  "last_modified",
22
29
  "business_number_id",
23
30
  ("members_count", lambda o: len(o.members)),
31
+ ("downloads", get_resource_download_count),
24
32
  )
25
33
 
26
34
  def dynamic_fields(self):
27
- return csv.metric_fields(Organization) + self.get_dynamic_field_downloads()
28
-
29
- def get_dynamic_field_downloads(self):
30
- downloads_counts = self.get_downloads_counts()
31
- return [("downloads", lambda o: downloads_counts.get(str(o.id), 0))]
32
-
33
- def get_downloads_counts(self):
34
- """
35
- Prefetch all the resources' downloads for all selected organization into memory
36
- """
37
- if self.downloads_counts is not None:
38
- return self.downloads_counts
39
-
40
- self.downloads_counts = {}
41
-
42
- ids = [o.id for o in self.queryset]
43
- for dataset in Dataset.objects(organization__in=ids):
44
- org_id = str(dataset.organization.id)
45
- if self.downloads_counts.get(org_id) is None:
46
- self.downloads_counts[org_id] = 0
47
-
48
- self.downloads_counts[org_id] += sum(
49
- resource.metrics.get("views", 0) for resource in dataset.resources
50
- )
51
-
52
- return self.downloads_counts
35
+ return csv.metric_fields(Organization)
@@ -35,3 +35,5 @@ def update_org_metrics(document, previous):
35
35
  previous.count_datasets()
36
36
  elif isinstance(document, Reuse):
37
37
  previous.count_reuses()
38
+ elif isinstance(document, Dataservice):
39
+ previous.count_dataservices()
@@ -188,6 +188,10 @@ class Organization(
188
188
  after_delete = Signal()
189
189
  on_delete = Signal()
190
190
 
191
+ def __init__(self, *args, **kwargs):
192
+ super().__init__(*args, **kwargs)
193
+ self.compute_aggregate_metrics = True
194
+
191
195
  @classmethod
192
196
  def pre_save(cls, sender, document, **kwargs):
193
197
  cls.before_save.send(document)
@@ -307,15 +311,16 @@ class Organization(
307
311
  from udata.models import Dataset, Follow, Reuse
308
312
 
309
313
  self.metrics["datasets"] = Dataset.objects(organization=self).visible().count()
310
- self.metrics["datasets_by_months"] = get_stock_metrics(
311
- Dataset.objects(organization=self).visible(), date_label="created_at_internal"
312
- )
313
- self.metrics["datasets_followers_by_months"] = get_stock_metrics(
314
- Follow.objects(following__in=Dataset.objects(organization=self)), date_label="since"
315
- )
316
- self.metrics["datasets_reuses_by_months"] = get_stock_metrics(
317
- Reuse.objects(datasets__in=Dataset.objects(organization=self)).visible()
318
- )
314
+ if self.compute_aggregate_metrics:
315
+ self.metrics["datasets_by_months"] = get_stock_metrics(
316
+ Dataset.objects(organization=self).visible(), date_label="created_at_internal"
317
+ )
318
+ self.metrics["datasets_followers_by_months"] = get_stock_metrics(
319
+ Follow.objects(following__in=Dataset.objects(organization=self)), date_label="since"
320
+ )
321
+ self.metrics["datasets_reuses_by_months"] = get_stock_metrics(
322
+ Reuse.objects(datasets__in=Dataset.objects(organization=self)).visible()
323
+ )
319
324
 
320
325
  self.save(signal_kwargs={"ignores": ["post_save"]})
321
326
 
@@ -42,3 +42,5 @@ def update_owner_metrics(document, previous):
42
42
  previous.count_datasets()
43
43
  elif isinstance(document, Reuse):
44
44
  previous.count_reuses()
45
+ elif isinstance(document, Dataservice):
46
+ previous.count_dataservices()