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 +27 -4
- udata/core/badges/tests/test_tasks.py +0 -2
- udata/core/dataservices/apiv2.py +1 -1
- udata/core/dataset/models.py +15 -3
- udata/core/dataset/rdf.py +10 -14
- udata/core/organization/apiv2.py +1 -1
- udata/core/organization/models.py +25 -5
- udata/core/reuse/apiv2.py +1 -1
- udata/core/user/models.py +21 -6
- udata/features/notifications/models.py +4 -1
- udata/features/transfer/models.py +16 -0
- udata/features/transfer/notifications.py +74 -0
- udata/harvest/backends/ckan/harvesters.py +10 -2
- udata/migrations/2025-12-16-create-transfer-request-notifications.py +66 -0
- udata/tasks.py +1 -0
- udata/tests/apiv2/test_dataservices.py +14 -0
- udata/tests/apiv2/test_organizations.py +9 -0
- udata/tests/apiv2/test_reuses.py +11 -0
- udata/tests/dataset/test_dataset_rdf.py +49 -0
- udata/tests/search/test_search_integration.py +37 -0
- udata/tests/test_transfer.py +104 -1
- udata/utils.py +23 -0
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/METADATA +2 -2
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/RECORD +28 -26
- udata/tests/apiv2/test_search.py +0 -30
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/WHEEL +0 -0
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/entry_points.txt +0 -0
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/licenses/LICENSE +0 -0
- {udata-14.5.1.dev11.dist-info → udata-14.6.1.dev5.dist-info}/top_level.txt +0 -0
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
|
udata/core/dataservices/apiv2.py
CHANGED
|
@@ -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")
|
udata/core/dataset/models.py
CHANGED
|
@@ -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(
|
|
564
|
-
|
|
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
|
|
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,
|
|
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
|
-
#
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
#
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
|
udata/core/organization/apiv2.py
CHANGED
|
@@ -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(
|
|
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"),
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
udata/tests/test_transfer.py
CHANGED
|
@@ -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.
|
|
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<
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
286
|
-
udata/features/transfer/notifications.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
494
|
-
udata/tests/apiv2/
|
|
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=
|
|
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=
|
|
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.
|
|
584
|
-
udata-14.
|
|
585
|
-
udata-14.
|
|
586
|
-
udata-14.
|
|
587
|
-
udata-14.
|
|
588
|
-
udata-14.
|
|
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,,
|
udata/tests/apiv2/test_search.py
DELETED
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|