udata 14.5.1.dev11__py3-none-any.whl → 14.6.1.dev5__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/api_fields.py CHANGED
@@ -79,13 +79,15 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
79
79
  user-supplied overrides, setting the readonly flag…), it's easier to have to do this only once at the end of the function.
80
80
 
81
81
  """
82
+ from udata.mongo.engine import db
83
+
82
84
  params: dict = {}
83
85
  params["required"] = field.required
84
86
 
85
87
  read_params: dict = {}
86
88
  write_params: dict = {}
87
89
 
88
- constructor: Callable
90
+ constructor: Callable | None = None
89
91
  constructor_read: Callable | None = None
90
92
  constructor_write: Callable | None = None
91
93
 
@@ -204,13 +206,34 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
204
206
  def constructor_write(**kwargs):
205
207
  return restx_fields.List(field_write, **kwargs)
206
208
 
207
- elif isinstance(
208
- field, (mongo_fields.GenericReferenceField, mongoengine.fields.GenericLazyReferenceField)
209
- ):
209
+ elif isinstance(field, mongoengine.fields.GenericLazyReferenceField):
210
210
 
211
211
  def constructor(**kwargs):
212
212
  return restx_fields.Nested(lazy_reference, **kwargs)
213
213
 
214
+ elif isinstance(field, mongo_fields.GenericReferenceField):
215
+ if field.choices:
216
+ generic_fields = {}
217
+ for cls in field.choices:
218
+ cls = db.resolve_model(cls) if isinstance(cls, str) else cls
219
+ generic_fields[cls.__name__] = convert_db_to_field(
220
+ f"{key}.{cls.__name__}",
221
+ # Instead of having GenericReferenceField() we'll create fields for each
222
+ # of the subclasses with ReferenceField(Organization)…
223
+ mongoengine.fields.ReferenceField(cls),
224
+ info,
225
+ )
226
+
227
+ def constructor_read(**kwargs):
228
+ return GenericField({k: v[0].model for k, v in generic_fields.items()}, **kwargs)
229
+
230
+ def constructor_write(**kwargs):
231
+ return GenericField({k: v[1].model for k, v in generic_fields.items()}, **kwargs)
232
+ else:
233
+
234
+ def constructor(**kwargs):
235
+ return restx_fields.Nested(lazy_reference, **kwargs)
236
+
214
237
  elif isinstance(field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField):
215
238
  # For reference we accept while writing a String representing the ID of the referenced model.
216
239
  # For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
@@ -1,5 +1,3 @@
1
- import udata.core.dataservices.tasks # noqa
2
- import udata.core.dataset.tasks # noqa
3
1
  from udata.core.badges.tasks import update_badges
4
2
  from udata.core.constants import HVD
5
3
  from udata.core.dataservices.factories import DataserviceFactory
@@ -15,7 +15,7 @@ apiv2.inherit("AccessAudience (read)", AccessAudience.__read_fields__)
15
15
 
16
16
  ns = apiv2.namespace("dataservices", "Dataservice related operations")
17
17
 
18
- search_parser = DataserviceSearch.as_request_parser()
18
+ search_parser = DataserviceSearch.as_request_parser(store_missing=False)
19
19
 
20
20
 
21
21
  @ns.route("/search/", endpoint="dataservice_search")
@@ -15,17 +15,19 @@ from mongoengine.fields import DateTimeField
15
15
  from mongoengine.signals import post_save, pre_init, pre_save
16
16
  from werkzeug.utils import cached_property
17
17
 
18
- from udata.api_fields import field
18
+ from udata.api_fields import field, generate_fields
19
19
  from udata.app import cache
20
20
  from udata.core import storages
21
21
  from udata.core.access_type.constants import AccessType
22
22
  from udata.core.access_type.models import WithAccessType, check_only_one_condition_per_role
23
23
  from udata.core.activity.models import Auditable
24
24
  from udata.core.constants import HVD
25
+ from udata.core.dataset.api_fields import temporal_coverage_fields
25
26
  from udata.core.dataset.preview import TabularAPIPreview
26
27
  from udata.core.linkable import Linkable
27
28
  from udata.core.metrics.helpers import get_stock_metrics
28
29
  from udata.core.owned import Owned, OwnedQuerySet
30
+ from udata.core.spatial.api_fields import spatial_coverage_fields
29
31
  from udata.frontend.markdown import mdstrip
30
32
  from udata.i18n import lazy_gettext as _
31
33
  from udata.models import Badge, BadgeMixin, BadgesList, SpatialCoverage, WithMetrics, db
@@ -89,6 +91,7 @@ def get_json_ld_extra(key, value):
89
91
  }
90
92
 
91
93
 
94
+ @generate_fields()
92
95
  class HarvestDatasetMetadata(db.EmbeddedDocument):
93
96
  backend = db.StringField()
94
97
  created_at = db.DateTimeField()
@@ -114,6 +117,7 @@ class HarvestResourceMetadata(db.EmbeddedDocument):
114
117
  dct_identifier = db.StringField()
115
118
 
116
119
 
120
+ @generate_fields()
117
121
  class Schema(db.EmbeddedDocument):
118
122
  """
119
123
  Schema can only be two things right now:
@@ -482,6 +486,7 @@ class ResourceMixin(object):
482
486
  return result
483
487
 
484
488
 
489
+ @generate_fields()
485
490
  class Resource(ResourceMixin, WithMetrics, db.EmbeddedDocument):
486
491
  """
487
492
  Local file, remote file or API provided by the original provider of the
@@ -533,6 +538,7 @@ class DatasetBadgeMixin(BadgeMixin):
533
538
  __badges__ = BADGES
534
539
 
535
540
 
541
+ @generate_fields()
536
542
  class Dataset(
537
543
  Auditable, WithMetrics, WithAccessType, DatasetBadgeMixin, Owned, Linkable, db.Document
538
544
  ):
@@ -560,8 +566,14 @@ class Dataset(
560
566
 
561
567
  frequency = field(db.EnumField(UpdateFrequency))
562
568
  frequency_date = field(db.DateTimeField(verbose_name=_("Future date of update")))
563
- temporal_coverage = field(db.EmbeddedDocumentField(db.DateRange))
564
- spatial = field(db.EmbeddedDocumentField(SpatialCoverage))
569
+ temporal_coverage = field(
570
+ db.EmbeddedDocumentField(db.DateRange),
571
+ nested_fields=temporal_coverage_fields,
572
+ )
573
+ spatial = field(
574
+ db.EmbeddedDocumentField(SpatialCoverage),
575
+ nested_fields=spatial_coverage_fields,
576
+ )
565
577
  schema = field(db.EmbeddedDocumentField(Schema))
566
578
 
567
579
  ext = field(db.MapField(db.GenericEmbeddedDocumentField()), auditable=False)
udata/core/dataset/rdf.py CHANGED
@@ -5,7 +5,7 @@ This module centralize dataset helpers for RDF/DCAT serialization and parsing
5
5
  import calendar
6
6
  import json
7
7
  import logging
8
- from datetime import date, datetime
8
+ from datetime import date
9
9
 
10
10
  from dateutil.parser import parse as parse_dt
11
11
  from flask import current_app
@@ -51,7 +51,7 @@ from udata.rdf import (
51
51
  themes_from_rdf,
52
52
  url_from_rdf,
53
53
  )
54
- from udata.utils import get_by, safe_unicode, to_naive_datetime
54
+ from udata.utils import get_by, safe_harvest_datetime, safe_unicode
55
55
 
56
56
  from .constants import OGC_SERVICE_FORMATS, UpdateFrequency
57
57
  from .models import Checksum, Dataset, License, Resource
@@ -729,12 +729,10 @@ def resource_from_rdf(graph_or_distrib, dataset=None, is_additionnal=False):
729
729
  resource.harvest = HarvestResourceMetadata()
730
730
  resource.harvest.issued_at = issued_at
731
731
 
732
- # In the past, we've encountered future `modified_at` during harvesting
733
- # do not save it. :FutureHarvestModifiedAt
734
- if modified_at and to_naive_datetime(modified_at) > datetime.utcnow():
735
- log.warning(f"Future `DCT.modified` date '{modified_at}' in resource")
736
- else:
737
- resource.harvest.modified_at = modified_at
732
+ # :FutureHarvestModifiedAt
733
+ resource.harvest.modified_at = safe_harvest_datetime(
734
+ modified_at, "DCT.modified (resource)", refuse_future=True
735
+ )
738
736
 
739
737
  resource.harvest.dct_identifier = identifier
740
738
  resource.harvest.uri = uri
@@ -845,12 +843,10 @@ def dataset_from_rdf(
845
843
  dataset.harvest.created_at = created_at
846
844
  dataset.harvest.issued_at = issued_at
847
845
 
848
- # In the past, we've encountered future `modified_at` during harvesting
849
- # do not save it. :FutureHarvestModifiedAt
850
- if modified_at and to_naive_datetime(modified_at) > datetime.utcnow():
851
- log.warning(f"Future `DCT.modified` date '{modified_at}' in dataset")
852
- else:
853
- dataset.harvest.modified_at = modified_at
846
+ # :FutureHarvestModifiedAt
847
+ dataset.harvest.modified_at = safe_harvest_datetime(
848
+ modified_at, "DCT.modified (dataset)", refuse_future=True
849
+ )
854
850
 
855
851
  return dataset
856
852
 
@@ -15,7 +15,7 @@ apiv2.inherit("ContactPoint", contact_point_fields)
15
15
 
16
16
 
17
17
  ns = apiv2.namespace("organizations", "Organization related operations")
18
- search_parser = OrganizationSearch.as_request_parser()
18
+ search_parser = OrganizationSearch.as_request_parser(store_missing=False)
19
19
 
20
20
  DEFAULT_SORTING = "-created_at"
21
21
 
@@ -7,7 +7,7 @@ from flask_babel import LazyString
7
7
  from mongoengine.signals import post_save, pre_save
8
8
  from werkzeug.utils import cached_property
9
9
 
10
- from udata.api_fields import field
10
+ from udata.api_fields import field, generate_fields
11
11
  from udata.core.activity.models import Auditable
12
12
  from udata.core.badges.models import Badge, BadgeMixin, BadgesList
13
13
  from udata.core.linkable import Linkable
@@ -21,6 +21,7 @@ from udata.uris import cdata_url
21
21
 
22
22
  from .constants import (
23
23
  ASSOCIATION,
24
+ BIGGEST_LOGO_SIZE,
24
25
  CERTIFIED,
25
26
  COMPANY,
26
27
  DEFAULT_ROLE,
@@ -44,6 +45,7 @@ BADGES: dict[str, LazyString] = {
44
45
  }
45
46
 
46
47
 
48
+ @generate_fields()
47
49
  class Team(db.EmbeddedDocument):
48
50
  name = db.StringField(required=True)
49
51
  slug = db.SlugField(
@@ -54,6 +56,7 @@ class Team(db.EmbeddedDocument):
54
56
  members = db.ListField(db.ReferenceField("User"))
55
57
 
56
58
 
59
+ @generate_fields()
57
60
  class Member(db.EmbeddedDocument):
58
61
  user = db.ReferenceField("User")
59
62
  role = db.StringField(choices=list(ORG_ROLES), default=DEFAULT_ROLE)
@@ -64,6 +67,7 @@ class Member(db.EmbeddedDocument):
64
67
  return ORG_ROLES[self.role]
65
68
 
66
69
 
70
+ @generate_fields()
67
71
  class MembershipRequest(db.EmbeddedDocument):
68
72
  """
69
73
  Pending organization membership requests
@@ -113,18 +117,22 @@ class OrganizationBadge(Badge):
113
117
 
114
118
 
115
119
  class OrganizationBadgeMixin(BadgeMixin):
116
- badges = field(BadgesList(OrganizationBadge), **BadgeMixin.default_badges_list_params)
120
+ badges = field(
121
+ BadgesList(OrganizationBadge), show_as_ref=True, **BadgeMixin.default_badges_list_params
122
+ )
117
123
  __badges__ = BADGES
118
124
 
119
125
 
126
+ @generate_fields()
120
127
  class Organization(
121
128
  Auditable, WithMetrics, OrganizationBadgeMixin, Linkable, db.Datetimed, db.Document
122
129
  ):
123
- name = field(db.StringField(required=True))
124
- acronym = field(db.StringField(max_length=128))
130
+ name = field(db.StringField(required=True), show_as_ref=True)
131
+ acronym = field(db.StringField(max_length=128), show_as_ref=True)
125
132
  slug = field(
126
133
  db.SlugField(max_length=255, required=True, populate_from="name", update=True, follow=True),
127
134
  auditable=False,
135
+ show_as_ref=True,
128
136
  )
129
137
  description = field(
130
138
  db.StringField(required=True),
@@ -138,7 +146,11 @@ class Organization(
138
146
  basename=default_image_basename,
139
147
  max_size=LOGO_MAX_SIZE,
140
148
  thumbnails=LOGO_SIZES,
141
- )
149
+ ),
150
+ show_as_ref=True,
151
+ thumbnail_info={
152
+ "size": BIGGEST_LOGO_SIZE,
153
+ },
142
154
  )
143
155
  business_number_id = field(db.StringField(max_length=ORG_BID_SIZE_LIMIT))
144
156
 
@@ -265,6 +277,14 @@ class Organization(
265
277
  return request
266
278
  return None
267
279
 
280
+ @field(description="Link to the API endpoint for this organization", show_as_ref=True)
281
+ def uri(self, *args, **kwargs):
282
+ return self.self_api_url(*args, **kwargs)
283
+
284
+ @field(description="Link to the udata web page for this organization", show_as_ref=True)
285
+ def page(self, *args, **kwargs):
286
+ return self.self_web_url(*args, **kwargs)
287
+
268
288
  @classmethod
269
289
  def get(cls, id_or_slug):
270
290
  obj = cls.objects(slug=id_or_slug).first()
udata/core/reuse/apiv2.py CHANGED
@@ -11,7 +11,7 @@ apiv2.inherit("Reuse (read)", Reuse.__read_fields__)
11
11
 
12
12
  ns = apiv2.namespace("reuses", "Reuse related operations")
13
13
 
14
- search_parser = ReuseSearch.as_request_parser()
14
+ search_parser = ReuseSearch.as_request_parser(store_missing=False)
15
15
 
16
16
  DEFAULT_SORTING = "-created_at"
17
17
 
udata/core/user/models.py CHANGED
@@ -12,7 +12,7 @@ from flask_security import MongoEngineUserDatastore, RoleMixin, UserMixin
12
12
  from mongoengine.signals import post_save, pre_save
13
13
  from werkzeug.utils import cached_property
14
14
 
15
- from udata.api_fields import field
15
+ from udata.api_fields import field, generate_fields
16
16
  from udata.core import storages
17
17
  from udata.core.discussions.models import Discussion
18
18
  from udata.core.linkable import Linkable
@@ -23,7 +23,7 @@ from udata.models import Follow, WithMetrics, db
23
23
  from udata.uris import cdata_url
24
24
 
25
25
  from . import mails
26
- from .constants import AVATAR_SIZES
26
+ from .constants import AVATAR_SIZES, BIGGEST_AVATAR_SIZE
27
27
 
28
28
  __all__ = ("User", "Role", "datastore")
29
29
 
@@ -45,9 +45,12 @@ class UserSettings(db.EmbeddedDocument):
45
45
  prefered_language = db.StringField()
46
46
 
47
47
 
48
+ @generate_fields()
48
49
  class User(WithMetrics, UserMixin, Linkable, db.Document):
49
50
  slug = field(
50
- db.SlugField(max_length=255, required=True, populate_from="fullname"), auditable=False
51
+ db.SlugField(max_length=255, required=True, populate_from="fullname"),
52
+ auditable=False,
53
+ show_as_ref=True,
51
54
  )
52
55
  email = field(db.StringField(max_length=255, required=True, unique=True))
53
56
  password = field(db.StringField())
@@ -55,12 +58,16 @@ class User(WithMetrics, UserMixin, Linkable, db.Document):
55
58
  fs_uniquifier = field(db.StringField(max_length=64, unique=True, sparse=True))
56
59
  roles = field(db.ListField(db.ReferenceField(Role), default=[]))
57
60
 
58
- first_name = field(db.StringField(max_length=255, required=True))
59
- last_name = field(db.StringField(max_length=255, required=True))
61
+ first_name = field(db.StringField(max_length=255, required=True), show_as_ref=True)
62
+ last_name = field(db.StringField(max_length=255, required=True), show_as_ref=True)
60
63
 
61
64
  avatar_url = field(db.URLField())
62
65
  avatar = field(
63
- db.ImageField(fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES)
66
+ db.ImageField(fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES),
67
+ show_as_ref=True,
68
+ thumbnail_info={
69
+ "size": BIGGEST_AVATAR_SIZE,
70
+ },
64
71
  )
65
72
  website = field(db.URLField())
66
73
  about = field(
@@ -199,6 +206,14 @@ class User(WithMetrics, UserMixin, Linkable, db.Document):
199
206
  """Return the number of followers of the user."""
200
207
  return self.metrics.get("followers", 0)
201
208
 
209
+ @field(description="Link to the API endpoint for this user", show_as_ref=True)
210
+ def uri(self, *args, **kwargs):
211
+ return self.self_api_url(*args, **kwargs)
212
+
213
+ @field(description="Link to the udata web page for this user", show_as_ref=True)
214
+ def page(self, *args, **kwargs):
215
+ return self.self_web_url(*args, **kwargs)
216
+
202
217
  def generate_api_key(self):
203
218
  payload = {
204
219
  "user": str(self.id),
@@ -5,6 +5,7 @@ from udata.api_fields import field, generate_fields
5
5
  from udata.core.organization.notifications import MembershipRequestNotificationDetails
6
6
  from udata.core.user.api_fields import user_ref_fields
7
7
  from udata.core.user.models import User
8
+ from udata.features.transfer.notifications import TransferRequestNotificationDetails
8
9
  from udata.models import db
9
10
  from udata.mongo.datetime_fields import Datetimed
10
11
  from udata.mongo.queryset import UDataQuerySet
@@ -51,6 +52,8 @@ class Notification(Datetimed, db.Document):
51
52
  filterable={},
52
53
  )
53
54
  details = field(
54
- db.GenericEmbeddedDocumentField(choices=(MembershipRequestNotificationDetails,)),
55
+ db.GenericEmbeddedDocumentField(
56
+ choices=(MembershipRequestNotificationDetails, TransferRequestNotificationDetails)
57
+ ),
55
58
  generic=True,
56
59
  )
@@ -1,6 +1,9 @@
1
1
  import logging
2
2
  from datetime import datetime
3
3
 
4
+ from blinker import Signal
5
+ from mongoengine.signals import post_save
6
+
4
7
  from udata.i18n import lazy_gettext as _
5
8
  from udata.mongo import db
6
9
 
@@ -30,6 +33,8 @@ class Transfer(db.Document):
30
33
  responder = db.ReferenceField("User")
31
34
  response_comment = db.StringField()
32
35
 
36
+ on_create = Signal()
37
+
33
38
  meta = {
34
39
  "indexes": [
35
40
  "owner",
@@ -38,3 +43,14 @@ class Transfer(db.Document):
38
43
  "status",
39
44
  ]
40
45
  }
46
+
47
+ @classmethod
48
+ def post_save(cls, sender, document, **kwargs):
49
+ """Handle post save signal for Transfer documents."""
50
+ # Only trigger on_create signal on creation, not on every save
51
+ if kwargs.get("created"):
52
+ cls.on_create.send(document)
53
+
54
+
55
+ # Connect the post_save signal
56
+ post_save.connect(Transfer.post_save, sender=Transfer)
@@ -1,11 +1,85 @@
1
1
  import logging
2
2
 
3
+ from udata.api_fields import field, generate_fields
4
+ from udata.core.dataservices.models import Dataservice
5
+ from udata.core.dataset.models import Dataset
6
+ from udata.core.organization.models import Organization
7
+ from udata.core.reuse.models import Reuse
8
+ from udata.core.user.models import User
3
9
  from udata.features.notifications.actions import notifier
4
10
  from udata.models import Transfer
11
+ from udata.mongo import db
5
12
 
6
13
  log = logging.getLogger(__name__)
7
14
 
8
15
 
16
+ @generate_fields()
17
+ class TransferRequestNotificationDetails(db.EmbeddedDocument):
18
+ transfer_owner = field(
19
+ db.GenericReferenceField(choices=(User, Organization), required=True),
20
+ readonly=True,
21
+ auditable=False,
22
+ allow_null=True,
23
+ filterable={},
24
+ )
25
+ transfer_recipient = field(
26
+ db.GenericReferenceField(choices=(User, Organization), required=True),
27
+ readonly=True,
28
+ auditable=False,
29
+ allow_null=True,
30
+ filterable={},
31
+ )
32
+ transfer_subject = field(
33
+ db.GenericReferenceField(choices=(Dataset, Dataservice, Reuse), required=True),
34
+ readonly=True,
35
+ auditable=False,
36
+ allow_null=True,
37
+ filterable={},
38
+ )
39
+
40
+
41
+ @Transfer.on_create.connect
42
+ def on_transfer_created(transfer, **kwargs):
43
+ """Create notification when a new transfer request is created"""
44
+
45
+ from udata.features.notifications.models import Notification
46
+
47
+ recipient = transfer.recipient
48
+ owner = transfer.owner
49
+ users = []
50
+
51
+ if isinstance(recipient, User):
52
+ users = [recipient]
53
+ elif isinstance(recipient, Organization):
54
+ users = [member.user for member in recipient.members if member.role == "admin"]
55
+
56
+ for user in users:
57
+ try:
58
+ existing = Notification.objects(
59
+ user=user,
60
+ details__transfer_recipient=recipient,
61
+ details__transfer_owner=owner,
62
+ details__transfer_subject=transfer.subject,
63
+ ).first()
64
+
65
+ if not existing:
66
+ notification = Notification(
67
+ user=user,
68
+ details=TransferRequestNotificationDetails(
69
+ transfer_owner=owner,
70
+ transfer_recipient=recipient,
71
+ transfer_subject=transfer.subject,
72
+ ),
73
+ )
74
+ notification.created_at = transfer.created
75
+ notification.save()
76
+ except Exception as e:
77
+ log.error(
78
+ f"Error creating notification for admin user {user.id} "
79
+ f"and recipient {recipient.id}: {e}"
80
+ )
81
+
82
+
9
83
  @notifier("transfer_request")
10
84
  def transfer_request_notifications(user):
11
85
  """Notify user about pending transfer requests"""
@@ -3,6 +3,8 @@ import logging
3
3
  from urllib.parse import urljoin
4
4
  from uuid import UUID
5
5
 
6
+ from dateutil.parser import ParserError
7
+
6
8
  from udata import uris
7
9
  from udata.core.dataset.constants import UpdateFrequency
8
10
  from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
@@ -202,10 +204,16 @@ class CkanBackend(BaseBackend):
202
204
  log.debug("frequency value not handled: %s", value)
203
205
  # Temporal coverage start
204
206
  elif key == "temporal_start":
205
- temporal_start = daterange_start(value)
207
+ try:
208
+ temporal_start = daterange_start(value)
209
+ except ParserError:
210
+ log.warning(f"Unparseable temporal_start value: '{value}'")
206
211
  # Temporal coverage end
207
212
  elif key == "temporal_end":
208
- temporal_end = daterange_end(value)
213
+ try:
214
+ temporal_end = daterange_end(value)
215
+ except ParserError:
216
+ log.warning(f"Unparseable temporal_end value: '{value}'")
209
217
  else:
210
218
  dataset.extras[extra["key"]] = value
211
219
 
@@ -0,0 +1,66 @@
1
+ """
2
+ Create TransferRequestNotification for all pending transfer requests
3
+ """
4
+
5
+ import logging
6
+
7
+ import click
8
+
9
+ from udata.features.notifications.models import Notification
10
+ from udata.features.transfer.models import Transfer
11
+ from udata.features.transfer.notifications import TransferRequestNotificationDetails
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def migrate(db):
17
+ log.info("Processing pending transfer requests...")
18
+
19
+ created_count = 0
20
+
21
+ # Get all pending transfers
22
+ transfers = Transfer.objects(status="pending")
23
+
24
+ with click.progressbar(transfers, length=transfers.count()) as transfer_list:
25
+ for transfer in transfer_list:
26
+ # Get the recipient (could be a user or an organization)
27
+ recipient = transfer.recipient
28
+
29
+ # For organizations, we need to find admins who should receive notifications
30
+ if recipient._cls == "Organization":
31
+ # Get all admin users for this organization
32
+ recipient_users = [
33
+ member.user for member in recipient.members if member.role == "admin"
34
+ ]
35
+ else:
36
+ # For users, just use the recipient directly
37
+ recipient_users = [recipient]
38
+
39
+ # Create a notification for each recipient user
40
+ for recipient_user in recipient_users:
41
+ try:
42
+ # Check if notification already exists
43
+ existing = Notification.objects(
44
+ user=recipient_user,
45
+ details__transfer_recipient=recipient,
46
+ details__transfer_owner=transfer.owner,
47
+ details__transfer_subject=transfer.subject,
48
+ ).first()
49
+ if not existing:
50
+ notification = Notification(user=recipient_user)
51
+ notification.details = TransferRequestNotificationDetails(
52
+ transfer_owner=transfer.owner,
53
+ transfer_recipient=recipient,
54
+ transfer_subject=transfer.subject,
55
+ )
56
+ # Set the created_at to match the transfer creation date
57
+ notification.created_at = transfer.created
58
+ notification.save()
59
+ created_count += 1
60
+ except Exception as e:
61
+ log.error(
62
+ f"Error creating notification for user {recipient_user.id} "
63
+ f"and transfer {transfer.id}: {e}"
64
+ )
65
+
66
+ log.info(f"Created {created_count} TransferRequestNotifications")
udata/tasks.py CHANGED
@@ -161,6 +161,7 @@ def init_app(app):
161
161
  import udata.core.metrics.tasks # noqa
162
162
  import udata.core.tags.tasks # noqa
163
163
  import udata.core.activity.tasks # noqa
164
+ import udata.core.dataservices.tasks # noqa
164
165
  import udata.core.dataset.tasks # noqa
165
166
  import udata.core.dataset.transport # noqa
166
167
  import udata.core.dataset.recommendations # noqa
@@ -0,0 +1,14 @@
1
+ from udata.core.dataservices.factories import DataserviceFactory
2
+ from udata.tests.api import APITestCase
3
+
4
+
5
+ class DataserviceSearchAPIV2Test(APITestCase):
6
+ def test_dataservice_search_with_model_query_param(self):
7
+ """Searching dataservices with 'model' as query param should not crash.
8
+
9
+ Regression test for: TypeError: query() got multiple values for argument 'model'
10
+ """
11
+ DataserviceFactory.create_batch(3)
12
+
13
+ response = self.get("/api/2/dataservices/search/?model=malicious")
14
+ self.assert200(response)
@@ -4,6 +4,15 @@ from udata.core.organization.factories import Member, OrganizationFactory
4
4
  from udata.tests.api import APITestCase
5
5
 
6
6
 
7
+ class OrganizationSearchAPIV2Test(APITestCase):
8
+ def test_organization_search_with_model_query_param(self):
9
+ """Searching organizations with 'model' as query param should not crash."""
10
+ OrganizationFactory.create_batch(3)
11
+
12
+ response = self.get("/api/2/organizations/search/?model=malicious")
13
+ self.assert200(response)
14
+
15
+
7
16
  class OrganizationExtrasAPITest(APITestCase):
8
17
  def setUp(self):
9
18
  self.login()
@@ -0,0 +1,11 @@
1
+ from udata.core.reuse.factories import ReuseFactory
2
+ from udata.tests.api import APITestCase
3
+
4
+
5
+ class ReuseSearchAPIV2Test(APITestCase):
6
+ def test_reuse_search_with_model_query_param(self):
7
+ """Searching reuses with 'model' as query param should not crash."""
8
+ ReuseFactory.create_batch(3)
9
+
10
+ response = self.get("/api/2/reuses/search/?model=malicious")
11
+ self.assert200(response)
@@ -518,6 +518,22 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
518
518
  assert isinstance(dataset, Dataset)
519
519
  assert dataset.harvest.modified_at is None
520
520
 
521
+ def test_unparseable_modified_at(self):
522
+ """Regression test: template strings like {{modified:toISO}} should not crash parsing."""
523
+ node = BNode()
524
+ g = Graph()
525
+
526
+ g.add((node, RDF.type, DCAT.Dataset))
527
+ g.add((node, DCT.identifier, Literal(faker.uuid4())))
528
+ g.add((node, DCT.title, Literal(faker.sentence())))
529
+ g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
530
+
531
+ dataset = dataset_from_rdf(g)
532
+ dataset.validate()
533
+
534
+ assert isinstance(dataset, Dataset)
535
+ assert dataset.harvest.modified_at is None
536
+
521
537
  def test_contact_point_individual_vcard(self):
522
538
  g = Graph()
523
539
  node = URIRef("https://test.org/dataset")
@@ -864,6 +880,39 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
864
880
  assert resource.harvest.modified_at.date() == modified.date()
865
881
  assert resource.format == "csv"
866
882
 
883
+ def test_resource_future_modified_at(self):
884
+ node = BNode()
885
+ g = Graph()
886
+
887
+ modified = faker.future_datetime()
888
+
889
+ g.add((node, RDF.type, DCAT.Distribution))
890
+ g.add((node, DCT.title, Literal(faker.sentence())))
891
+ g.add((node, DCAT.downloadURL, Literal(faker.uri())))
892
+ g.add((node, DCT.modified, Literal(modified)))
893
+
894
+ resource = resource_from_rdf(g)
895
+ resource.validate()
896
+
897
+ assert isinstance(resource, Resource)
898
+ assert resource.harvest.modified_at is None
899
+
900
+ def test_resource_unparseable_modified_at(self):
901
+ """Regression test: template strings like {{modified:toISO}} should not crash parsing."""
902
+ node = BNode()
903
+ g = Graph()
904
+
905
+ g.add((node, RDF.type, DCAT.Distribution))
906
+ g.add((node, DCT.title, Literal(faker.sentence())))
907
+ g.add((node, DCAT.downloadURL, Literal(faker.uri())))
908
+ g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
909
+
910
+ resource = resource_from_rdf(g)
911
+ resource.validate()
912
+
913
+ assert isinstance(resource, Resource)
914
+ assert resource.harvest.modified_at is None
915
+
867
916
  def test_download_url_over_access_url(self):
868
917
  node = BNode()
869
918
  g = Graph()
@@ -3,6 +3,8 @@ import time
3
3
  import pytest
4
4
 
5
5
  from udata.core.dataset.factories import DatasetFactory
6
+ from udata.core.organization.factories import OrganizationFactory
7
+ from udata.core.reuse.factories import VisibleReuseFactory
6
8
  from udata.tests.api import APITestCase
7
9
  from udata.tests.helpers import requires_search_service
8
10
 
@@ -31,3 +33,38 @@ class SearchIntegrationTest(APITestCase):
31
33
 
32
34
  titles = [d["title"] for d in response.json["data"]]
33
35
  assert "Données spectaculaires sur les transports" in titles
36
+
37
+ def test_reuse_search_with_organization_filter(self):
38
+ """
39
+ Regression test for: 500 Server Error when None values are passed to search service.
40
+
41
+ When searching reuses with only an organization filter, other params should not be
42
+ sent as literal 'None' strings (e.g. ?q=None&tag=None).
43
+ """
44
+ org = OrganizationFactory()
45
+ reuse = VisibleReuseFactory(organization=org)
46
+
47
+ time.sleep(1)
48
+
49
+ response = self.get(f"/api/2/reuses/search/?organization={org.id}")
50
+ self.assert200(response)
51
+ assert response.json["total"] >= 1
52
+ ids = [r["id"] for r in response.json["data"]]
53
+ assert str(reuse.id) in ids
54
+
55
+ def test_organization_search_with_query(self):
56
+ """
57
+ Regression test for: 500 Server Error when None values are passed to search service.
58
+
59
+ When searching organizations, other params should not be sent as literal
60
+ 'None' strings (e.g. ?badge=None).
61
+ """
62
+ org = OrganizationFactory(name="Organisation Unique Test")
63
+
64
+ time.sleep(1)
65
+
66
+ response = self.get("/api/2/organizations/search/?q=unique")
67
+ self.assert200(response)
68
+ assert response.json["total"] >= 1
69
+ ids = [o["id"] for o in response.json["data"]]
70
+ assert str(org.id) in ids
@@ -10,11 +10,13 @@ from udata.core.user.factories import UserFactory
10
10
  from udata.core.user.metrics import (
11
11
  update_owner_metrics, # noqa needed to register signals
12
12
  )
13
+ from udata.features.notifications.models import Notification
13
14
  from udata.features.transfer.actions import accept_transfer, request_transfer
14
15
  from udata.features.transfer.factories import TransferFactory
15
16
  from udata.features.transfer.notifications import transfer_request_notifications
16
17
  from udata.models import Member
17
- from udata.tests.api import PytestOnlyDBTestCase
18
+ from udata.tests.api import DBTestCase, PytestOnlyDBTestCase
19
+ from udata.tests.helpers import assert_equal_dates
18
20
  from udata.utils import faker
19
21
 
20
22
 
@@ -218,3 +220,104 @@ class TransferNotificationsTest(PytestOnlyDBTestCase):
218
220
  transfer = transfers[details["id"]]
219
221
  assert details["subject"]["class"] == "dataset"
220
222
  assert details["subject"]["id"] == transfer.subject.id
223
+
224
+
225
+ class TransferRequestNotificationTest(DBTestCase):
226
+ def test_notification_created_for_user_recipient(self):
227
+ """Notification is created for user recipient when transfer is requested"""
228
+ owner = UserFactory()
229
+ recipient = UserFactory()
230
+ dataset = DatasetFactory(owner=owner)
231
+
232
+ login_user(owner)
233
+ transfer = request_transfer(dataset, recipient, faker.sentence())
234
+
235
+ notifications = Notification.objects.all()
236
+ assert len(notifications) == 1
237
+
238
+ notification = notifications[0]
239
+ assert notification.user == recipient
240
+ assert notification.details.transfer_owner == owner
241
+ assert notification.details.transfer_recipient == recipient
242
+ assert notification.details.transfer_subject == dataset
243
+ assert_equal_dates(notification.created_at, transfer.created)
244
+
245
+ def test_notification_created_for_org_admins_only(self):
246
+ """Notifications are created for all admin users of recipient org, not editors"""
247
+ owner = UserFactory()
248
+ admin1 = UserFactory()
249
+ admin2 = UserFactory()
250
+ editor = UserFactory()
251
+ members = [
252
+ Member(user=editor, role="editor"),
253
+ Member(user=admin1, role="admin"),
254
+ Member(user=admin2, role="admin"),
255
+ ]
256
+ org = OrganizationFactory(members=members)
257
+ dataset = DatasetFactory(owner=owner)
258
+
259
+ login_user(owner)
260
+ transfer = request_transfer(dataset, org, faker.sentence())
261
+
262
+ notifications = Notification.objects.all()
263
+ assert len(notifications) == 2
264
+
265
+ admin_users = [notif.user for notif in notifications]
266
+ self.assertIn(admin1, admin_users)
267
+ self.assertIn(admin2, admin_users)
268
+
269
+ for notification in notifications:
270
+ assert notification.details.transfer_owner == owner
271
+ assert notification.details.transfer_recipient == org
272
+ assert notification.details.transfer_subject == dataset
273
+ assert_equal_dates(notification.created_at, transfer.created)
274
+
275
+ def test_no_duplicate_notifications(self):
276
+ """Duplicate notifications are not created for same transfer"""
277
+ owner = UserFactory()
278
+ recipient = UserFactory()
279
+ dataset = DatasetFactory(owner=owner)
280
+
281
+ login_user(owner)
282
+ request_transfer(dataset, recipient, faker.sentence())
283
+ request_transfer(dataset, recipient, faker.sentence())
284
+
285
+ assert Notification.objects.count() == 1
286
+
287
+ def test_multiple_transfers_create_separate_notifications(self):
288
+ """Multiple transfer requests create separate notifications"""
289
+ owner = UserFactory()
290
+ recipient = UserFactory()
291
+ dataset1 = DatasetFactory(owner=owner)
292
+ dataset2 = DatasetFactory(owner=owner)
293
+
294
+ login_user(owner)
295
+ request_transfer(dataset1, recipient, faker.sentence())
296
+ request_transfer(dataset2, recipient, faker.sentence())
297
+
298
+ notifications = Notification.objects.all()
299
+ assert len(notifications) == 2
300
+
301
+ subjects = [notif.details.transfer_subject for notif in notifications]
302
+ self.assertIn(dataset1, subjects)
303
+ self.assertIn(dataset2, subjects)
304
+
305
+ def test_notification_created_for_org_to_user_transfer(self):
306
+ """Notification is created when transferring from org to user"""
307
+ admin = UserFactory()
308
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
309
+ dataset = DatasetFactory(organization=org)
310
+ recipient = UserFactory()
311
+
312
+ login_user(admin)
313
+ transfer = request_transfer(dataset, recipient, faker.sentence())
314
+
315
+ notifications = Notification.objects.all()
316
+ assert len(notifications) == 1
317
+
318
+ notification = notifications[0]
319
+ assert notification.user == recipient
320
+ assert notification.details.transfer_owner == org
321
+ assert notification.details.transfer_recipient == recipient
322
+ assert notification.details.transfer_subject == dataset
323
+ assert_equal_dates(notification.created_at, transfer.created)
udata/utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import hashlib
2
2
  import itertools
3
+ import logging
3
4
  import math
4
5
  import re
5
6
  from collections import Counter
@@ -13,6 +14,7 @@ from xml.sax.saxutils import escape
13
14
  import factory
14
15
  from bson import ObjectId
15
16
  from bson.errors import InvalidId
17
+ from dateutil.parser import ParserError
16
18
  from dateutil.parser import parse as parse_dt
17
19
  from dateutil.relativedelta import relativedelta
18
20
  from faker import Faker
@@ -216,6 +218,27 @@ def to_naive_datetime(given_date: Any) -> datetime:
216
218
  return given_date
217
219
 
218
220
 
221
+ log = logging.getLogger(__name__)
222
+
223
+
224
+ def safe_harvest_datetime(value: Any, field: str, refuse_future: bool = False) -> datetime | None:
225
+ """
226
+ Safely parse a date/datetime value from harvested data.
227
+ Returns None and logs a warning if the value cannot be parsed or is in the future.
228
+ """
229
+ if value is None:
230
+ return None
231
+ try:
232
+ parsed = to_naive_datetime(value)
233
+ except ParserError:
234
+ log.warning(f"Unparseable {field} value: '{value}'")
235
+ return None
236
+ if refuse_future and parsed and parsed > datetime.utcnow():
237
+ log.warning(f"Future {field} value: '{value}'")
238
+ return None
239
+ return parsed
240
+
241
+
219
242
  def to_iso(dt: date | datetime) -> str | None:
220
243
  """
221
244
  Format a date or datetime into an ISO-8601 string
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: udata
3
- Version: 14.5.1.dev11
3
+ Version: 14.6.1.dev5
4
4
  Summary: Open data portal
5
5
  Author-email: Opendata Team <opendatateam@data.gouv.fr>
6
6
  Maintainer-email: Opendata Team <opendatateam@data.gouv.fr>
@@ -85,7 +85,7 @@ Requires-Dist: tzdata
85
85
  Requires-Dist: urlextract<2.0.0,>=1.9.0
86
86
  Requires-Dist: urllib3<3.0.0,>=2.0.0
87
87
  Requires-Dist: voluptuous<1.0.0,>=0.15.2
88
- Requires-Dist: werkzeug<3.1.4,>=3.0.0
88
+ Requires-Dist: werkzeug<4.0.0,>=3.0.0
89
89
  Requires-Dist: wtforms[email]<4.0.0,>=3.2.1
90
90
  Requires-Dist: wtforms-json<1.0.0,>=0.3.5
91
91
  Dynamic: license-file
@@ -1,5 +1,5 @@
1
1
  udata/__init__.py,sha256=U0HEYqKCLOY43O1UCVeuAb3b3SSX1pPhsJGpHJmK67k,75
2
- udata/api_fields.py,sha256=audhSLNXFXqEZgOo7bKXCTotxU_9mDB5nM9LUSDp44M,38487
2
+ udata/api_fields.py,sha256=Kdp_dzVgYRBy5e5L40F-itQcEJO2FFupKhsW7MHYzsQ,39523
3
3
  udata/app.py,sha256=E4s0z50dHULPY1ZxxCB1H01Qd-EWupT6fk9zg2L9KUw,8521
4
4
  udata/cors.py,sha256=7An9bsiNZk0PbNqDy8--ZJ0vmq1ubrtcfGB3DWAHBlA,3681
5
5
  udata/errors.py,sha256=E8W7b4PH7c5B85g_nsUMt8fHqMVpDFOZFkO6wMPl6bA,117
@@ -12,10 +12,10 @@ udata/sentry.py,sha256=j_6PSHV1id21KFX1XvpQR-Ur4d24310HgIq7MynEZ2Q,2887
12
12
  udata/settings.py,sha256=tk9VyAxg-umX3QW8hfFRsdA683IHxrEOBV-lJOWScGg,22490
13
13
  udata/sitemap.py,sha256=oRRWoPI7ZsFFnUAOqGT1YuXFFKHBe8EcRnUCNHD7xjM,979
14
14
  udata/tags.py,sha256=8R2gJieQtHgj7ZWIckMCkQu39fqzEUehxlYRfSD6bYQ,631
15
- udata/tasks.py,sha256=1cK9HCtbFFbsOztW_3sr-9LDM3h7vKvSW1GaUnOTUnI,5208
15
+ udata/tasks.py,sha256=nP_FqUrRaNvfVLByekN0sRXYTAE2PADMhY5PLTgmN5s,5257
16
16
  udata/tracking.py,sha256=WOcqA1RlHN8EPFuEc2kNau54mec4-pvi-wUFrMXevzg,345
17
17
  udata/uris.py,sha256=YMIZv6ypIYfYGAeLUJuBt58fbkpJ6fSzdWDKCbAylKA,4391
18
- udata/utils.py,sha256=S8ViJSbQgzT3RYbcCW5JTkTjkI1hhifLinPXVuASvl4,13820
18
+ udata/utils.py,sha256=Uxw3glbyKKqp_BC7c9_zAsTPiOOKxhuFyqxbWjvTG80,14531
19
19
  udata/worker.py,sha256=K-Wafye5-uXP4kQlffRKws2J9YbJ6m6n2QjcVsY8Nsg,118
20
20
  udata/wsgi.py,sha256=MY8en9K9eDluvJYUxTdzqSDoYaDgCVZ69ZcUvxAvgqA,77
21
21
  udata/api/__init__.py,sha256=y-ULl38jINI7RfdZezCZ544nIGiZo9_pgo3Zj4NKA8w,11854
@@ -75,7 +75,7 @@ udata/core/badges/tasks.py,sha256=6ulyDIE6GmR1U9UtQWzGA6joupqRF8GntR6CWFifSzY,13
75
75
  udata/core/badges/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  udata/core/badges/tests/test_commands.py,sha256=3MMs6DnltAQUClrd7xdi2dYT0qIfmcxojOFL-I9q5Bg,1295
77
77
  udata/core/badges/tests/test_model.py,sha256=pTmGFPNKMvphFMOCYmVaqxCEBdsVAfUVcG5Bhmn3WzI,5409
78
- udata/core/badges/tests/test_tasks.py,sha256=BhBoO1lciq-mADZxojk4Pqtj0BAZarLraeQbDIxascM,2203
78
+ udata/core/badges/tests/test_tasks.py,sha256=0ScMM0NWY7QAAo8LVZzmp2atatuvNwdnQrqX9zhUJbE,2118
79
79
  udata/core/contact_point/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
80
  udata/core/contact_point/api.py,sha256=vZI4Ue8hYi1n2aplQ80q_l50aQ4s1YKJwIABFw3lurg,2772
81
81
  udata/core/contact_point/api_fields.py,sha256=qx_80yeRMS6rKSjQZOSGZk56le0t6l2s9_qJ5us8eZg,1396
@@ -85,7 +85,7 @@ udata/core/contact_point/models.py,sha256=4GKbf0C1r1id35t4eQ3RcZIEfS_9kb6WW4QaXq
85
85
  udata/core/dataservices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  udata/core/dataservices/activities.py,sha256=wcYQCyYpKciCz99VqQqKti72a5Fyhc-AvDZBWdF0KUc,1763
87
87
  udata/core/dataservices/api.py,sha256=u1aiwIwz9ypw8I6XT8gAbpN6M3qJGcVSK2UaFRJT5tg,10263
88
- udata/core/dataservices/apiv2.py,sha256=FWkEfreOuyX7Krr604rQzSGLtBoVZrOR4nBEkYlhVIc,1231
88
+ udata/core/dataservices/apiv2.py,sha256=hHwWp0mJtrFJMdylO2_2vPmvdJwFbJNsG9ufcPWONyE,1250
89
89
  udata/core/dataservices/constants.py,sha256=PlfoLJv1rqRSUHbCe80vGkfTl9B0hUYgCxrop_e5JUY,45
90
90
  udata/core/dataservices/csv.py,sha256=HWI2JrN_Vuw0te9FHlJ6eyqcRcKHOKXuzg45D4Ti6F0,1106
91
91
  udata/core/dataservices/factories.py,sha256=pKVoArNSCIbvGA-cWUc7vr8TmjYsUvOXzzcuUB5JyF4,964
@@ -108,10 +108,10 @@ udata/core/dataset/exceptions.py,sha256=uKiayLSpSzsnLvClObS6hOO0qXEqvURKN7_w8eim
108
108
  udata/core/dataset/factories.py,sha256=tb18axsk8Tx5iUIqWM9IELdt-2Ryp2UN0-iY4fdea4U,9059
109
109
  udata/core/dataset/forms.py,sha256=q345go8G6qN1_YTX1Uhpl7M2kqF_44KA91OnFMcOGYk,8207
110
110
  udata/core/dataset/metrics.py,sha256=s8Xs_rqRXfNWsErkiJTuRMG5o_cU5iSK8mUJFKVSc7w,1204
111
- udata/core/dataset/models.py,sha256=NY1h3aY-OQzp80ZR9LuEzg2aXWTHk0srpsAJd_Np5jI,42089
111
+ udata/core/dataset/models.py,sha256=fmJCuRbqVEJR6EzBQWBiPhrf-F-kQMdSGi6LJ_hw13c,42440
112
112
  udata/core/dataset/permissions.py,sha256=qZCo_wKRwm_hONKdAPhYcb4PqJj7qW4PBqH5WJ0FWB4,2199
113
113
  udata/core/dataset/preview.py,sha256=uFEpK-p5nIAlY8hVOMhd7mtkwFt6C_PQRMNxPvAyoo4,839
114
- udata/core/dataset/rdf.py,sha256=jEpp0wrCvpfpkzlNEr6yroLCChcPo9TCHIfbjoRnYPM,33088
114
+ udata/core/dataset/rdf.py,sha256=wYX1olDd6n9M6QEm76q7nV_RVUuwSUDzz46ujIpWg64,32730
115
115
  udata/core/dataset/recommendations.py,sha256=DlGSLU8D0nW6Ds1rjBav1WxC-0VW5yOCjkO5w-ltFcI,7171
116
116
  udata/core/dataset/search.py,sha256=UEYMPwj4kDkGlGaUmsYECh1c3_BjCt1RDcMLDGrV_dA,6019
117
117
  udata/core/dataset/signals.py,sha256=WN4sV-lJlNsRkhcnhoy0SYJvCoYmK_5QFYZd1u-h4gs,161
@@ -154,7 +154,7 @@ udata/core/organization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
154
154
  udata/core/organization/activities.py,sha256=Mw4-R8Q6G745IZnCDgrj7h2ax2crGYRhZtcewSK6_Ok,1213
155
155
  udata/core/organization/api.py,sha256=RGyxe5LRqkyk5HzsXRB9r6z_YhGVqNeKkiaEgMaOvU8,22908
156
156
  udata/core/organization/api_fields.py,sha256=NXZc__i31ntun3Zt4OGt2EdpodbtQvHqzuta-TskNPY,7776
157
- udata/core/organization/apiv2.py,sha256=HVZmfO-Cw9hlMPcHKAC1HnSmPXeWpBz-hqWUDl2Bhs8,2983
157
+ udata/core/organization/apiv2.py,sha256=0zn-16vdCgpLkqWGw9XcVp43mrFLGR5mQEPJ-gkv9vA,3002
158
158
  udata/core/organization/commands.py,sha256=DsRAtFDZvTciYNsUWumQWdn0jnNmKW-PwfIHUUZoBb8,1591
159
159
  udata/core/organization/constants.py,sha256=fncNtA-vFrRM22K1Wo6iYu9DQZjzknYxH6TIYfxM9kA,563
160
160
  udata/core/organization/csv.py,sha256=zdLeB4La-TeOEELg0WI3FryoQWnoAYit_DssInVSHRI,979
@@ -162,7 +162,7 @@ udata/core/organization/factories.py,sha256=g8ubBcz79xbjvpunZ02IDOakFg1KE6cXjNkE
162
162
  udata/core/organization/forms.py,sha256=tscDb1_yOpbTx3ahl8ttA7oDkX9jIyzLc4gOf6WbN3s,3552
163
163
  udata/core/organization/mails.py,sha256=JxVzsJe4hoFbohB5hmWY33BmqMg8Bz-nwYDvnvUzXW8,4944
164
164
  udata/core/organization/metrics.py,sha256=CEhkZLUufDyWi2XyizMoXkuddz7xDJvmdkPTweqfWyI,1144
165
- udata/core/organization/models.py,sha256=6_HT7O5Fa1viiC2rv_bY3B8H3v5F-NoMcmCm1hi37Ag,11338
165
+ udata/core/organization/models.py,sha256=cGuavWXqCaCZgTveGKu7dCOFNYN5mv899w19_0Sez0g,12009
166
166
  udata/core/organization/notifications.py,sha256=bAtb-Of3KCAu30KBg0Y55qHJk9kx82HlMWSCYOfYyHM,3710
167
167
  udata/core/organization/permissions.py,sha256=hcnFuc8RkDesFSnq-ei4LV0ZUpRUf8zXyxRoXT_aLQc,1274
168
168
  udata/core/organization/rdf.py,sha256=KYJXTE5Yxhp3Cb7GZsRT1BY3Bd7rcRfwFSK9dWG2xQ4,1807
@@ -190,7 +190,7 @@ udata/core/reuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
190
190
  udata/core/reuse/activities.py,sha256=5D7cV-hGZnzHsp8hohZqqgK3RSGQpfAqJ_Wfq_AYfM8,1420
191
191
  udata/core/reuse/api.py,sha256=zRHYVkpceWwXyIKM7eMBz0VZAL0TMSdo7m-CicxbN5w,13779
192
192
  udata/core/reuse/api_fields.py,sha256=ccym6v9Ap68PlHZmIMMtHQFnEyV7Gbxrfdw0b6rj51A,1232
193
- udata/core/reuse/apiv2.py,sha256=ixayPlYkCl67GxqFBHijSigjkzmkXuY-HX7X-_SH-3U,863
193
+ udata/core/reuse/apiv2.py,sha256=EGAGY90rZpuKWi2kq7hBPCO9DIpbm9085ebwQSl42wM,882
194
194
  udata/core/reuse/constants.py,sha256=JgDBrjOKSt9q0auv9rjzbGsch83H-Oi8YXAKeI5hO4o,1215
195
195
  udata/core/reuse/csv.py,sha256=c9t9nyAqjx-QNyeku6RpcC8kSdlQ12wxzXCJHUj6GBY,899
196
196
  udata/core/reuse/factories.py,sha256=GrQqYTIvwQrwkvJrbTr38-2faFW_PC99gn3yOVpgFec,850
@@ -262,7 +262,7 @@ udata/core/user/factories.py,sha256=kkwaojciLzfuAOeRnL1E7XCcGPo8waAal_G2eeuVc0k,
262
262
  udata/core/user/forms.py,sha256=yotqZozH9ViKuNI8SwdKocDEi7NXVs_XUMpdr_bIe5s,966
263
263
  udata/core/user/mails.py,sha256=JPoPdjt38T3QECR7g7dqc5MbWxm1wwCNUYIO1I4VSaI,1945
264
264
  udata/core/user/metrics.py,sha256=J4zgjcAudQAi6NUIb47o2Pfe0xZ_Eu17ta9HjhE-HvE,1274
265
- udata/core/user/models.py,sha256=R0imfkTiNTryiccOZIdNgWIhKRwqDgbUMv9EWFgVe4E,11787
265
+ udata/core/user/models.py,sha256=9WIba-F4rmsIeECiJqu-vtPcGdZ2f4EazpOcUznuAKA,12362
266
266
  udata/core/user/permissions.py,sha256=Wbd8bLqSjqp9RHJ6ffLBj74L-LECcAhWazuw4Hq-2Gk,435
267
267
  udata/core/user/rdf.py,sha256=_tN8KlJHt8mYwJhLKoULhmZ3sapUGmX1Sl5W8uawxeU,718
268
268
  udata/core/user/tasks.py,sha256=iaL997_aYTvHSXekHWWHuYb6a1WNQkc48dtfOdE_4SI,2924
@@ -276,14 +276,14 @@ udata/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
276
276
  udata/features/notifications/__init__.py,sha256=R4-dHlQxCsLNWXhV05UnPiVuIpKEpV9KBboWnrdNSOo,352
277
277
  udata/features/notifications/actions.py,sha256=i8KvFbsC_JJnPOXSitfdNIdK6ABy8G88E5iwLiGYOyw,759
278
278
  udata/features/notifications/api.py,sha256=mdexblDEqhfQkcTfTT1ZO7dguDunXsopYF5mLuh3IZ0,665
279
- udata/features/notifications/models.py,sha256=0ldEsx3nm_-gJdnhYIKhZJzeux7aaVRLfHlrgwkWu9M,1825
279
+ udata/features/notifications/models.py,sha256=s92gODW0sIULQ4TMdJjMWGnlYDpPJ8YRRZ-vMPYXgaI,1967
280
280
  udata/features/notifications/tasks.py,sha256=dLXiQmElW9cmFAKLzZZgG4aruyRJsMPG7Dsy-U3H0f0,720
281
281
  udata/features/transfer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
282
  udata/features/transfer/actions.py,sha256=u27rJU3E3ZCPYlIMzVesrR1jvIbJIqaJ7EuasXiG8lg,1811
283
283
  udata/features/transfer/api.py,sha256=64lJWb7hFusaGF3bCtJ17-VxhBymzyb7x886vkNProY,6715
284
284
  udata/features/transfer/factories.py,sha256=2dAvRaemGieLn5aVEXQ6cmIGD2dTX5I0Ql2RrT77PfU,208
285
- udata/features/transfer/models.py,sha256=sFmdbEHL7eUxYe2XdtjZ2zhrUWOW_ryP_5T_8N1qUEA,958
286
- udata/features/transfer/notifications.py,sha256=jeH5buqTuWcokgXfEj-EZ7C3YCrtw-NikfrH2bqFyHM,1121
285
+ udata/features/transfer/models.py,sha256=b84x6CKzGKOfgA1VKzPaV1LvZ5GtT3kgEFqYpszRZlg,1420
286
+ udata/features/transfer/notifications.py,sha256=ynRizwI-JvZvFc68uaAzre65s-6-Zj_SnQCq79BbP4Q,3604
287
287
  udata/features/transfer/permissions.py,sha256=0Iwt_I3S3QACpq4Ba6Ovb8RUBuozj2pbXDtFFp9jdLI,953
288
288
  udata/flask_mongoengine/__init__.py,sha256=Ijqs6lu009OSWgUFdw1AdihjbbLL7-BekwZojXg92bU,599
289
289
  udata/flask_mongoengine/connection.py,sha256=b4dPVa8X_xbCFJ8ngt-RWLZtjNCHyZQSw-0fhRL6ySA,5191
@@ -318,7 +318,7 @@ udata/harvest/backends/dcat.py,sha256=FwFJ0SHhKkyrUUfILYn3iZ79dQJqGUF3BhW7Qrxb5f
318
318
  udata/harvest/backends/maaf.py,sha256=SN_831VeWphR63EC9v0n94bRH-9B5htTE70HSsVfzyc,8373
319
319
  udata/harvest/backends/maaf.xsd,sha256=vEyG8Vqw7Yn_acjRdXjqUJgxOj4pv8bibep-FX-f3BQ,18322
320
320
  udata/harvest/backends/ckan/__init__.py,sha256=JE7Qa7kX7Yd8OvmJnAO_NupZe0tqYyhhkgJ-iGNxX64,35
321
- udata/harvest/backends/ckan/harvesters.py,sha256=Bi4teYM1_Pw0HacRPObWFZimAxSz9CVdS9nGyb__Tnc,11035
321
+ udata/harvest/backends/ckan/harvesters.py,sha256=q_tkdpPE9lCTSe1l1KK-sFsujCVqUZfoCflZV5Hw5bE,11356
322
322
  udata/harvest/backends/ckan/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
323
323
  udata/harvest/backends/ckan/schemas/ckan.py,sha256=59VQoIxPBb5vOHFzzMtdnPcxHL4qNOTkPYa4cqn-7xQ,2353
324
324
  udata/harvest/backends/ckan/schemas/dkan.py,sha256=RyQGPEaKqnunIrr9yApcz7WEfgye0IT1bV85wa2E8Lc,2774
@@ -408,6 +408,7 @@ udata/migrations/2025-10-29-harvesters-sources-integrity.py,sha256=tt8ThVhtgiBSv
408
408
  udata/migrations/2025-10-31-create-membership-request-notifications.py,sha256=CZfUG7024dQj-Lz9fyp0duoATeBLlGSGb-8Gvr9B97Y,2228
409
409
  udata/migrations/2025-11-13-delete-user-email-index.py,sha256=3fSkKzDxaA-DDHRUAMg-IVFNsQrEHw8cFsDsZPX6dqE,569
410
410
  udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py,sha256=DODVCMMvBP1pF4mY69nK1tzYPMFuR51bfuEciron_mg,759
411
+ udata/migrations/2025-12-16-create-transfer-request-notifications.py,sha256=ewo3SexZ9a8jlDM1k1c4gxA741NvHORM42kVeShZoSc,2681
411
412
  udata/models/__init__.py,sha256=Z3RLj1KIFNc4RZCqHNtIrMm7RZ_5_60HLL9H5nS4lUk,1198
412
413
  udata/mongo/__init__.py,sha256=y4Rv-kq3o_kcEulcNpePLzocXPBNpx3Jd82G-VZPaMc,1421
413
414
  udata/mongo/datetime_fields.py,sha256=xACagQZu1OKPvpcznI-bMC1tJfAvo-VBUe7OOadnBdg,2089
@@ -466,7 +467,7 @@ udata/tests/test_routing.py,sha256=oOO_vxS2hc0YuDbr8S7rgKTj5HSrxjjkTqSgcDoxmR0,1
466
467
  udata/tests/test_storages.py,sha256=jJ74ieRNDk5IiqW28HTifksa5_vNey_cmqdvZ0nSRVs,9813
467
468
  udata/tests/test_tags.py,sha256=vIkI7FfWii6irMLJr7FuuugJGWZ0tTJAe58W8zu34rk,3732
468
469
  udata/tests/test_topics.py,sha256=KKcvsnRzdTy0n3jM2pG6G-CIj3qMfwCGhp0CluBFDLk,6209
469
- udata/tests/test_transfer.py,sha256=Ds3milUm9cblUvB6YNuMXDr3BBxAGBcfGIKS2y20vZc,7844
470
+ udata/tests/test_transfer.py,sha256=hVwn4ZWNMChgc0A9iPv1MZHg-KOcFnkRZXM3FwtDVG4,12025
470
471
  udata/tests/test_uris.py,sha256=RZpWlmR4nAVjBnzOubI3BdNie_skgJc75ES92QOJAjk,9714
471
472
  udata/tests/test_utils.py,sha256=3BGnlvw-GOE6tLHQteo-uUeYuzq4rsjePOuytFGkpOg,7967
472
473
  udata/tests/api/__init__.py,sha256=ByLEucyyN2y-hqEF8N9C_Qreo5mEjgwu1ej5Yd-GPY4,7065
@@ -488,10 +489,11 @@ udata/tests/api/test_tags_api.py,sha256=7XROh60k0rI9lzzJbFagbSEtE3nFdx3cnHNdOxix
488
489
  udata/tests/api/test_transfer_api.py,sha256=uYeouDL42X8ruzclgaVOa6VCA3_wc8Hq8HLa8jgpZyM,7485
489
490
  udata/tests/api/test_user_api.py,sha256=Xi4vcG1cpdh7L669tWdSUwOLUvD4E4qf_ShnoyBNwUU,16854
490
491
  udata/tests/apiv2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
492
+ udata/tests/apiv2/test_dataservices.py,sha256=-eg9ztOdFkgbXVrLZnv4uzwqxqMBS560HayR67Zjkng,550
491
493
  udata/tests/apiv2/test_datasets.py,sha256=lugL_CE2MlP4R--GEersBIwJOn66xVmLz5lU8L4YpcA,23216
492
494
  udata/tests/apiv2/test_me_api.py,sha256=YxSCtZBlSXgzQPgwMJoZHMcpSoKjL6IKz2jhJi8xL5I,1399
493
- udata/tests/apiv2/test_organizations.py,sha256=JqQHnOzRLR4l19P-EC9k2VToLbebU7jYESY4T-Y9LS8,6370
494
- udata/tests/apiv2/test_search.py,sha256=PW6vzchc-OUVH80kPIHdShqyD0lvbDmupa-DLjyT1QQ,1233
495
+ udata/tests/apiv2/test_organizations.py,sha256=rWIoYLL5-vP_pvX0W-H9YigAp64LYTLhJE-XhmFqNGs,6721
496
+ udata/tests/apiv2/test_reuses.py,sha256=zxS_YrsR4-c8gjFek711Mm057hPkyskcD0l6qi1852I,408
495
497
  udata/tests/apiv2/test_swagger.py,sha256=fIwblhKFnbXnLJb_Qs33P0c44LRm0c1JODblKEcf_4M,832
496
498
  udata/tests/apiv2/test_topics.py,sha256=pHHsPo7Qs8DoDqLBBcB0GtoptkHHXT25Jo96B2Vg_4k,37970
497
499
  udata/tests/cli/test_cli_base.py,sha256=opaL3La-ig47wpak7A-zdRUzj2hIXIYXD_0t842YrBQ,465
@@ -507,7 +509,7 @@ udata/tests/dataset/test_dataset_actions.py,sha256=8d-6AUKCt3Nnb_uEaztV0BzNYFDyq
507
509
  udata/tests/dataset/test_dataset_commands.py,sha256=e-tLIveg_Vp7nfOd5VhcPSiUQZ-VBv2no2lAu-2j6BE,820
508
510
  udata/tests/dataset/test_dataset_events.py,sha256=tKCQ55y_pc0wOKv2B2iej5dDxWalguU9FLtr6J2y8hE,3682
509
511
  udata/tests/dataset/test_dataset_model.py,sha256=YWhQ6RxVdgUrQbtvwATzypyl8XyNpdDjHDetyK37chU,35795
510
- udata/tests/dataset/test_dataset_rdf.py,sha256=RVIib5ah3B_wpTj9swbx4XSzUGyYj9_6kP_n416bnSQ,58782
512
+ udata/tests/dataset/test_dataset_rdf.py,sha256=__jm3_J5-r1u9YaBZPNFYaPlYCQ6Rt4Qr5elA-WKVzw,60503
511
513
  udata/tests/dataset/test_dataset_recommendations.py,sha256=UMwAsLHs6_XA1vp7-lnTjaPOc9E6zQYqw9VIuSCNUtk,7102
512
514
  udata/tests/dataset/test_dataset_tasks.py,sha256=6YmDBJUv3pIPNFj6DvY7FLsa4k7XxGeQ7BgOLrJOeTY,4564
513
515
  udata/tests/dataset/test_resource_preview.py,sha256=387FEdNHWZyEeOlmSETMIFlnhVuKQ-U4o2RkWgxXwik,2208
@@ -549,7 +551,7 @@ udata/tests/search/__init__.py,sha256=ub8kS6vG9EjzkJ-9sAR7j1eLSYpud1Gzry4MQ8fpgx
549
551
  udata/tests/search/test_adapter.py,sha256=4Sh8T-a8TYRMWUNe93WkVOy2b5q7i5hvjFUjXbX3UZw,8648
550
552
  udata/tests/search/test_query.py,sha256=aFZKFTJjlih5csy3eNp4MxL5hg13NqiIsrS5bCTOLrI,2182
551
553
  udata/tests/search/test_results.py,sha256=vHMkywoW6SQKGy5OtCriYpSo-KbVEdjVzOf8un2mjZE,2043
552
- udata/tests/search/test_search_integration.py,sha256=BZhoCNmvwDJV_jSYE9ILC3pmp_SeBYOiDfHfJMrwn4I,1190
554
+ udata/tests/search/test_search_integration.py,sha256=icDg7wJowl2P9ZUhFKe__MbQBH5QjLDx3d7U_fVpdAk,2623
553
555
  udata/tests/site/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
554
556
  udata/tests/site/test_site_api.py,sha256=j9pu91KfkL8-O0lyTA546cJJGZyENEauebF_uuxKVTE,1363
555
557
  udata/tests/site/test_site_csv_exports.py,sha256=XNNCUsX3Q17ov2nBOEfBLklKpwF3wxIG_1ZeayFfKls,17758
@@ -580,9 +582,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=n7ZHvruSL9hIPoSl4aCkGeC52LZYeg
580
582
  udata/translations/pt/LC_MESSAGES/udata.po,sha256=24CsHDZ84nqTMr-cOvOZ-LNYsokLQNyQchI41o3Cq9M,49765
581
583
  udata/translations/sr/LC_MESSAGES/udata.mo,sha256=pw3gsvr8lPQJZvX9Jo8ymu59I3L6-rrpX2Fqy0Nu5r4,20441
582
584
  udata/translations/sr/LC_MESSAGES/udata.po,sha256=1h8akWRpcQ1uD5zezqjp-Q-gAld5_93MkJL4BRlqKjQ,54738
583
- udata-14.5.1.dev11.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
584
- udata-14.5.1.dev11.dist-info/METADATA,sha256=trjyuqYYSgV-Z_WlafWqiw-iwiQu4DG3Or0AOGHXajo,4359
585
- udata-14.5.1.dev11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
586
- udata-14.5.1.dev11.dist-info/entry_points.txt,sha256=XwrEzP-n_6CKnwTsrNHzyCTWbMwg2FkvxVVB686f_C0,476
587
- udata-14.5.1.dev11.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
588
- udata-14.5.1.dev11.dist-info/RECORD,,
585
+ udata-14.6.1.dev5.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
586
+ udata-14.6.1.dev5.dist-info/METADATA,sha256=XQ1_c5xX7pIahXaX898-9JNVyCqUQXsA_B-gZd0_SqI,4358
587
+ udata-14.6.1.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
588
+ udata-14.6.1.dev5.dist-info/entry_points.txt,sha256=XwrEzP-n_6CKnwTsrNHzyCTWbMwg2FkvxVVB686f_C0,476
589
+ udata-14.6.1.dev5.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
590
+ udata-14.6.1.dev5.dist-info/RECORD,,
@@ -1,30 +0,0 @@
1
- from udata.core.dataservices.factories import DataserviceFactory
2
- from udata.core.organization.factories import OrganizationFactory
3
- from udata.core.reuse.factories import ReuseFactory
4
- from udata.tests.api import APITestCase
5
-
6
-
7
- class SearchAPIV2Test(APITestCase):
8
- def test_dataservice_search_with_model_query_param(self):
9
- """Searching dataservices with 'model' as query param should not crash.
10
-
11
- Regression test for: TypeError: query() got multiple values for argument 'model'
12
- """
13
- DataserviceFactory.create_batch(3)
14
-
15
- response = self.get("/api/2/dataservices/search/?model=malicious")
16
- self.assert200(response)
17
-
18
- def test_reuse_search_with_model_query_param(self):
19
- """Searching reuses with 'model' as query param should not crash."""
20
- ReuseFactory.create_batch(3)
21
-
22
- response = self.get("/api/2/reuses/search/?model=malicious")
23
- self.assert200(response)
24
-
25
- def test_organization_search_with_model_query_param(self):
26
- """Searching organizations with 'model' as query param should not crash."""
27
- OrganizationFactory.create_batch(3)
28
-
29
- response = self.get("/api/2/organizations/search/?model=malicious")
30
- self.assert200(response)