udata 11.1.2.dev7__py3-none-any.whl → 11.1.2.dev11__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/oauth2.py +22 -3
- udata/app.py +3 -0
- udata/auth/__init__.py +11 -0
- udata/auth/forms.py +70 -3
- udata/auth/mails.py +6 -0
- udata/auth/proconnect.py +127 -0
- udata/auth/views.py +57 -2
- udata/commands/db.py +2 -3
- udata/core/__init__.py +2 -0
- udata/core/captchetat.py +80 -0
- udata/core/dataservices/api.py +1 -2
- udata/core/dataset/api.py +3 -4
- udata/core/dataset/api_fields.py +3 -4
- udata/core/dataset/apiv2.py +6 -6
- udata/core/dataset/commands.py +0 -10
- udata/core/dataset/constants.py +124 -38
- udata/core/dataset/factories.py +2 -1
- udata/core/dataset/forms.py +14 -10
- udata/core/dataset/models.py +8 -36
- udata/core/dataset/preview.py +3 -3
- udata/core/dataset/rdf.py +84 -65
- udata/core/dataset/tasks.py +2 -50
- udata/core/metrics/helpers.py +6 -7
- udata/core/metrics/tasks.py +3 -6
- udata/core/post/api.py +1 -2
- udata/core/reuse/api.py +1 -2
- udata/core/user/api.py +1 -3
- udata/cors.py +19 -2
- udata/harvest/backends/ckan/harvesters.py +10 -14
- udata/harvest/backends/maaf.py +15 -14
- udata/harvest/tests/ckan/test_ckan_backend.py +4 -3
- udata/harvest/tests/test_dcat_backend.py +3 -2
- udata/i18n.py +7 -32
- udata/migrations/2025-01-05-dataservices-fields-changes.py +1 -2
- udata/migrations/2025-09-04-update-legacy-frequencies.py +36 -0
- udata/settings.py +27 -0
- udata/templates/security/email/reset_instructions.html +1 -1
- udata/templates/security/email/reset_instructions.txt +1 -1
- udata/tests/api/test_datasets_api.py +41 -12
- udata/tests/dataset/test_dataset_model.py +17 -53
- udata/tests/dataset/test_dataset_rdf.py +27 -28
- udata/translations/udata.pot +226 -150
- udata/uris.py +1 -2
- udata/utils.py +8 -1
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/METADATA +3 -4
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/RECORD +50 -50
- udata/templates/mail/frequency_reminder.html +0 -34
- udata/templates/mail/frequency_reminder.txt +0 -18
- udata/tests/test_i18n.py +0 -93
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/WHEEL +0 -0
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/entry_points.txt +0 -0
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/licenses/LICENSE +0 -0
- {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/top_level.txt +0 -0
udata/core/dataset/tasks.py
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
import os
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime
|
|
4
4
|
from tempfile import NamedTemporaryFile
|
|
5
5
|
|
|
6
6
|
from celery.utils.log import get_task_logger
|
|
7
7
|
from flask import current_app
|
|
8
8
|
from mongoengine import ValidationError
|
|
9
9
|
|
|
10
|
-
from udata import mail
|
|
11
10
|
from udata import models as udata_models
|
|
12
11
|
from udata.core import csv, storages
|
|
13
12
|
from udata.core.dataservices.models import Dataservice
|
|
14
13
|
from udata.harvest.models import HarvestJob
|
|
15
|
-
from udata.
|
|
16
|
-
from udata.models import Activity, Discussion, Follow, Organization, TopicElement, Transfer, db
|
|
14
|
+
from udata.models import Activity, Discussion, Follow, TopicElement, Transfer, db
|
|
17
15
|
from udata.tasks import job
|
|
18
16
|
|
|
19
|
-
from .constants import UPDATE_FREQUENCIES
|
|
20
17
|
from .models import Checksum, CommunityResource, Dataset, Resource
|
|
21
18
|
|
|
22
19
|
log = get_task_logger(__name__)
|
|
@@ -75,51 +72,6 @@ def purge_datasets(self):
|
|
|
75
72
|
dataset.delete()
|
|
76
73
|
|
|
77
74
|
|
|
78
|
-
@job("send-frequency-reminder")
|
|
79
|
-
def send_frequency_reminder(self):
|
|
80
|
-
# We exclude irrelevant frequencies.
|
|
81
|
-
frequencies = [
|
|
82
|
-
f
|
|
83
|
-
for f in UPDATE_FREQUENCIES.keys()
|
|
84
|
-
if f not in ("unknown", "realtime", "punctual", "irregular", "continuous")
|
|
85
|
-
]
|
|
86
|
-
now = datetime.utcnow()
|
|
87
|
-
reminded_orgs = {}
|
|
88
|
-
reminded_people = []
|
|
89
|
-
allowed_delay = current_app.config["DELAY_BEFORE_REMINDER_NOTIFICATION"]
|
|
90
|
-
for org in Organization.objects.visible():
|
|
91
|
-
outdated_datasets = []
|
|
92
|
-
for dataset in Dataset.objects.filter(
|
|
93
|
-
frequency__in=frequencies, organization=org
|
|
94
|
-
).visible():
|
|
95
|
-
if dataset.next_update + timedelta(days=allowed_delay) < now:
|
|
96
|
-
dataset.outdated = now - dataset.next_update
|
|
97
|
-
dataset.frequency_str = UPDATE_FREQUENCIES[dataset.frequency]
|
|
98
|
-
outdated_datasets.append(dataset)
|
|
99
|
-
if outdated_datasets:
|
|
100
|
-
reminded_orgs[org] = outdated_datasets
|
|
101
|
-
for reminded_org, datasets in reminded_orgs.items():
|
|
102
|
-
print(
|
|
103
|
-
"{org.name} will be emailed for {datasets_nb} datasets".format(
|
|
104
|
-
org=reminded_org, datasets_nb=len(datasets)
|
|
105
|
-
)
|
|
106
|
-
)
|
|
107
|
-
recipients = [m.user for m in reminded_org.members]
|
|
108
|
-
reminded_people.append(recipients)
|
|
109
|
-
subject = _("You need to update some frequency-based datasets")
|
|
110
|
-
mail.send(subject, recipients, "frequency_reminder", org=reminded_org, datasets=datasets)
|
|
111
|
-
|
|
112
|
-
print("{nb_orgs} orgs concerned".format(nb_orgs=len(reminded_orgs)))
|
|
113
|
-
reminded_people = list(flatten(reminded_people))
|
|
114
|
-
print(
|
|
115
|
-
"{nb_emails} people contacted ({nb_emails_twice} twice)".format(
|
|
116
|
-
nb_emails=len(reminded_people),
|
|
117
|
-
nb_emails_twice=len(reminded_people) - len(set(reminded_people)),
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
print("Done")
|
|
121
|
-
|
|
122
|
-
|
|
123
75
|
def get_queryset(model_cls):
|
|
124
76
|
# special case for resources
|
|
125
77
|
if model_cls.__name__ == "Resource":
|
udata/core/metrics/helpers.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections import OrderedDict
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
|
-
from typing import Dict, List, Union
|
|
5
4
|
from urllib.parse import urlencode
|
|
6
5
|
|
|
7
6
|
import requests
|
|
@@ -14,13 +13,13 @@ from pymongo.command_cursor import CommandCursor
|
|
|
14
13
|
log = logging.getLogger(__name__)
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
def get_last_13_months() ->
|
|
16
|
+
def get_last_13_months() -> list[str]:
|
|
18
17
|
dstart = datetime.today().replace(day=1) - timedelta(days=365)
|
|
19
18
|
months = rrule(freq=MONTHLY, count=13, dtstart=dstart)
|
|
20
19
|
return [month.strftime("%Y-%m") for month in months]
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
def compute_monthly_metrics(metrics_data:
|
|
22
|
+
def compute_monthly_metrics(metrics_data: list[dict], metrics_labels: list[str]) -> OrderedDict:
|
|
24
23
|
# Initialize default monthly_metrics
|
|
25
24
|
monthly_metrics = OrderedDict(
|
|
26
25
|
(month, {label: 0 for label in metrics_labels}) for month in get_last_13_months()
|
|
@@ -35,7 +34,7 @@ def compute_monthly_metrics(metrics_data: List[Dict], metrics_labels: List[str])
|
|
|
35
34
|
return monthly_metrics
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def metrics_by_label(monthly_metrics:
|
|
37
|
+
def metrics_by_label(monthly_metrics: dict, metrics_labels: list[str]) -> list[OrderedDict]:
|
|
39
38
|
metrics_by_label = []
|
|
40
39
|
for label in metrics_labels:
|
|
41
40
|
metrics_by_label.append(
|
|
@@ -45,8 +44,8 @@ def metrics_by_label(monthly_metrics: Dict, metrics_labels: List[str]) -> List[O
|
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
def get_metrics_for_model(
|
|
48
|
-
model: str, id:
|
|
49
|
-
) ->
|
|
47
|
+
model: str, id: str | ObjectId | None, metrics_labels: list[str]
|
|
48
|
+
) -> list[OrderedDict]:
|
|
50
49
|
"""
|
|
51
50
|
Get distant metrics for a particular model object
|
|
52
51
|
"""
|
|
@@ -69,7 +68,7 @@ def get_metrics_for_model(
|
|
|
69
68
|
return [{} for _ in range(len(metrics_labels))]
|
|
70
69
|
|
|
71
70
|
|
|
72
|
-
def get_download_url(model: str, id:
|
|
71
|
+
def get_download_url(model: str, id: str | ObjectId | None) -> str:
|
|
73
72
|
api_namespace = model + "s" if model != "site" else model
|
|
74
73
|
base_url = f"{current_app.config['METRICS_API']}/{api_namespace}/data/csv/"
|
|
75
74
|
args = {"metric_month__sort": "asc"}
|
udata/core/metrics/tasks.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
from functools import wraps
|
|
4
|
-
from typing import Dict, List
|
|
5
4
|
|
|
6
5
|
import requests
|
|
7
6
|
from flask import current_app
|
|
@@ -17,9 +16,7 @@ log = logging.getLogger(__name__)
|
|
|
17
16
|
def log_timing(func):
|
|
18
17
|
@wraps(func)
|
|
19
18
|
def timeit_wrapper(*args, **kwargs):
|
|
20
|
-
|
|
21
|
-
name = func.__name__
|
|
22
|
-
model = name.removeprefix("update_") if hasattr(name, "removeprefix") else name
|
|
19
|
+
model = func.__name__.removeprefix("update_")
|
|
23
20
|
|
|
24
21
|
log.info(f"Processing {model}…")
|
|
25
22
|
start_time = time.perf_counter()
|
|
@@ -31,7 +28,7 @@ def log_timing(func):
|
|
|
31
28
|
return timeit_wrapper
|
|
32
29
|
|
|
33
30
|
|
|
34
|
-
def save_model(model: db.Document, model_id: str, metrics:
|
|
31
|
+
def save_model(model: db.Document, model_id: str, metrics: dict[str, int]) -> None:
|
|
35
32
|
try:
|
|
36
33
|
result = model.objects(id=model_id).update(
|
|
37
34
|
**{f"set__metrics__{key}": value for key, value in metrics.items()}
|
|
@@ -43,7 +40,7 @@ def save_model(model: db.Document, model_id: str, metrics: Dict[str, int]) -> No
|
|
|
43
40
|
log.exception(e)
|
|
44
41
|
|
|
45
42
|
|
|
46
|
-
def iterate_on_metrics(target: str, value_keys:
|
|
43
|
+
def iterate_on_metrics(target: str, value_keys: list[str], page_size: int = 50) -> dict:
|
|
47
44
|
"""
|
|
48
45
|
Yield all elements with not zero values for the keys inside `value_keys`.
|
|
49
46
|
If you pass ['visit', 'download_resource'], it will do a `OR` and get
|
udata/core/post/api.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import List
|
|
3
2
|
|
|
4
3
|
from feedgenerator.django.utils.feedgenerator import Atom1Feed
|
|
5
4
|
from flask import make_response, request
|
|
@@ -122,7 +121,7 @@ class PostsAtomFeedAPI(API):
|
|
|
122
121
|
link=request.url_root,
|
|
123
122
|
)
|
|
124
123
|
|
|
125
|
-
posts:
|
|
124
|
+
posts: list[Post] = Post.objects().published().order_by("-published").limit(15)
|
|
126
125
|
for post in posts:
|
|
127
126
|
feed.add_item(
|
|
128
127
|
post.name,
|
udata/core/reuse/api.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import List
|
|
3
2
|
|
|
4
3
|
import mongoengine
|
|
5
4
|
from bson.objectid import ObjectId
|
|
@@ -144,7 +143,7 @@ class ReusesAtomFeedAPI(API):
|
|
|
144
143
|
link=request.url_root,
|
|
145
144
|
)
|
|
146
145
|
|
|
147
|
-
reuses:
|
|
146
|
+
reuses: list[Reuse] = Reuse.objects.visible().order_by("-created_at").limit(15)
|
|
148
147
|
for reuse in reuses:
|
|
149
148
|
author_name = None
|
|
150
149
|
author_uri = None
|
udata/core/user/api.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
from flask_security import current_user, logout_user
|
|
4
2
|
from slugify import slugify
|
|
5
3
|
|
|
@@ -370,7 +368,7 @@ suggest_parser.add_argument(
|
|
|
370
368
|
)
|
|
371
369
|
|
|
372
370
|
|
|
373
|
-
def suggest_size(value: str) ->
|
|
371
|
+
def suggest_size(value: str) -> int | None:
|
|
374
372
|
"""Parse an integer that must be between 1 and 20."""
|
|
375
373
|
help_message = "The size must be an integer between 1 and 20."
|
|
376
374
|
try:
|
udata/cors.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from flask import g, request
|
|
3
|
+
from flask import current_app, g, request
|
|
4
4
|
from werkzeug.datastructures import Headers
|
|
5
5
|
|
|
6
6
|
log = logging.getLogger(__name__)
|
|
@@ -36,10 +36,27 @@ def is_allowed_cors_route():
|
|
|
36
36
|
path: str = request.path.removeprefix(f"/{g.lang_code}")
|
|
37
37
|
else:
|
|
38
38
|
path: str = request.path
|
|
39
|
+
|
|
40
|
+
# Allow to keep clean CORS when `udata` and the frontend are on the same domain
|
|
41
|
+
# (as it's the case in data.gouv with cdata/udata).
|
|
42
|
+
if not current_app.config["SECURITY_SPA_ON_SAME_DOMAIN"] and (
|
|
43
|
+
path.startswith("/login")
|
|
44
|
+
or path.startswith("/logout")
|
|
45
|
+
or path.startswith("/reset")
|
|
46
|
+
or path.startswith("/register")
|
|
47
|
+
or path.startswith("/confirm")
|
|
48
|
+
or path.startswith("/change")
|
|
49
|
+
or path.startswith("/change-email")
|
|
50
|
+
or path.startswith("/oauth")
|
|
51
|
+
or path.startswith("/get-csrf")
|
|
52
|
+
):
|
|
53
|
+
return True
|
|
54
|
+
|
|
39
55
|
return (
|
|
40
56
|
path.endswith((".js", ".css", ".woff", ".woff2", ".png", ".jpg", ".jpeg", ".svg"))
|
|
41
57
|
or path.startswith("/api")
|
|
42
|
-
or path.startswith("/oauth")
|
|
58
|
+
or path.startswith("/oauth/token")
|
|
59
|
+
or path.startswith("/oauth/revoke")
|
|
43
60
|
or path.startswith("/datasets/r/")
|
|
44
61
|
)
|
|
45
62
|
|
|
@@ -4,19 +4,14 @@ from urllib.parse import urljoin
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
from udata import uris
|
|
7
|
-
from udata.
|
|
8
|
-
from udata.i18n import lazy_gettext as _
|
|
9
|
-
|
|
10
|
-
try:
|
|
11
|
-
from udata.core.dataset.constants import UPDATE_FREQUENCIES
|
|
12
|
-
except ImportError:
|
|
13
|
-
# legacy import of constants in udata
|
|
14
|
-
from udata.models import UPDATE_FREQUENCIES
|
|
7
|
+
from udata.core.dataset.constants import UpdateFrequency
|
|
15
8
|
from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
|
|
16
9
|
from udata.core.dataset.rdf import frequency_from_rdf
|
|
17
10
|
from udata.frontend.markdown import parse_html
|
|
18
11
|
from udata.harvest.backends.base import BaseBackend, HarvestFilter
|
|
19
12
|
from udata.harvest.exceptions import HarvestException, HarvestSkipException
|
|
13
|
+
from udata.harvest.models import HarvestItem
|
|
14
|
+
from udata.i18n import lazy_gettext as _
|
|
20
15
|
from udata.models import GeoZone, License, Resource, SpatialCoverage, db
|
|
21
16
|
from udata.utils import daterange_end, daterange_start, get_by
|
|
22
17
|
|
|
@@ -193,14 +188,15 @@ class CkanBackend(BaseBackend):
|
|
|
193
188
|
log.debug("spatial-uri value not handled: %s", value)
|
|
194
189
|
elif key == "frequency":
|
|
195
190
|
# Update frequency
|
|
196
|
-
freq
|
|
197
|
-
if freq:
|
|
191
|
+
if freq := frequency_from_rdf(value):
|
|
198
192
|
dataset.frequency = freq
|
|
199
|
-
elif value in UPDATE_FREQUENCIES:
|
|
200
|
-
dataset.frequency = value
|
|
201
193
|
else:
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
# FIXME(python 3.12+): prefer `if value in UpdateFrequency`
|
|
195
|
+
try:
|
|
196
|
+
dataset.frequency = UpdateFrequency(value)
|
|
197
|
+
except ValueError:
|
|
198
|
+
dataset.extras["ckan:frequency"] = value
|
|
199
|
+
log.debug("frequency value not handled: %s", value)
|
|
204
200
|
# Temporal coverage start
|
|
205
201
|
elif key == "temporal_start":
|
|
206
202
|
temporal_start = daterange_start(value)
|
udata/harvest/backends/maaf.py
CHANGED
|
@@ -7,6 +7,7 @@ from urllib.parse import urljoin
|
|
|
7
7
|
from lxml import etree, html
|
|
8
8
|
from voluptuous import All, Any, In, Length, Lower, Optional, Schema
|
|
9
9
|
|
|
10
|
+
from udata.core.dataset.constants import UpdateFrequency
|
|
10
11
|
from udata.harvest.backends import BaseBackend
|
|
11
12
|
from udata.harvest.filters import (
|
|
12
13
|
boolean,
|
|
@@ -38,19 +39,19 @@ ZONES = {
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
FREQUENCIES = {
|
|
41
|
-
"ponctuelle":
|
|
42
|
-
"temps réel":
|
|
43
|
-
"quotidienne":
|
|
44
|
-
"hebdomadaire":
|
|
45
|
-
"bimensuelle":
|
|
46
|
-
"mensuelle":
|
|
47
|
-
"bimestrielle":
|
|
48
|
-
"trimestrielle":
|
|
49
|
-
"semestrielle":
|
|
50
|
-
"annuelle":
|
|
51
|
-
"triennale":
|
|
52
|
-
"quinquennale":
|
|
53
|
-
"aucune":
|
|
42
|
+
"ponctuelle": UpdateFrequency.PUNCTUAL,
|
|
43
|
+
"temps réel": UpdateFrequency.CONTINUOUS,
|
|
44
|
+
"quotidienne": UpdateFrequency.DAILY,
|
|
45
|
+
"hebdomadaire": UpdateFrequency.WEEKLY,
|
|
46
|
+
"bimensuelle": UpdateFrequency.SEMIMONTHLY,
|
|
47
|
+
"mensuelle": UpdateFrequency.MONTHLY,
|
|
48
|
+
"bimestrielle": UpdateFrequency.BIMONTHLY,
|
|
49
|
+
"trimestrielle": UpdateFrequency.QUARTERLY,
|
|
50
|
+
"semestrielle": UpdateFrequency.SEMIANNUAL,
|
|
51
|
+
"annuelle": UpdateFrequency.ANNUAL,
|
|
52
|
+
"triennale": UpdateFrequency.TRIENNIAL,
|
|
53
|
+
"quinquennale": UpdateFrequency.QUINQUENNIAL,
|
|
54
|
+
"aucune": UpdateFrequency.UNKNOWN,
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
XSD_PATH = os.path.join(os.path.dirname(__file__), "maaf.xsd")
|
|
@@ -161,7 +162,7 @@ class MaafBackend(BaseBackend):
|
|
|
161
162
|
dataset = self.get_dataset(item.remote_id)
|
|
162
163
|
|
|
163
164
|
dataset.title = metadata["title"]
|
|
164
|
-
dataset.frequency = FREQUENCIES.get(metadata["frequency"],
|
|
165
|
+
dataset.frequency = FREQUENCIES.get(metadata["frequency"], UpdateFrequency.UNKNOWN)
|
|
165
166
|
dataset.description = metadata["notes"]
|
|
166
167
|
dataset.private = metadata["private"]
|
|
167
168
|
dataset.tags = sorted(set(metadata["tags"]))
|
|
@@ -5,6 +5,7 @@ from datetime import date
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from udata.app import create_app
|
|
8
|
+
from udata.core.dataset.constants import UpdateFrequency
|
|
8
9
|
from udata.core.organization.factories import OrganizationFactory
|
|
9
10
|
from udata.core.spatial.factories import GeoZoneFactory
|
|
10
11
|
from udata.harvest import actions
|
|
@@ -336,7 +337,7 @@ def frequency_as_rdf_uri(resource_data):
|
|
|
336
337
|
"resources": [resource_data],
|
|
337
338
|
"extras": [{"key": "frequency", "value": "http://purl.org/cld/freq/daily"}],
|
|
338
339
|
}
|
|
339
|
-
return data, {"expected":
|
|
340
|
+
return data, {"expected": UpdateFrequency.DAILY}
|
|
340
341
|
|
|
341
342
|
|
|
342
343
|
@pytest.fixture
|
|
@@ -348,7 +349,7 @@ def frequency_as_exact_match(resource_data):
|
|
|
348
349
|
"resources": [resource_data],
|
|
349
350
|
"extras": [{"key": "frequency", "value": "daily"}],
|
|
350
351
|
}
|
|
351
|
-
return data, {"expected":
|
|
352
|
+
return data, {"expected": UpdateFrequency.DAILY}
|
|
352
353
|
|
|
353
354
|
|
|
354
355
|
@pytest.fixture
|
|
@@ -486,7 +487,7 @@ def test_can_parse_frequency_as_exact_match(result, kwargs):
|
|
|
486
487
|
|
|
487
488
|
|
|
488
489
|
@pytest.mark.ckan_data("frequency_as_unknown_value")
|
|
489
|
-
def
|
|
490
|
+
def test_can_parse_frequency_as_unknown_value(result, kwargs):
|
|
490
491
|
dataset = dataset_for(result)
|
|
491
492
|
assert dataset.extras["ckan:frequency"] == kwargs["expected"]
|
|
492
493
|
assert dataset.frequency is None
|
|
@@ -10,6 +10,7 @@ from rdflib import Graph
|
|
|
10
10
|
|
|
11
11
|
from udata.core.dataservices.factories import DataserviceFactory
|
|
12
12
|
from udata.core.dataservices.models import Dataservice
|
|
13
|
+
from udata.core.dataset.constants import UpdateFrequency
|
|
13
14
|
from udata.core.dataset.factories import DatasetFactory, LicenseFactory, ResourceSchemaMockData
|
|
14
15
|
from udata.core.dataset.rdf import dataset_from_rdf
|
|
15
16
|
from udata.core.organization.factories import OrganizationFactory
|
|
@@ -560,7 +561,7 @@ class DcatBackendTest:
|
|
|
560
561
|
assert dataset.harvest.issued_at.date() == date(2016, 12, 14)
|
|
561
562
|
assert dataset.harvest.created_at.date() == date(2016, 12, 12)
|
|
562
563
|
assert dataset.harvest.modified_at.date() == date(2016, 12, 14)
|
|
563
|
-
assert dataset.frequency ==
|
|
564
|
+
assert dataset.frequency == UpdateFrequency.DAILY
|
|
564
565
|
assert dataset.description == "Dataset 3 description"
|
|
565
566
|
|
|
566
567
|
assert dataset.temporal_coverage is not None
|
|
@@ -681,7 +682,7 @@ class DcatBackendTest:
|
|
|
681
682
|
dataset = Dataset.objects.filter(organization=org).first()
|
|
682
683
|
|
|
683
684
|
assert dataset is not None
|
|
684
|
-
assert dataset.frequency ==
|
|
685
|
+
assert dataset.frequency == UpdateFrequency.IRREGULAR
|
|
685
686
|
assert "gravi" in dataset.tags # support dcat:keyword
|
|
686
687
|
assert "geodesy" in dataset.tags # support dcat:theme
|
|
687
688
|
assert dataset.license.id == "fr-lo"
|
udata/i18n.py
CHANGED
|
@@ -206,38 +206,13 @@ class I18nBlueprintSetupState(BlueprintSetupState):
|
|
|
206
206
|
if "defaults" in options:
|
|
207
207
|
defaults = dict(defaults, **options.pop("defaults"))
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
self.app.add_url_rule(
|
|
219
|
-
rule,
|
|
220
|
-
"%s.%s_redirect" % (self.blueprint.name, endpoint),
|
|
221
|
-
redirect_to_lang,
|
|
222
|
-
defaults=defaults,
|
|
223
|
-
**options,
|
|
224
|
-
)
|
|
225
|
-
else:
|
|
226
|
-
self.app.add_url_rule(
|
|
227
|
-
rule,
|
|
228
|
-
"%s.%s" % (self.blueprint.name, endpoint),
|
|
229
|
-
view_func,
|
|
230
|
-
defaults=defaults,
|
|
231
|
-
**options,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
self.app.add_url_rule(
|
|
235
|
-
"/<lang:lang_code>" + rule,
|
|
236
|
-
"%s.%s_redirect" % (self.blueprint.name, endpoint),
|
|
237
|
-
redirect_to_unlocalized,
|
|
238
|
-
defaults=defaults,
|
|
239
|
-
**options,
|
|
240
|
-
)
|
|
209
|
+
self.app.add_url_rule(
|
|
210
|
+
rule,
|
|
211
|
+
"%s.%s" % (self.blueprint.name, endpoint),
|
|
212
|
+
view_func,
|
|
213
|
+
defaults=defaults,
|
|
214
|
+
**options,
|
|
215
|
+
)
|
|
241
216
|
|
|
242
217
|
|
|
243
218
|
class I18nBlueprint(Blueprint):
|
|
@@ -3,7 +3,6 @@ This migration keeps only the "Local authority" badge if the organization also h
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
from mongoengine.connection import get_db
|
|
9
8
|
|
|
@@ -84,7 +83,7 @@ def migrate(db):
|
|
|
84
83
|
)
|
|
85
84
|
log.info(f"\t{count.modified_count} open dataservices to DATASERVICE_ACCESS_TYPE_OPEN")
|
|
86
85
|
|
|
87
|
-
dataservices:
|
|
86
|
+
dataservices: list[Dataservice] = get_db().dataservice.find()
|
|
88
87
|
for dataservice in dataservices:
|
|
89
88
|
if (
|
|
90
89
|
"endpoint_description_url" not in dataservice
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convert legacy frequencies to latest vocabulary
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from udata.core.dataset.constants import UpdateFrequency
|
|
8
|
+
from udata.models import Dataset
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def migrate(db):
|
|
14
|
+
log.info("Updating datasets legacy frequencies:")
|
|
15
|
+
|
|
16
|
+
for legacy_value, frequency in UpdateFrequency._LEGACY_FREQUENCIES.items():
|
|
17
|
+
count = 0
|
|
18
|
+
for dataset in Dataset.objects(frequency=legacy_value).no_cache().timeout(False):
|
|
19
|
+
# Explicitly call update() to force writing the new frequency string to mongo.
|
|
20
|
+
# We can't rely on save() here because:
|
|
21
|
+
# 1. save() only writes modified fields to mongo, basically comparing the Dataset's
|
|
22
|
+
# object state initially returned by the query with its state when save() is called,
|
|
23
|
+
# and only sending the diffset to mongo.
|
|
24
|
+
# 2. At the ODM layer, the Dataset.frequency field has already been instantiated as an
|
|
25
|
+
# UpdateFrequency object, and so legacy frequency strings have already been
|
|
26
|
+
# mapped to their new ids (via UpdateFrequency._missing_).
|
|
27
|
+
# => While the raw frequency string has changed, the save() function sees the same
|
|
28
|
+
# UpdateFrequency and therefore ignores the field in is diffset.
|
|
29
|
+
dataset.update(frequency=frequency.value)
|
|
30
|
+
# Still call save() afterwards so computed fields like quality_cached are updated if
|
|
31
|
+
# necessary, e.g. if moving from a predictable timedelta to an unpredictable one.
|
|
32
|
+
dataset.save()
|
|
33
|
+
count += 1
|
|
34
|
+
log.info(f"- {legacy_value} -> {frequency.value}: {count} updated")
|
|
35
|
+
|
|
36
|
+
log.info("Completed.")
|
udata/settings.py
CHANGED
|
@@ -101,6 +101,24 @@ class Defaults(object):
|
|
|
101
101
|
SECURITY_RESET_URL = "/reset/"
|
|
102
102
|
SECURITY_CHANGE_EMAIL_URL = "/change-email/"
|
|
103
103
|
|
|
104
|
+
# See https://flask-security.readthedocs.io/en/stable/configuration.html#SECURITY_REDIRECT_BEHAVIOR
|
|
105
|
+
# We do not define all the URLs requested in the documentation because most of the time we do JSON requests in cdata
|
|
106
|
+
# and catch errors instead of followings the redirects.
|
|
107
|
+
# The only place where we don't have control over the redirect is when the user is clicking a link directly to udata
|
|
108
|
+
# (instead of a link to `cdata`) as in /confirm. When the user is clicking on the confirmation link, he's redirected
|
|
109
|
+
# to `confirm_change_email` endpoint, and then udata redirect him to the homepage of `cdata` with a custom flash message.
|
|
110
|
+
SECURITY_REDIRECT_BEHAVIOR = "spa"
|
|
111
|
+
# SECURITY_POST_OAUTH_LOGIN_VIEW = "" # SECURITY_OAUTH_ENABLE is disabled
|
|
112
|
+
# SECURITY_LOGIN_ERROR_VIEW = "" # We don't follow the redirects since we do JSON POST requests during login
|
|
113
|
+
# SECURITY_CONFIRM_ERROR_VIEW = "" # Manually changed in `confirm_change_email`
|
|
114
|
+
# SECURITY_POST_CHANGE_EMAIL_VIEW = "" # We don't follow the redirects since we do JSON POST requests during change email
|
|
115
|
+
# SECURITY_CHANGE_EMAIL_ERROR_VIEW = "" # We don't follow the redirects since we do JSON POST requests during change email
|
|
116
|
+
# SECURITY_POST_CONFIRM_VIEW = "" # Set at runtime. See :SecurityPostConfirmViewAtRuntime
|
|
117
|
+
# SECURITY_RESET_ERROR_VIEW = "" # We don't follow the redirects since we do JSON POST requests during request reset
|
|
118
|
+
# SECURITY_RESET_VIEW = "" # We don't follow the redirects since we do JSON POST requests during request reset
|
|
119
|
+
|
|
120
|
+
SECURITY_SPA_ON_SAME_DOMAIN = False
|
|
121
|
+
|
|
104
122
|
SECURITY_PASSWORD_SALT = "Default uData secret password salt"
|
|
105
123
|
SECURITY_CONFIRM_SALT = "Default uData secret confirm salt"
|
|
106
124
|
SECURITY_RESET_SALT = "Default uData secret reset salt"
|
|
@@ -122,6 +140,15 @@ class Defaults(object):
|
|
|
122
140
|
DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY = 30
|
|
123
141
|
MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS = 200
|
|
124
142
|
|
|
143
|
+
# You can activate CaptchEtat, a captcha.com integration by providing
|
|
144
|
+
# CAPTCHETAT_BASE_URL, CAPTCHETAT_OAUTH_BASE_URL, CAPTCHETAT_CLIENT_ID and CAPTCHETAT_CLIENT_SECRET
|
|
145
|
+
CAPTCHETAT_BASE_URL = None
|
|
146
|
+
CAPTCHETAT_OAUTH_BASE_URL = None
|
|
147
|
+
CAPTCHETAT_CLIENT_ID = None
|
|
148
|
+
CAPTCHETAT_CLIENT_SECRET = None
|
|
149
|
+
CAPTCHETAT_TOKEN_CACHE_KEY = "captchetat-bearer-token"
|
|
150
|
+
CAPTCHETAT_STYLE_NAME = "captchaFR"
|
|
151
|
+
|
|
125
152
|
# Sentry configuration
|
|
126
153
|
SENTRY_DSN = None
|
|
127
154
|
SENTRY_TAGS = {}
|