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.

Files changed (53) hide show
  1. udata/api/oauth2.py +22 -3
  2. udata/app.py +3 -0
  3. udata/auth/__init__.py +11 -0
  4. udata/auth/forms.py +70 -3
  5. udata/auth/mails.py +6 -0
  6. udata/auth/proconnect.py +127 -0
  7. udata/auth/views.py +57 -2
  8. udata/commands/db.py +2 -3
  9. udata/core/__init__.py +2 -0
  10. udata/core/captchetat.py +80 -0
  11. udata/core/dataservices/api.py +1 -2
  12. udata/core/dataset/api.py +3 -4
  13. udata/core/dataset/api_fields.py +3 -4
  14. udata/core/dataset/apiv2.py +6 -6
  15. udata/core/dataset/commands.py +0 -10
  16. udata/core/dataset/constants.py +124 -38
  17. udata/core/dataset/factories.py +2 -1
  18. udata/core/dataset/forms.py +14 -10
  19. udata/core/dataset/models.py +8 -36
  20. udata/core/dataset/preview.py +3 -3
  21. udata/core/dataset/rdf.py +84 -65
  22. udata/core/dataset/tasks.py +2 -50
  23. udata/core/metrics/helpers.py +6 -7
  24. udata/core/metrics/tasks.py +3 -6
  25. udata/core/post/api.py +1 -2
  26. udata/core/reuse/api.py +1 -2
  27. udata/core/user/api.py +1 -3
  28. udata/cors.py +19 -2
  29. udata/harvest/backends/ckan/harvesters.py +10 -14
  30. udata/harvest/backends/maaf.py +15 -14
  31. udata/harvest/tests/ckan/test_ckan_backend.py +4 -3
  32. udata/harvest/tests/test_dcat_backend.py +3 -2
  33. udata/i18n.py +7 -32
  34. udata/migrations/2025-01-05-dataservices-fields-changes.py +1 -2
  35. udata/migrations/2025-09-04-update-legacy-frequencies.py +36 -0
  36. udata/settings.py +27 -0
  37. udata/templates/security/email/reset_instructions.html +1 -1
  38. udata/templates/security/email/reset_instructions.txt +1 -1
  39. udata/tests/api/test_datasets_api.py +41 -12
  40. udata/tests/dataset/test_dataset_model.py +17 -53
  41. udata/tests/dataset/test_dataset_rdf.py +27 -28
  42. udata/translations/udata.pot +226 -150
  43. udata/uris.py +1 -2
  44. udata/utils.py +8 -1
  45. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/METADATA +3 -4
  46. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/RECORD +50 -50
  47. udata/templates/mail/frequency_reminder.html +0 -34
  48. udata/templates/mail/frequency_reminder.txt +0 -18
  49. udata/tests/test_i18n.py +0 -93
  50. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/WHEEL +0 -0
  51. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/entry_points.txt +0 -0
  52. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/licenses/LICENSE +0 -0
  53. {udata-11.1.2.dev7.dist-info → udata-11.1.2.dev11.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,19 @@
1
1
  import collections
2
2
  import os
3
- from datetime import datetime, timedelta
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.i18n import lazy_gettext as _
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":
@@ -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() -> List[str]:
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: List[Dict], metrics_labels: List[str]) -> OrderedDict:
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: Dict, metrics_labels: List[str]) -> List[OrderedDict]:
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: Union[str, ObjectId, None], metrics_labels: List[str]
49
- ) -> List[OrderedDict]:
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: Union[str, ObjectId, None]) -> str:
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"}
@@ -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
- # Better log if we're using Python 3.9
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: Dict[str, int]) -> None:
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: List[str], page_size: int = 50) -> dict:
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: List[Post] = Post.objects().published().order_by("-published").limit(15)
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: List[Reuse] = Reuse.objects.visible().order_by("-created_at").limit(15)
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) -> Optional[int]:
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.harvest.models import HarvestItem
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 = frequency_from_rdf(value)
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
- dataset.extras["ckan:frequency"] = value
203
- log.debug("frequency value not handled: %s", value)
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)
@@ -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": "punctual",
42
- "temps réel": "continuous",
43
- "quotidienne": "daily",
44
- "hebdomadaire": "weekly",
45
- "bimensuelle": "semimonthly",
46
- "mensuelle": "monthly",
47
- "bimestrielle": "bimonthly",
48
- "trimestrielle": "quarterly",
49
- "semestrielle": "semiannual",
50
- "annuelle": "annual",
51
- "triennale": "triennial",
52
- "quinquennale": "quinquennial",
53
- "aucune": "unknown",
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"], "unknown")
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": "daily"}
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": "daily"}
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 test_can_parse_frequency_as_unkown_value(result, kwargs):
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 == "daily"
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 == "irregular"
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
- if options.pop("localize", True):
210
- self.app.add_url_rule(
211
- "/<lang:lang_code>" + rule,
212
- "%s.%s" % (self.blueprint.name, endpoint),
213
- view_func,
214
- defaults=defaults,
215
- **options,
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: List[Dataservice] = get_db().dataservice.find()
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 = {}
@@ -16,7 +16,7 @@
16
16
  <td align="center">
17
17
  {{ mail_button(
18
18
  _('Reset my password'),
19
- reset_link
19
+ cdata_url("/reset/" ~ reset_token) or reset_link
20
20
  ) }}
21
21
  </td>
22
22
  </tr>
@@ -4,6 +4,6 @@
4
4
 
5
5
  {{ _('To reset your password, please confirm your request through the link below:') }}
6
6
 
7
- {{ reset_link }}
7
+ {{ cdata_url("/reset/" ~ reset_token) or reset_link }}
8
8
 
9
9
  {% endblock %}