udata 12.0.2.dev10__py3-none-any.whl → 13.0.1.dev21__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/__init__.py +1 -0
- udata/api_fields.py +10 -4
- udata/app.py +11 -10
- udata/auth/__init__.py +9 -10
- udata/auth/mails.py +137 -45
- udata/auth/views.py +5 -12
- udata/commands/__init__.py +2 -4
- udata/commands/info.py +1 -3
- udata/commands/tests/test_fixtures.py +6 -3
- udata/core/access_type/api.py +18 -0
- udata/core/access_type/constants.py +98 -0
- udata/core/access_type/models.py +44 -0
- udata/core/activity/models.py +1 -1
- udata/core/badges/models.py +1 -1
- udata/core/badges/tasks.py +35 -1
- udata/core/badges/tests/test_commands.py +2 -4
- udata/core/badges/tests/test_model.py +2 -2
- udata/core/badges/tests/test_tasks.py +55 -0
- udata/core/constants.py +1 -0
- udata/core/contact_point/models.py +8 -0
- udata/core/dataservices/api.py +10 -12
- udata/core/dataservices/apiv2.py +3 -1
- udata/core/dataservices/constants.py +0 -29
- udata/core/dataservices/models.py +44 -44
- udata/core/dataservices/rdf.py +2 -1
- udata/core/dataservices/search.py +5 -9
- udata/core/dataservices/tasks.py +33 -0
- udata/core/dataset/api.py +15 -24
- udata/core/dataset/api_fields.py +11 -0
- udata/core/dataset/apiv2.py +11 -0
- udata/core/dataset/constants.py +0 -1
- udata/core/dataset/forms.py +29 -0
- udata/core/dataset/models.py +24 -42
- udata/core/dataset/rdf.py +2 -1
- udata/core/dataset/search.py +2 -2
- udata/core/dataset/tasks.py +86 -8
- udata/core/discussions/mails.py +63 -0
- udata/core/discussions/tasks.py +4 -18
- udata/core/metrics/__init__.py +0 -6
- udata/core/organization/api.py +20 -14
- udata/core/organization/mails.py +144 -0
- udata/core/organization/models.py +2 -1
- udata/core/organization/rdf.py +3 -3
- udata/core/organization/search.py +1 -1
- udata/core/organization/tasks.py +21 -49
- udata/core/pages/tests/test_api.py +0 -2
- udata/core/reuse/api.py +29 -3
- udata/core/reuse/mails.py +21 -0
- udata/core/reuse/models.py +10 -1
- udata/core/reuse/search.py +1 -1
- udata/core/reuse/tasks.py +2 -3
- udata/core/site/api.py +27 -19
- udata/core/site/models.py +2 -6
- udata/core/site/rdf.py +2 -2
- udata/core/spatial/tests/test_api.py +17 -20
- udata/core/spatial/tests/test_models.py +3 -3
- udata/core/user/mails.py +54 -0
- udata/core/user/models.py +2 -3
- udata/core/user/tasks.py +8 -23
- udata/core/user/tests/test_user_model.py +2 -6
- udata/entrypoints.py +0 -6
- udata/features/identicon/tests/test_backends.py +3 -13
- udata/forms/fields.py +3 -3
- udata/forms/widgets.py +2 -2
- udata/frontend/__init__.py +3 -32
- udata/harvest/actions.py +4 -9
- udata/harvest/api.py +5 -14
- udata/harvest/backends/__init__.py +20 -11
- udata/harvest/backends/base.py +2 -2
- udata/harvest/backends/ckan/harvesters.py +2 -1
- udata/harvest/backends/dcat.py +3 -0
- udata/harvest/backends/maaf.py +1 -0
- udata/harvest/commands.py +6 -4
- udata/harvest/forms.py +9 -6
- udata/harvest/tasks.py +3 -5
- udata/harvest/tests/ckan/test_ckan_backend.py +300 -337
- udata/harvest/tests/ckan/test_ckan_backend_errors.py +94 -99
- udata/harvest/tests/ckan/test_ckan_backend_filters.py +128 -122
- udata/harvest/tests/ckan/test_dkan_backend.py +39 -51
- udata/harvest/tests/dcat/bnodes.xml +17 -1
- udata/harvest/tests/dcat/datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml +255 -0
- udata/harvest/tests/dcat/datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml +289 -0
- udata/harvest/tests/factories.py +1 -1
- udata/harvest/tests/test_actions.py +11 -9
- udata/harvest/tests/test_api.py +4 -5
- udata/harvest/tests/test_base_backend.py +5 -4
- udata/harvest/tests/test_dcat_backend.py +72 -16
- udata/harvest/tests/test_models.py +2 -4
- udata/harvest/tests/test_notifications.py +2 -4
- udata/harvest/tests/test_tasks.py +2 -3
- udata/mail.py +90 -53
- udata/migrations/2025-01-05-dataservices-fields-changes.py +8 -14
- udata/migrations/2025-10-21-remove-ckan-harvest-modified-at.py +28 -0
- udata/migrations/2025-10-29-harvesters-sources-integrity.py +27 -0
- udata/models/__init__.py +0 -2
- udata/mongo/extras_fields.py +4 -3
- udata/mongo/taglist_field.py +3 -3
- udata/rdf.py +65 -20
- udata/sentry.py +3 -4
- udata/settings.py +15 -13
- udata/tags.py +5 -5
- udata/tasks.py +3 -3
- udata/templates/mail/message.html +65 -0
- udata/templates/mail/message.txt +16 -0
- udata/tests/__init__.py +40 -58
- udata/tests/api/__init__.py +87 -2
- udata/tests/api/test_activities_api.py +17 -23
- udata/tests/api/test_auth_api.py +2 -4
- udata/tests/api/test_contact_points.py +48 -54
- udata/tests/api/test_dataservices_api.py +65 -97
- udata/tests/api/test_datasets_api.py +171 -56
- udata/tests/api/test_me_api.py +4 -6
- udata/tests/api/test_organizations_api.py +19 -38
- udata/tests/api/test_reports_api.py +0 -4
- udata/tests/api/test_reuses_api.py +99 -23
- udata/tests/api/test_security_api.py +124 -0
- udata/tests/api/test_swagger.py +2 -3
- udata/tests/api/test_tags_api.py +6 -7
- udata/tests/api/test_transfer_api.py +0 -2
- udata/tests/api/test_user_api.py +8 -10
- udata/tests/apiv2/test_datasets.py +0 -4
- udata/tests/apiv2/test_me_api.py +0 -2
- udata/tests/apiv2/test_organizations.py +0 -2
- udata/tests/apiv2/test_swagger.py +2 -3
- udata/tests/apiv2/test_topics.py +0 -2
- udata/tests/cli/test_cli_base.py +14 -12
- udata/tests/cli/test_db_cli.py +51 -54
- udata/tests/contact_point/test_contact_point_models.py +2 -2
- udata/tests/dataservice/test_csv_adapter.py +2 -5
- udata/tests/dataservice/test_dataservice_rdf.py +64 -4
- udata/tests/dataservice/test_dataservice_tasks.py +36 -38
- udata/tests/dataset/test_csv_adapter.py +2 -5
- udata/tests/dataset/test_dataset_actions.py +2 -4
- udata/tests/dataset/test_dataset_commands.py +2 -4
- udata/tests/dataset/test_dataset_events.py +3 -3
- udata/tests/dataset/test_dataset_model.py +6 -7
- udata/tests/dataset/test_dataset_rdf.py +205 -16
- udata/tests/dataset/test_dataset_recommendations.py +2 -2
- udata/tests/dataset/test_dataset_tasks.py +66 -68
- udata/tests/dataset/test_resource_preview.py +39 -48
- udata/tests/dataset/test_transport_tasks.py +2 -2
- udata/tests/features/territories/__init__.py +0 -6
- udata/tests/features/territories/test_territories_api.py +25 -24
- udata/tests/forms/test_current_user_field.py +2 -2
- udata/tests/forms/test_dict_field.py +2 -4
- udata/tests/forms/test_extras_fields.py +2 -3
- udata/tests/forms/test_image_field.py +2 -2
- udata/tests/forms/test_model_field.py +2 -4
- udata/tests/forms/test_publish_as_field.py +2 -4
- udata/tests/forms/test_user_forms.py +26 -29
- udata/tests/frontend/test_auth.py +2 -3
- udata/tests/frontend/test_csv.py +5 -6
- udata/tests/frontend/test_error_handlers.py +2 -3
- udata/tests/frontend/test_hooks.py +5 -7
- udata/tests/frontend/test_markdown.py +3 -4
- udata/tests/helpers.py +2 -7
- udata/tests/metrics/test_metrics.py +52 -48
- udata/tests/metrics/test_tasks.py +154 -150
- udata/tests/organization/test_csv_adapter.py +2 -5
- udata/tests/organization/test_notifications.py +2 -4
- udata/tests/organization/test_organization_model.py +3 -4
- udata/tests/organization/test_organization_rdf.py +6 -12
- udata/tests/plugin.py +6 -110
- udata/tests/reuse/test_reuse_model.py +3 -4
- udata/tests/site/test_site_api.py +0 -2
- udata/tests/site/test_site_csv_exports.py +0 -2
- udata/tests/site/test_site_metrics.py +2 -4
- udata/tests/site/test_site_model.py +2 -2
- udata/tests/site/test_site_rdf.py +85 -29
- udata/tests/test_activity.py +3 -3
- udata/tests/test_api_fields.py +6 -9
- udata/tests/test_cors.py +0 -2
- udata/tests/test_dcat_commands.py +2 -3
- udata/tests/test_discussions.py +2 -7
- udata/tests/test_mail.py +150 -114
- udata/tests/test_migrations.py +413 -419
- udata/tests/test_model.py +10 -11
- udata/tests/test_notifications.py +2 -3
- udata/tests/test_owned.py +3 -3
- udata/tests/test_rdf.py +19 -15
- udata/tests/test_routing.py +5 -5
- udata/tests/test_storages.py +6 -5
- udata/tests/test_tags.py +2 -4
- udata/tests/test_topics.py +2 -4
- udata/tests/test_transfer.py +4 -5
- udata/tests/topic/test_topic_tasks.py +25 -27
- udata/tests/user/test_user_rdf.py +2 -8
- udata/tests/user/test_user_tasks.py +3 -5
- udata/tests/workers/test_jobs_commands.py +2 -2
- udata/tests/workers/test_tasks_routing.py +27 -27
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +369 -435
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +371 -437
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +369 -435
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +381 -447
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +371 -437
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +371 -437
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +372 -438
- udata/translations/udata.pot +379 -440
- udata/utils.py +66 -4
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/METADATA +1 -4
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/RECORD +212 -256
- udata/linkchecker/__init__.py +0 -0
- udata/linkchecker/backends.py +0 -31
- udata/linkchecker/checker.py +0 -75
- udata/linkchecker/commands.py +0 -21
- udata/linkchecker/models.py +0 -9
- udata/linkchecker/tasks.py +0 -55
- udata/templates/mail/account_deleted.html +0 -5
- udata/templates/mail/account_deleted.txt +0 -6
- udata/templates/mail/account_inactivity.html +0 -40
- udata/templates/mail/account_inactivity.txt +0 -31
- udata/templates/mail/badge_added_association.html +0 -33
- udata/templates/mail/badge_added_association.txt +0 -11
- udata/templates/mail/badge_added_certified.html +0 -33
- udata/templates/mail/badge_added_certified.txt +0 -11
- udata/templates/mail/badge_added_company.html +0 -33
- udata/templates/mail/badge_added_company.txt +0 -11
- udata/templates/mail/badge_added_local_authority.html +0 -33
- udata/templates/mail/badge_added_local_authority.txt +0 -11
- udata/templates/mail/badge_added_public_service.html +0 -33
- udata/templates/mail/badge_added_public_service.txt +0 -11
- udata/templates/mail/discussion_closed.html +0 -47
- udata/templates/mail/discussion_closed.txt +0 -16
- udata/templates/mail/inactive_account_deleted.html +0 -5
- udata/templates/mail/inactive_account_deleted.txt +0 -6
- udata/templates/mail/membership_refused.html +0 -20
- udata/templates/mail/membership_refused.txt +0 -11
- udata/templates/mail/membership_request.html +0 -46
- udata/templates/mail/membership_request.txt +0 -12
- udata/templates/mail/new_discussion.html +0 -44
- udata/templates/mail/new_discussion.txt +0 -15
- udata/templates/mail/new_discussion_comment.html +0 -45
- udata/templates/mail/new_discussion_comment.txt +0 -16
- udata/templates/mail/new_member.html +0 -27
- udata/templates/mail/new_member.txt +0 -11
- udata/templates/mail/new_reuse.html +0 -37
- udata/templates/mail/new_reuse.txt +0 -9
- udata/templates/mail/test.html +0 -6
- udata/templates/mail/test.txt +0 -6
- udata/templates/mail/user_mail_card.html +0 -26
- udata/templates/security/email/base.html +0 -105
- udata/templates/security/email/base.txt +0 -6
- udata/templates/security/email/button.html +0 -3
- udata/templates/security/email/change_notice.html +0 -22
- udata/templates/security/email/change_notice.txt +0 -8
- udata/templates/security/email/confirmation_instructions.html +0 -20
- udata/templates/security/email/confirmation_instructions.txt +0 -7
- udata/templates/security/email/login_instructions.html +0 -19
- udata/templates/security/email/login_instructions.txt +0 -7
- udata/templates/security/email/reset_instructions.html +0 -24
- udata/templates/security/email/reset_instructions.txt +0 -9
- udata/templates/security/email/reset_notice.html +0 -11
- udata/templates/security/email/reset_notice.txt +0 -4
- udata/templates/security/email/welcome.html +0 -24
- udata/templates/security/email/welcome.txt +0 -9
- udata/templates/security/email/welcome_existing.html +0 -32
- udata/templates/security/email/welcome_existing.txt +0 -14
- udata/terms.md +0 -6
- udata/tests/frontend/__init__.py +0 -23
- udata/tests/metrics/conftest.py +0 -15
- udata/tests/test_linkchecker.py +0 -277
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/WHEEL +0 -0
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/entry_points.txt +0 -0
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/licenses/LICENSE +0 -0
- {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/top_level.txt +0 -0
udata/core/dataset/api_fields.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from udata.api import api, base_reference, fields
|
|
2
|
+
from udata.core.access_type.models import AccessAudience
|
|
2
3
|
from udata.core.badges.fields import badge_fields
|
|
3
4
|
from udata.core.contact_point.api_fields import contact_point_fields
|
|
4
5
|
from udata.core.organization.api_fields import org_ref_fields
|
|
@@ -287,6 +288,11 @@ DEFAULT_MASK = ",".join(
|
|
|
287
288
|
"temporal_coverage",
|
|
288
289
|
"spatial",
|
|
289
290
|
"license",
|
|
291
|
+
"access_type",
|
|
292
|
+
"access_audiences",
|
|
293
|
+
"authorization_request_url",
|
|
294
|
+
"access_type_reason_category",
|
|
295
|
+
"access_type_reason",
|
|
290
296
|
"uri",
|
|
291
297
|
"page",
|
|
292
298
|
"last_update",
|
|
@@ -394,6 +400,11 @@ dataset_fields = api.model(
|
|
|
394
400
|
"license": fields.String(
|
|
395
401
|
attribute="license.id", default=DEFAULT_LICENSE["id"], description="The dataset license"
|
|
396
402
|
),
|
|
403
|
+
"access_type": fields.String(allow_null=True),
|
|
404
|
+
"access_audiences": fields.List(fields.Nested(AccessAudience.__read_fields__)),
|
|
405
|
+
"authorization_request_url": fields.String(allow_null=True),
|
|
406
|
+
"access_type_reason_category": fields.String(allow_null=True),
|
|
407
|
+
"access_type_reason": fields.String(allow_null=True),
|
|
397
408
|
"uri": fields.String(
|
|
398
409
|
attribute=lambda d: d.self_api_url(),
|
|
399
410
|
description="The API URI for this dataset",
|
udata/core/dataset/apiv2.py
CHANGED
|
@@ -7,6 +7,7 @@ from flask_restx import marshal
|
|
|
7
7
|
|
|
8
8
|
from udata import search
|
|
9
9
|
from udata.api import API, apiv2, fields
|
|
10
|
+
from udata.core.access_type.models import AccessAudience
|
|
10
11
|
from udata.core.contact_point.api_fields import contact_point_fields
|
|
11
12
|
from udata.core.dataset.api_fields import license_fields
|
|
12
13
|
from udata.core.organization.api_fields import member_user_with_email_fields
|
|
@@ -62,6 +63,11 @@ DEFAULT_MASK_APIV2 = ",".join(
|
|
|
62
63
|
"temporal_coverage",
|
|
63
64
|
"spatial",
|
|
64
65
|
"license",
|
|
66
|
+
"access_type",
|
|
67
|
+
"access_audiences",
|
|
68
|
+
"access_type_reason_category",
|
|
69
|
+
"access_type_reason",
|
|
70
|
+
"authorization_request_url",
|
|
65
71
|
"uri",
|
|
66
72
|
"page",
|
|
67
73
|
"last_update",
|
|
@@ -202,6 +208,11 @@ dataset_fields = apiv2.model(
|
|
|
202
208
|
default=DEFAULT_LICENSE["id"],
|
|
203
209
|
description="The dataset license (full License object if `X-Get-Datasets-Full-Objects` is set, ID of the license otherwise)",
|
|
204
210
|
),
|
|
211
|
+
"access_type": fields.String(allow_null=True),
|
|
212
|
+
"access_audiences": fields.Nested(AccessAudience.__read_fields__),
|
|
213
|
+
"authorization_request_url": fields.String(allow_null=True),
|
|
214
|
+
"access_type_reason_category": fields.String(allow_null=True),
|
|
215
|
+
"access_type_reason": fields.String(allow_null=True),
|
|
205
216
|
"uri": fields.String(
|
|
206
217
|
attribute=lambda d: d.self_api_url(),
|
|
207
218
|
description="The API URI for this dataset",
|
udata/core/dataset/constants.py
CHANGED
udata/core/dataset/forms.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
from udata.core.access_type.constants import (
|
|
2
|
+
AccessAudienceCondition,
|
|
3
|
+
AccessAudienceType,
|
|
4
|
+
AccessType,
|
|
5
|
+
InspireLimitationCategory,
|
|
6
|
+
)
|
|
7
|
+
from udata.core.access_type.models import AccessAudience
|
|
1
8
|
from udata.core.spatial.forms import SpatialCoverageField
|
|
2
9
|
from udata.core.storages import resources
|
|
3
10
|
from udata.forms import ModelForm, fields, validators
|
|
@@ -116,6 +123,8 @@ class CommunityResourceForm(BaseResourceForm):
|
|
|
116
123
|
|
|
117
124
|
|
|
118
125
|
def unmarshal_frequency(form, field):
|
|
126
|
+
if field.data is None:
|
|
127
|
+
return
|
|
119
128
|
# We don't need to worry about invalid field.data being fed to UpdateFrequency here,
|
|
120
129
|
# since the API will already have ensured incoming data matches the field definition,
|
|
121
130
|
# which in our case is an enum of valid UpdateFrequency values.
|
|
@@ -139,6 +148,13 @@ def validate_contact_point(form, field):
|
|
|
139
148
|
)
|
|
140
149
|
|
|
141
150
|
|
|
151
|
+
class AccessAudienceForm(ModelForm):
|
|
152
|
+
model_class = AccessAudience
|
|
153
|
+
|
|
154
|
+
role = fields.SelectField(choices=[(e.value, e.value) for e in AccessAudienceType])
|
|
155
|
+
condition = fields.SelectField(choices=[(e.value, e.value) for e in AccessAudienceCondition])
|
|
156
|
+
|
|
157
|
+
|
|
142
158
|
class DatasetForm(ModelForm):
|
|
143
159
|
model_class = Dataset
|
|
144
160
|
|
|
@@ -157,6 +173,19 @@ class DatasetForm(ModelForm):
|
|
|
157
173
|
description=_("A short description of the dataset."),
|
|
158
174
|
)
|
|
159
175
|
license = fields.ModelSelectField(_("License"), model=License, allow_blank=True)
|
|
176
|
+
access_type = fields.SelectField(
|
|
177
|
+
choices=[(e.value, e.value) for e in AccessType],
|
|
178
|
+
default=AccessType.OPEN,
|
|
179
|
+
validators=[validators.optional()],
|
|
180
|
+
)
|
|
181
|
+
access_audiences = fields.NestedModelList(AccessAudienceForm)
|
|
182
|
+
authorization_request_url = fields.StringField(_("Authorization request URL"))
|
|
183
|
+
access_type_reason_category = fields.SelectField(
|
|
184
|
+
_("Access type reason category"),
|
|
185
|
+
choices=[(e.value, e.label) for e in InspireLimitationCategory],
|
|
186
|
+
validators=[validators.optional()],
|
|
187
|
+
)
|
|
188
|
+
access_type_reason = fields.StringField(_("Access type reason"))
|
|
160
189
|
frequency = fields.SelectField(
|
|
161
190
|
_("Update frequency"),
|
|
162
191
|
choices=list(UpdateFrequency),
|
udata/core/dataset/models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime
|
|
4
4
|
from pydoc import locate
|
|
5
5
|
from typing import Self
|
|
6
6
|
from urllib.parse import urlparse
|
|
@@ -8,8 +8,8 @@ from urllib.parse import urlparse
|
|
|
8
8
|
import Levenshtein
|
|
9
9
|
import requests
|
|
10
10
|
from blinker import signal
|
|
11
|
-
from dateutil.parser import parse as parse_dt
|
|
12
11
|
from flask import current_app, url_for
|
|
12
|
+
from flask_babel import LazyString
|
|
13
13
|
from mongoengine import ValidationError as MongoEngineValidationError
|
|
14
14
|
from mongoengine.fields import DateTimeField
|
|
15
15
|
from mongoengine.signals import post_save, pre_init, pre_save
|
|
@@ -18,7 +18,10 @@ from werkzeug.utils import cached_property
|
|
|
18
18
|
from udata.api_fields import field
|
|
19
19
|
from udata.app import cache
|
|
20
20
|
from udata.core import storages
|
|
21
|
+
from udata.core.access_type.constants import AccessType
|
|
22
|
+
from udata.core.access_type.models import WithAccessType, check_only_one_condition_per_role
|
|
21
23
|
from udata.core.activity.models import Auditable
|
|
24
|
+
from udata.core.constants import HVD
|
|
22
25
|
from udata.core.dataset.preview import TabularAPIPreview
|
|
23
26
|
from udata.core.linkable import Linkable
|
|
24
27
|
from udata.core.metrics.helpers import get_stock_metrics
|
|
@@ -36,7 +39,6 @@ from .constants import (
|
|
|
36
39
|
CLOSED_FORMATS,
|
|
37
40
|
DEFAULT_LICENSE,
|
|
38
41
|
DESCRIPTION_SHORT_SIZE_LIMIT,
|
|
39
|
-
HVD,
|
|
40
42
|
INSPIRE,
|
|
41
43
|
MAX_DISTANCE,
|
|
42
44
|
PIVOTAL_DATA,
|
|
@@ -63,7 +65,7 @@ __all__ = (
|
|
|
63
65
|
"ResourceSchema",
|
|
64
66
|
)
|
|
65
67
|
|
|
66
|
-
BADGES: dict[str,
|
|
68
|
+
BADGES: dict[str, LazyString] = {
|
|
67
69
|
PIVOTAL_DATA: _("Pivotal data"),
|
|
68
70
|
SPD: _("Reference data public service"),
|
|
69
71
|
INSPIRE: _("Inspire"),
|
|
@@ -369,7 +371,13 @@ class ResourceMixin(object):
|
|
|
369
371
|
mime = db.StringField()
|
|
370
372
|
filesize = db.IntField() # `size` is a reserved keyword for mongoengine.
|
|
371
373
|
fs_filename = db.StringField()
|
|
372
|
-
extras = db.ExtrasField(
|
|
374
|
+
extras = db.ExtrasField(
|
|
375
|
+
{
|
|
376
|
+
"check:available": db.BooleanField,
|
|
377
|
+
"check:status": db.IntField,
|
|
378
|
+
"check:date": db.DateTimeField,
|
|
379
|
+
}
|
|
380
|
+
)
|
|
373
381
|
harvest = db.EmbeddedDocumentField(HarvestResourceMetadata)
|
|
374
382
|
schema = db.EmbeddedDocumentField(Schema)
|
|
375
383
|
|
|
@@ -428,41 +436,6 @@ class ResourceMixin(object):
|
|
|
428
436
|
"""
|
|
429
437
|
return self.extras.get("check:available", "unknown")
|
|
430
438
|
|
|
431
|
-
def need_check(self):
|
|
432
|
-
"""Does the resource needs to be checked against its linkchecker?
|
|
433
|
-
|
|
434
|
-
We check unavailable resources often, unless they go over the
|
|
435
|
-
threshold. Available resources are checked less and less frequently
|
|
436
|
-
based on their historical availability.
|
|
437
|
-
"""
|
|
438
|
-
min_cache_duration, max_cache_duration, ko_threshold = [
|
|
439
|
-
current_app.config.get(k)
|
|
440
|
-
for k in (
|
|
441
|
-
"LINKCHECKING_MIN_CACHE_DURATION",
|
|
442
|
-
"LINKCHECKING_MAX_CACHE_DURATION",
|
|
443
|
-
"LINKCHECKING_UNAVAILABLE_THRESHOLD",
|
|
444
|
-
)
|
|
445
|
-
]
|
|
446
|
-
count_availability = self.extras.get("check:count-availability", 1)
|
|
447
|
-
is_available = self.check_availability()
|
|
448
|
-
if is_available == "unknown":
|
|
449
|
-
return True
|
|
450
|
-
elif is_available or count_availability > ko_threshold:
|
|
451
|
-
delta = min(min_cache_duration * count_availability, max_cache_duration)
|
|
452
|
-
else:
|
|
453
|
-
delta = min_cache_duration
|
|
454
|
-
if self.extras.get("check:date"):
|
|
455
|
-
limit_date = datetime.utcnow() - timedelta(minutes=delta)
|
|
456
|
-
check_date = self.extras["check:date"]
|
|
457
|
-
if not isinstance(check_date, datetime):
|
|
458
|
-
try:
|
|
459
|
-
check_date = parse_dt(check_date)
|
|
460
|
-
except (ValueError, TypeError):
|
|
461
|
-
return True
|
|
462
|
-
if check_date >= limit_date:
|
|
463
|
-
return False
|
|
464
|
-
return True
|
|
465
|
-
|
|
466
439
|
@property
|
|
467
440
|
def latest(self):
|
|
468
441
|
"""
|
|
@@ -560,7 +533,9 @@ class DatasetBadgeMixin(BadgeMixin):
|
|
|
560
533
|
__badges__ = BADGES
|
|
561
534
|
|
|
562
535
|
|
|
563
|
-
class Dataset(
|
|
536
|
+
class Dataset(
|
|
537
|
+
Auditable, WithMetrics, WithAccessType, DatasetBadgeMixin, Owned, Linkable, db.Document
|
|
538
|
+
):
|
|
564
539
|
title = field(db.StringField(required=True))
|
|
565
540
|
acronym = field(db.StringField(max_length=128))
|
|
566
541
|
# /!\ do not set directly the slug when creating or updating a dataset
|
|
@@ -711,6 +686,10 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
|
|
|
711
686
|
|
|
712
687
|
self.quality_cached = self.compute_quality()
|
|
713
688
|
|
|
689
|
+
check_only_one_condition_per_role(self.access_audiences)
|
|
690
|
+
if self.access_type and self.access_type != AccessType.OPEN:
|
|
691
|
+
self.license = None
|
|
692
|
+
|
|
714
693
|
for key, value in self.extras.items():
|
|
715
694
|
if not key.startswith("custom:"):
|
|
716
695
|
continue
|
|
@@ -810,10 +789,13 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
|
|
|
810
789
|
|
|
811
790
|
def compute_last_update(self):
|
|
812
791
|
"""
|
|
813
|
-
|
|
792
|
+
If dataset is harvested and its metadata contains a modified_at date, use it.
|
|
793
|
+
Else, use the more recent date we would have at the resource level (harvest, modified).
|
|
814
794
|
Default to dataset last_modified if no resource.
|
|
815
795
|
Resources should be fetched when calling this method.
|
|
816
796
|
"""
|
|
797
|
+
if self.harvest and self.harvest.modified_at:
|
|
798
|
+
return self.harvest.modified_at
|
|
817
799
|
if self.resources:
|
|
818
800
|
return max([res.last_modified for res in self.resources])
|
|
819
801
|
else:
|
udata/core/dataset/rdf.py
CHANGED
|
@@ -16,6 +16,7 @@ from rdflib.namespace import RDF
|
|
|
16
16
|
from rdflib.resource import Resource as RdfResource
|
|
17
17
|
|
|
18
18
|
from udata import i18n, uris
|
|
19
|
+
from udata.core.constants import HVD
|
|
19
20
|
from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
|
|
20
21
|
from udata.core.spatial.models import SpatialCoverage
|
|
21
22
|
from udata.harvest.exceptions import HarvestSkipException
|
|
@@ -330,7 +331,7 @@ def dataset_to_rdf(dataset: Dataset, graph: Graph | None = None) -> RdfResource:
|
|
|
330
331
|
|
|
331
332
|
# Add DCAT-AP HVD properties if the dataset is tagged hvd.
|
|
332
333
|
# See https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/
|
|
333
|
-
is_hvd = current_app.config["HVD_SUPPORT"] and
|
|
334
|
+
is_hvd = current_app.config["HVD_SUPPORT"] and any(b.kind == HVD for b in dataset.badges)
|
|
334
335
|
if is_hvd:
|
|
335
336
|
d.add(DCATAP.applicableLegislation, URIRef(HVD_LEGISLATION))
|
|
336
337
|
|
udata/core/dataset/search.py
CHANGED
|
@@ -54,8 +54,8 @@ class DatasetSearch(ModelSearchAdapter):
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
@classmethod
|
|
57
|
-
def is_indexable(cls, dataset):
|
|
58
|
-
return dataset.
|
|
57
|
+
def is_indexable(cls, dataset: Dataset):
|
|
58
|
+
return dataset.is_visible
|
|
59
59
|
|
|
60
60
|
@classmethod
|
|
61
61
|
def mongo_search(cls, args):
|
udata/core/dataset/tasks.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
import os
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import date, datetime
|
|
4
4
|
from tempfile import NamedTemporaryFile
|
|
5
5
|
|
|
6
6
|
from celery.utils.log import get_task_logger
|
|
@@ -9,9 +9,15 @@ from mongoengine import ValidationError
|
|
|
9
9
|
|
|
10
10
|
from udata import models as udata_models
|
|
11
11
|
from udata.core import csv, storages
|
|
12
|
+
from udata.core.badges import tasks as badge_tasks
|
|
13
|
+
from udata.core.constants import HVD
|
|
12
14
|
from udata.core.dataservices.models import Dataservice
|
|
15
|
+
from udata.core.dataset.constants import INSPIRE
|
|
16
|
+
from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
|
|
17
|
+
from udata.core.organization.models import Organization
|
|
13
18
|
from udata.harvest.models import HarvestJob
|
|
14
19
|
from udata.models import Activity, Discussion, Follow, TopicElement, Transfer, db
|
|
20
|
+
from udata.storage.s3 import store_bytes
|
|
15
21
|
from udata.tasks import job
|
|
16
22
|
|
|
17
23
|
from .models import Checksum, CommunityResource, Dataset, Resource
|
|
@@ -85,12 +91,14 @@ def get_queryset(model_cls):
|
|
|
85
91
|
return model_cls.objects.filter(**params).no_cache()
|
|
86
92
|
|
|
87
93
|
|
|
94
|
+
def get_resource_for_csv_export_model(model, dataset):
|
|
95
|
+
for resource in dataset.resources:
|
|
96
|
+
if resource.extras.get("csv-export:model", "") == model:
|
|
97
|
+
return resource
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
def get_or_create_resource(r_info, model, dataset):
|
|
89
|
-
resource =
|
|
90
|
-
for r in dataset.resources:
|
|
91
|
-
if r.extras.get("csv-export:model", "") == model:
|
|
92
|
-
resource = r
|
|
93
|
-
break
|
|
101
|
+
resource = get_resource_for_csv_export_model(model, dataset)
|
|
94
102
|
if resource:
|
|
95
103
|
for k, v in r_info.items():
|
|
96
104
|
setattr(resource, k, v)
|
|
@@ -121,11 +129,16 @@ def store_resource(csvfile, model, dataset):
|
|
|
121
129
|
return get_or_create_resource(r_info, model, dataset)
|
|
122
130
|
|
|
123
131
|
|
|
124
|
-
def export_csv_for_model(model, dataset):
|
|
132
|
+
def export_csv_for_model(model, dataset, replace: bool = False):
|
|
125
133
|
model_cls = getattr(udata_models, model.capitalize(), None)
|
|
126
134
|
if not model_cls:
|
|
127
135
|
log.error("Unknow model %s" % model)
|
|
128
136
|
return
|
|
137
|
+
|
|
138
|
+
fs_filename_to_remove = None
|
|
139
|
+
if existing_resource := get_resource_for_csv_export_model(model, dataset):
|
|
140
|
+
fs_filename_to_remove = existing_resource.fs_filename
|
|
141
|
+
|
|
129
142
|
queryset = get_queryset(model_cls)
|
|
130
143
|
adapter = csv.get_adapter(model_cls)
|
|
131
144
|
if not adapter:
|
|
@@ -151,6 +164,10 @@ def export_csv_for_model(model, dataset):
|
|
|
151
164
|
else:
|
|
152
165
|
dataset.last_modified_internal = datetime.utcnow()
|
|
153
166
|
dataset.save()
|
|
167
|
+
# remove previous catalog if exists and replace is True
|
|
168
|
+
if replace and fs_filename_to_remove:
|
|
169
|
+
storages.resources.delete(fs_filename_to_remove)
|
|
170
|
+
return resource
|
|
154
171
|
finally:
|
|
155
172
|
csvfile.close()
|
|
156
173
|
os.unlink(csvfile.name)
|
|
@@ -179,7 +196,23 @@ def export_csv(self, model=None):
|
|
|
179
196
|
|
|
180
197
|
models = (model,) if model else ALLOWED_MODELS
|
|
181
198
|
for model in models:
|
|
182
|
-
export_csv_for_model(model, dataset)
|
|
199
|
+
resource = export_csv_for_model(model, dataset, replace=True)
|
|
200
|
+
|
|
201
|
+
# If we are the first day of the month, archive today catalogs
|
|
202
|
+
if (
|
|
203
|
+
current_app.config["EXPORT_CSV_ARCHIVE_S3_BUCKET"]
|
|
204
|
+
and resource
|
|
205
|
+
and date.today().day == 1
|
|
206
|
+
):
|
|
207
|
+
log.info(
|
|
208
|
+
f"Archiving {model} csv catalog on {current_app.config['EXPORT_CSV_ARCHIVE_S3_BUCKET']} bucket"
|
|
209
|
+
)
|
|
210
|
+
with storages.resources.open(resource.fs_filename, "rb") as f:
|
|
211
|
+
store_bytes(
|
|
212
|
+
bucket=current_app.config["EXPORT_CSV_ARCHIVE_S3_BUCKET"],
|
|
213
|
+
filename=f"{current_app.config['EXPORT_CSV_ARCHIVE_S3_FILENAME_PREFIX']}{resource.title}",
|
|
214
|
+
bytes=f.read(),
|
|
215
|
+
)
|
|
183
216
|
|
|
184
217
|
|
|
185
218
|
@job("bind-tabular-dataservice")
|
|
@@ -213,3 +246,48 @@ def bind_tabular_dataservice(self):
|
|
|
213
246
|
log.error(exc_info=e)
|
|
214
247
|
|
|
215
248
|
log.info(f"Bound {datasets.count()} datasets to TabularAPI dataservice")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@badge_tasks.register(model=Dataset, badge=HVD)
|
|
252
|
+
def update_dataset_hvd_badge() -> None:
|
|
253
|
+
"""
|
|
254
|
+
Update HVD badges to candidate datasets, based on the hvd tag.
|
|
255
|
+
Only datasets owned by certified and public service organizations are candidate to have a HVD badge.
|
|
256
|
+
"""
|
|
257
|
+
if not current_app.config["HVD_SUPPORT"]:
|
|
258
|
+
log.error("You need to set HVD_SUPPORT if you want to update dataset hvd badge")
|
|
259
|
+
return
|
|
260
|
+
public_certified_orgs = (
|
|
261
|
+
Organization.objects(badges__kind=PUBLIC_SERVICE).filter(badges__kind=CERTIFIED).only("id")
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
datasets = Dataset.objects(
|
|
265
|
+
tags="hvd", badges__kind__ne="hvd", organization__in=public_certified_orgs
|
|
266
|
+
)
|
|
267
|
+
log.info(f"Adding HVD badge to {datasets.count()} datasets")
|
|
268
|
+
for dataset in datasets:
|
|
269
|
+
dataset.add_badge(HVD)
|
|
270
|
+
|
|
271
|
+
datasets = Dataset.objects(tags__nin=["hvd"], badges__kind="hvd")
|
|
272
|
+
log.info(f"Removing HVD badge from {datasets.count()} datasets")
|
|
273
|
+
for dataset in datasets:
|
|
274
|
+
dataset.remove_badge(HVD)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@badge_tasks.register(model=Dataset, badge=INSPIRE)
|
|
278
|
+
def update_dataset_inspire_badge() -> None:
|
|
279
|
+
"""
|
|
280
|
+
Update INSPIRE badges to candidate datasets, based on the inspire tag.
|
|
281
|
+
"""
|
|
282
|
+
if not current_app.config["INSPIRE_SUPPORT"]:
|
|
283
|
+
log.error("You need to set INSPIRE_SUPPORT if you want to update dataset INSPIRE badge")
|
|
284
|
+
return
|
|
285
|
+
datasets = Dataset.objects(tags="inspire", badges__kind__ne="inspire")
|
|
286
|
+
log.info(f"Adding INSPIRE badge to {datasets.count()} datasets")
|
|
287
|
+
for dataset in datasets:
|
|
288
|
+
dataset.add_badge(INSPIRE)
|
|
289
|
+
|
|
290
|
+
datasets = Dataset.objects(tags__nin=["inspire"], badges__kind="inspire")
|
|
291
|
+
log.info(f"Removing INSPIRE badge from {datasets.count()} datasets")
|
|
292
|
+
for dataset in datasets:
|
|
293
|
+
dataset.remove_badge(INSPIRE)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from udata.core.discussions.models import Discussion, Message
|
|
2
|
+
from udata.i18n import lazy_gettext as _
|
|
3
|
+
from udata.mail import LabelledContent, MailCTA, MailMessage, ParagraphWithLinks
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def new_discussion(discussion: Discussion) -> MailMessage:
|
|
7
|
+
return MailMessage(
|
|
8
|
+
subject=_(
|
|
9
|
+
"A new discussion has been opened on your %(type)s",
|
|
10
|
+
type=discussion.subject.verbose_name,
|
|
11
|
+
),
|
|
12
|
+
paragraphs=[
|
|
13
|
+
ParagraphWithLinks(
|
|
14
|
+
_(
|
|
15
|
+
"You have a new discussion from %(user_or_org)s on your %(type)s %(object)s",
|
|
16
|
+
user_or_org=discussion.organization or discussion.user,
|
|
17
|
+
type=discussion.subject.verbose_name,
|
|
18
|
+
object=discussion.subject,
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
LabelledContent(_("Discussion title:"), discussion.title, inline=True),
|
|
22
|
+
LabelledContent(_("Comment:"), discussion.discussion[0].content),
|
|
23
|
+
MailCTA(_("Reply"), discussion.url_for()),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def new_discussion_comment(discussion: Discussion, comment: Message) -> MailMessage:
|
|
29
|
+
return MailMessage(
|
|
30
|
+
subject=_("A new comment has been added to a discussion"),
|
|
31
|
+
paragraphs=[
|
|
32
|
+
ParagraphWithLinks(
|
|
33
|
+
_(
|
|
34
|
+
"You have a new comment from %(user_or_org)s on your %(type)s %(object)s",
|
|
35
|
+
user_or_org=comment.posted_by_org_or_user,
|
|
36
|
+
type=discussion.subject.verbose_name,
|
|
37
|
+
object=discussion.subject,
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
LabelledContent(_("Discussion title:"), discussion.title, inline=True),
|
|
41
|
+
LabelledContent(_("Comment:"), comment.content),
|
|
42
|
+
MailCTA(_("Reply"), discussion.url_for()),
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def discussion_closed(discussion: Discussion, comment: Message | None) -> MailMessage:
|
|
48
|
+
return MailMessage(
|
|
49
|
+
subject=_("A discussion has been closed"),
|
|
50
|
+
paragraphs=[
|
|
51
|
+
ParagraphWithLinks(
|
|
52
|
+
_(
|
|
53
|
+
"The discussion you participated in on the %(type)s %(object)s has been closed by %(user_or_org)s.",
|
|
54
|
+
user_or_org=discussion.closed_by_org_or_user,
|
|
55
|
+
type=discussion.subject.verbose_name,
|
|
56
|
+
object=discussion.subject,
|
|
57
|
+
)
|
|
58
|
+
),
|
|
59
|
+
LabelledContent(_("Discussion title:"), discussion.title, inline=True),
|
|
60
|
+
LabelledContent(_("Comment:"), comment.content) if comment else None,
|
|
61
|
+
MailCTA(_("View the discussion"), discussion.url_for()),
|
|
62
|
+
],
|
|
63
|
+
)
|
udata/core/discussions/tasks.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from udata import mail
|
|
2
|
-
from udata.i18n import lazy_gettext as _
|
|
3
1
|
from udata.tasks import connect, get_logger
|
|
4
2
|
|
|
3
|
+
from . import mails
|
|
5
4
|
from .constants import NOTIFY_DISCUSSION_SUBJECTS
|
|
6
5
|
from .models import Discussion
|
|
7
6
|
from .signals import on_discussion_closed, on_new_discussion, on_new_discussion_comment
|
|
@@ -22,15 +21,7 @@ def owner_recipients(discussion):
|
|
|
22
21
|
def notify_new_discussion(discussion_id):
|
|
23
22
|
discussion = Discussion.objects.get(pk=discussion_id)
|
|
24
23
|
if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
|
|
25
|
-
|
|
26
|
-
subject = _("Your %(type)s have a new discussion", type=discussion.subject.verbose_name)
|
|
27
|
-
mail.send(
|
|
28
|
-
subject,
|
|
29
|
-
recipients,
|
|
30
|
-
"new_discussion",
|
|
31
|
-
discussion=discussion,
|
|
32
|
-
message=discussion.discussion[0],
|
|
33
|
-
)
|
|
24
|
+
mails.new_discussion(discussion).send(owner_recipients(discussion))
|
|
34
25
|
else:
|
|
35
26
|
log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
|
|
36
27
|
|
|
@@ -42,11 +33,7 @@ def notify_new_discussion_comment(discussion_id, message=None):
|
|
|
42
33
|
if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
|
|
43
34
|
recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
|
|
44
35
|
recipients = list({u.id: u for u in recipients if u != message.posted_by}.values())
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
mail.send(
|
|
48
|
-
subject, recipients, "new_discussion_comment", discussion=discussion, message=message
|
|
49
|
-
)
|
|
36
|
+
mails.new_discussion_comment(discussion, message).send(recipients)
|
|
50
37
|
else:
|
|
51
38
|
log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
|
|
52
39
|
|
|
@@ -58,7 +45,6 @@ def notify_discussion_closed(discussion_id, message=None):
|
|
|
58
45
|
if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
|
|
59
46
|
recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
|
|
60
47
|
recipients = list({u.id: u for u in recipients if u != discussion.closed_by}.values())
|
|
61
|
-
|
|
62
|
-
mail.send(subject, recipients, "discussion_closed", discussion=discussion, message=message)
|
|
48
|
+
mails.discussion_closed(discussion, message).send(recipients)
|
|
63
49
|
else:
|
|
64
50
|
log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
|
udata/core/metrics/__init__.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
from udata import entrypoints
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
def init_app(app):
|
|
5
2
|
# Load all core metrics
|
|
6
3
|
import udata.core.user.metrics # noqa
|
|
@@ -9,6 +6,3 @@ def init_app(app):
|
|
|
9
6
|
import udata.core.dataset.metrics # noqa
|
|
10
7
|
import udata.core.reuse.metrics # noqa
|
|
11
8
|
import udata.core.followers.metrics # noqa
|
|
12
|
-
|
|
13
|
-
# Load metrics from plugins
|
|
14
|
-
entrypoints.get_enabled("udata.metrics", app)
|
udata/core/organization/api.py
CHANGED
|
@@ -13,7 +13,7 @@ from udata.core.contact_point.api import ContactPointApiParser
|
|
|
13
13
|
from udata.core.contact_point.api_fields import contact_point_fields, contact_point_page_fields
|
|
14
14
|
from udata.core.dataservices.csv import DataserviceCsvAdapter
|
|
15
15
|
from udata.core.dataservices.models import Dataservice
|
|
16
|
-
from udata.core.dataset.api import DatasetApiParser
|
|
16
|
+
from udata.core.dataset.api import DatasetApiParser, catalog_parser
|
|
17
17
|
from udata.core.dataset.api_fields import dataset_page_fields
|
|
18
18
|
from udata.core.dataset.csv import DatasetCsvAdapter, ResourcesCsvAdapter
|
|
19
19
|
from udata.core.dataset.models import Dataset
|
|
@@ -29,7 +29,6 @@ from udata.core.storages.api import (
|
|
|
29
29
|
)
|
|
30
30
|
from udata.models import ContactPoint
|
|
31
31
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
32
|
-
from udata.utils import multi_to_dict
|
|
33
32
|
|
|
34
33
|
from .api_fields import (
|
|
35
34
|
member_fields,
|
|
@@ -50,7 +49,7 @@ from .forms import (
|
|
|
50
49
|
from .models import Member, MembershipRequest, Organization
|
|
51
50
|
from .permissions import EditOrganizationPermission, OrganizationPrivatePermission
|
|
52
51
|
from .rdf import build_org_catalog
|
|
53
|
-
from .tasks import notify_membership_request, notify_membership_response
|
|
52
|
+
from .tasks import notify_membership_request, notify_membership_response, notify_new_member
|
|
54
53
|
|
|
55
54
|
DEFAULT_SORTING = "-created_at"
|
|
56
55
|
SUGGEST_SORTING = "-metrics.followers"
|
|
@@ -234,32 +233,37 @@ class DatasetsResourcesCsvAPI(API):
|
|
|
234
233
|
class OrganizationRdfAPI(API):
|
|
235
234
|
@api.doc("rdf_organization")
|
|
236
235
|
def get(self, org):
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
_format = RDF_EXTENSIONS[negociate_content()]
|
|
237
|
+
# We sanitize the args used as kwargs in url_for
|
|
238
|
+
params = catalog_parser.parse_args()
|
|
239
|
+
url = url_for("api.organization_rdf_format", org=org.id, _format=_format, **params)
|
|
239
240
|
return redirect(url)
|
|
240
241
|
|
|
241
242
|
|
|
242
|
-
@ns.route("/<org:org>/catalog.<
|
|
243
|
+
@ns.route("/<org:org>/catalog.<_format>", endpoint="organization_rdf_format", doc=common_doc)
|
|
243
244
|
@api.response(404, "Organization not found")
|
|
244
245
|
@api.response(410, "Organization has been deleted")
|
|
245
246
|
class OrganizationRdfFormatAPI(API):
|
|
246
247
|
@api.doc("rdf_organization_format")
|
|
247
|
-
|
|
248
|
+
@api.expect(catalog_parser)
|
|
249
|
+
def get(self, org, _format):
|
|
248
250
|
if org.deleted:
|
|
249
251
|
api.abort(410)
|
|
250
|
-
params =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
params = catalog_parser.parse_args()
|
|
253
|
+
datasets = DatasetApiParser.parse_filters(
|
|
254
|
+
Dataset.objects(organization=org).visible(), params
|
|
255
|
+
)
|
|
256
|
+
datasets = datasets.paginate(params["page"], params["page_size"])
|
|
257
|
+
|
|
254
258
|
dataservices = (
|
|
255
259
|
Dataservice.objects(organization=org)
|
|
256
260
|
.visible()
|
|
257
|
-
.filter_by_dataset_pagination(datasets, page)
|
|
261
|
+
.filter_by_dataset_pagination(datasets, params["page"])
|
|
258
262
|
)
|
|
259
|
-
catalog = build_org_catalog(org, datasets, dataservices,
|
|
263
|
+
catalog = build_org_catalog(org, datasets, dataservices, _format=_format, **params)
|
|
260
264
|
# bypass flask-restplus make_response, since graph_response
|
|
261
265
|
# is handling the content negociation directly
|
|
262
|
-
return make_response(*graph_response(catalog,
|
|
266
|
+
return make_response(*graph_response(catalog, _format))
|
|
263
267
|
|
|
264
268
|
|
|
265
269
|
@ns.route("/badges/", endpoint="available_organization_badges")
|
|
@@ -468,6 +472,8 @@ class MemberAPI(API):
|
|
|
468
472
|
org.count_members()
|
|
469
473
|
org.save()
|
|
470
474
|
|
|
475
|
+
notify_new_member.delay(str(org.id), str(member.user.email))
|
|
476
|
+
|
|
471
477
|
return member, 201
|
|
472
478
|
|
|
473
479
|
@api.secure
|