udata 13.0.1.dev12__py3-none-any.whl → 14.4.1.dev7__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 (177) hide show
  1. udata/api/__init__.py +2 -8
  2. udata/api_fields.py +35 -4
  3. udata/app.py +30 -50
  4. udata/auth/__init__.py +29 -6
  5. udata/auth/forms.py +8 -6
  6. udata/auth/views.py +6 -3
  7. udata/commands/__init__.py +2 -14
  8. udata/commands/db.py +13 -25
  9. udata/commands/info.py +0 -16
  10. udata/commands/serve.py +3 -11
  11. udata/commands/tests/test_fixtures.py +9 -9
  12. udata/core/access_type/api.py +1 -1
  13. udata/core/access_type/constants.py +12 -8
  14. udata/core/activity/api.py +5 -6
  15. udata/core/avatars/api.py +43 -0
  16. udata/core/avatars/test_avatar_api.py +30 -0
  17. udata/core/badges/tests/test_commands.py +6 -6
  18. udata/core/csv.py +5 -0
  19. udata/core/dataservices/models.py +15 -3
  20. udata/core/dataservices/tasks.py +7 -0
  21. udata/core/dataset/api.py +2 -0
  22. udata/core/dataset/models.py +2 -2
  23. udata/core/dataset/permissions.py +31 -0
  24. udata/core/dataset/tasks.py +50 -10
  25. udata/core/discussions/models.py +1 -0
  26. udata/core/metrics/__init__.py +0 -6
  27. udata/core/organization/api.py +8 -5
  28. udata/core/organization/mails.py +1 -1
  29. udata/core/organization/models.py +9 -1
  30. udata/core/organization/notifications.py +84 -0
  31. udata/core/organization/permissions.py +1 -1
  32. udata/core/organization/tasks.py +3 -0
  33. udata/core/pages/tests/test_api.py +32 -0
  34. udata/core/post/api.py +24 -69
  35. udata/core/post/models.py +84 -16
  36. udata/core/post/tests/test_api.py +24 -1
  37. udata/core/reports/api.py +18 -0
  38. udata/core/reports/models.py +42 -2
  39. udata/core/reuse/models.py +1 -1
  40. udata/core/reuse/tasks.py +7 -0
  41. udata/core/site/models.py +2 -6
  42. udata/core/spatial/commands.py +2 -4
  43. udata/core/spatial/forms.py +2 -2
  44. udata/core/spatial/models.py +0 -10
  45. udata/core/spatial/tests/test_api.py +1 -36
  46. udata/core/user/models.py +15 -2
  47. udata/cors.py +2 -5
  48. udata/db/migrations.py +279 -0
  49. udata/features/notifications/api.py +7 -18
  50. udata/features/notifications/models.py +56 -0
  51. udata/features/notifications/tasks.py +25 -0
  52. udata/flask_mongoengine/engine.py +0 -4
  53. udata/frontend/__init__.py +3 -122
  54. udata/frontend/markdown.py +2 -1
  55. udata/harvest/actions.py +24 -9
  56. udata/harvest/api.py +30 -22
  57. udata/harvest/backends/__init__.py +21 -9
  58. udata/harvest/backends/base.py +29 -3
  59. udata/harvest/backends/ckan/harvesters.py +13 -2
  60. udata/harvest/backends/dcat.py +3 -0
  61. udata/harvest/backends/maaf.py +1 -0
  62. udata/harvest/commands.py +39 -4
  63. udata/harvest/filters.py +17 -6
  64. udata/harvest/forms.py +9 -6
  65. udata/harvest/models.py +16 -0
  66. udata/harvest/permissions.py +27 -0
  67. udata/harvest/tasks.py +3 -5
  68. udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
  69. udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
  70. udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
  71. udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
  72. udata/harvest/tests/dcat/udata.xml +6 -6
  73. udata/harvest/tests/factories.py +1 -1
  74. udata/harvest/tests/test_actions.py +63 -8
  75. udata/harvest/tests/test_api.py +278 -123
  76. udata/harvest/tests/test_base_backend.py +88 -1
  77. udata/harvest/tests/test_dcat_backend.py +60 -13
  78. udata/harvest/tests/test_filters.py +6 -0
  79. udata/i18n.py +11 -273
  80. udata/mail.py +5 -1
  81. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  82. udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
  83. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  84. udata/models/__init__.py +0 -8
  85. udata/mongo/slug_fields.py +1 -1
  86. udata/rdf.py +45 -6
  87. udata/routing.py +2 -10
  88. udata/sentry.py +4 -10
  89. udata/settings.py +23 -17
  90. udata/tasks.py +4 -3
  91. udata/templates/mail/message.html +5 -31
  92. udata/tests/__init__.py +28 -12
  93. udata/tests/api/__init__.py +108 -21
  94. udata/tests/api/test_activities_api.py +36 -0
  95. udata/tests/api/test_auth_api.py +121 -95
  96. udata/tests/api/test_base_api.py +7 -4
  97. udata/tests/api/test_dataservices_api.py +29 -1
  98. udata/tests/api/test_datasets_api.py +45 -21
  99. udata/tests/api/test_organizations_api.py +192 -197
  100. udata/tests/api/test_reports_api.py +157 -0
  101. udata/tests/api/test_reuses_api.py +147 -147
  102. udata/tests/api/test_security_api.py +12 -12
  103. udata/tests/api/test_swagger.py +4 -4
  104. udata/tests/api/test_tags_api.py +8 -8
  105. udata/tests/api/test_user_api.py +13 -1
  106. udata/tests/apiv2/test_swagger.py +4 -4
  107. udata/tests/apiv2/test_topics.py +1 -1
  108. udata/tests/cli/test_cli_base.py +8 -9
  109. udata/tests/dataset/test_dataset_commands.py +4 -4
  110. udata/tests/dataset/test_dataset_model.py +66 -26
  111. udata/tests/dataset/test_dataset_rdf.py +99 -5
  112. udata/tests/dataset/test_resource_preview.py +0 -1
  113. udata/tests/frontend/test_auth.py +24 -1
  114. udata/tests/frontend/test_csv.py +0 -3
  115. udata/tests/helpers.py +37 -27
  116. udata/tests/organization/test_notifications.py +67 -2
  117. udata/tests/plugin.py +6 -261
  118. udata/tests/site/test_site_csv_exports.py +22 -10
  119. udata/tests/test_activity.py +9 -9
  120. udata/tests/test_cors.py +1 -1
  121. udata/tests/test_dcat_commands.py +2 -2
  122. udata/tests/test_discussions.py +5 -5
  123. udata/tests/test_migrations.py +181 -481
  124. udata/tests/test_notifications.py +15 -57
  125. udata/tests/test_notifications_task.py +43 -0
  126. udata/tests/test_owned.py +81 -1
  127. udata/tests/test_storages.py +25 -19
  128. udata/tests/test_topics.py +77 -61
  129. udata/tests/test_uris.py +33 -0
  130. udata/tests/workers/test_jobs_commands.py +23 -23
  131. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  132. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  133. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  134. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  135. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  136. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  137. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  138. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  139. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  140. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  141. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  142. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  143. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  144. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  145. udata/translations/udata.pot +215 -106
  146. udata/uris.py +0 -2
  147. udata/utils.py +5 -0
  148. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  149. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
  150. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
  151. udata/core/followers/views.py +0 -15
  152. udata/core/post/forms.py +0 -30
  153. udata/entrypoints.py +0 -93
  154. udata/features/identicon/__init__.py +0 -0
  155. udata/features/identicon/api.py +0 -13
  156. udata/features/identicon/backends.py +0 -131
  157. udata/features/identicon/tests/__init__.py +0 -0
  158. udata/features/identicon/tests/test_backends.py +0 -18
  159. udata/features/territories/__init__.py +0 -49
  160. udata/features/territories/api.py +0 -25
  161. udata/features/territories/models.py +0 -51
  162. udata/flask_mongoengine/json.py +0 -38
  163. udata/migrations/__init__.py +0 -367
  164. udata/templates/mail/base.html +0 -105
  165. udata/templates/mail/base.txt +0 -6
  166. udata/templates/mail/button.html +0 -3
  167. udata/templates/mail/layouts/1-column.html +0 -19
  168. udata/templates/mail/layouts/2-columns.html +0 -20
  169. udata/templates/mail/layouts/center-panel.html +0 -16
  170. udata/tests/cli/test_db_cli.py +0 -68
  171. udata/tests/features/territories/__init__.py +0 -20
  172. udata/tests/features/territories/test_territories_api.py +0 -185
  173. udata/tests/frontend/test_hooks.py +0 -149
  174. udata-13.0.1.dev12.dist-info/METADATA +0 -133
  175. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  176. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  177. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/tests/helpers.py CHANGED
@@ -4,11 +4,11 @@ from datetime import timedelta
4
4
  from io import BytesIO
5
5
  from urllib.parse import parse_qs, urlparse
6
6
 
7
- import mock
8
7
  from flask import current_app, json
9
8
  from flask_security.babel import FsDomain
10
9
  from PIL import Image
11
10
 
11
+ from udata.core.spatial.factories import GeoZoneFactory
12
12
  from udata.mail import mail_sent
13
13
 
14
14
 
@@ -34,51 +34,50 @@ def assert_json_equal(first, second):
34
34
 
35
35
 
36
36
  @contextmanager
37
- def mock_signals(callback, *signals):
37
+ def mock_signals(*signals):
38
38
  __tracebackhide__ = True
39
- specs = []
40
39
 
41
- def handler(sender, **kwargs):
42
- pass
40
+ callbacks_by_signal = {}
41
+ calls_kwargs_by_signal = {}
43
42
 
44
- for signal in signals:
45
- m = mock.Mock(spec=handler)
46
- signal.connect(m, weak=False)
47
- specs.append((signal, m))
43
+ for requestSignal in signals:
44
+ # We capture requestSignal with a default argument
45
+ def callback(*args, requestSignal=requestSignal, **kwargs):
46
+ calls_kwargs_by_signal.setdefault(requestSignal, [])
47
+ calls_kwargs_by_signal[requestSignal].append(kwargs)
48
+
49
+ callbacks_by_signal[requestSignal] = callback
50
+ requestSignal.connect(callback, weak=False)
48
51
 
49
- yield
52
+ yield calls_kwargs_by_signal
50
53
 
51
- for signal, mock_handler in specs:
52
- signal.disconnect(mock_handler)
53
- signal_name = getattr(signal, "name", str(signal))
54
- callback(signal_name, mock_handler)
54
+ for sig in signals:
55
+ sig.disconnect(callbacks_by_signal[sig])
55
56
 
56
57
 
57
58
  @contextmanager
58
59
  def assert_emit(*signals, assertions_callback=None):
59
60
  __tracebackhide__ = True
60
- msg = 'Signal "{0}" should have been emitted'
61
-
62
- def callback(name, handler):
63
- assert handler.called, msg.format(name)
64
- if assertions_callback is not None:
65
- assertions_callback(handler.call_args)
66
61
 
67
- with mock_signals(callback, *signals):
62
+ with mock_signals(*signals) as calls_kwargs_by_signal:
68
63
  yield
69
64
 
65
+ for signal in signals:
66
+ assert signal in calls_kwargs_by_signal, f'Signal "{signal}" should have been emitted'
67
+ if assertions_callback is not None:
68
+ for kwargs in calls_kwargs_by_signal[signal]:
69
+ assertions_callback(kwargs)
70
+
70
71
 
71
72
  @contextmanager
72
73
  def assert_not_emit(*signals):
73
74
  __tracebackhide__ = True
74
- msg = 'Signal "{0}" should NOT have been emitted'
75
-
76
- def callback(name, handler):
77
- assert not handler.called, msg.format(name)
78
-
79
- with mock_signals(callback, *signals):
75
+ with mock_signals(*signals) as calls_args_by_signal:
80
76
  yield
81
77
 
78
+ for signal in signals:
79
+ assert signal not in calls_args_by_signal, f'Signal "{signal}" should not have been emitted'
80
+
82
81
 
83
82
  @contextmanager
84
83
  def capture_mails():
@@ -221,5 +220,16 @@ def create_test_image():
221
220
  return file
222
221
 
223
222
 
223
+ def create_geozones_fixtures():
224
+ paca = GeoZoneFactory(
225
+ id="fr:region:93", level="fr:region", name="Provence Alpes Côtes dAzur", code="93"
226
+ )
227
+ bdr = GeoZoneFactory(
228
+ id="fr:departement:13", level="fr:departement", name="Bouches-du-Rhône", code="13"
229
+ )
230
+ arles = GeoZoneFactory(id="fr:commune:13004", level="fr:commune", name="Arles", code="13004")
231
+ return paca, bdr, arles
232
+
233
+
224
234
  def security_gettext(string):
225
235
  return FsDomain(current_app).gettext(string)
@@ -1,8 +1,11 @@
1
1
  from udata.core.organization.factories import OrganizationFactory
2
- from udata.core.organization.notifications import membership_request_notifications
2
+ from udata.core.organization.notifications import (
3
+ membership_request_notifications,
4
+ )
3
5
  from udata.core.user.factories import UserFactory
6
+ from udata.features.notifications.models import Notification
4
7
  from udata.models import Member, MembershipRequest
5
- from udata.tests.api import PytestOnlyDBTestCase
8
+ from udata.tests.api import DBTestCase, PytestOnlyDBTestCase
6
9
  from udata.tests.helpers import assert_equal_dates
7
10
 
8
11
 
@@ -27,3 +30,65 @@ class OrganizationNotificationsTest(PytestOnlyDBTestCase):
27
30
  assert details["user"]["id"] == applicant.id
28
31
  assert details["user"]["fullname"] == applicant.fullname
29
32
  assert details["user"]["avatar"] == str(applicant.avatar)
33
+
34
+
35
+ class MembershipRequestNotificationTest(DBTestCase):
36
+ def test_notification_created_for_admins_only(self):
37
+ """Notifications are created for all admin users, not editors"""
38
+ admin1 = UserFactory()
39
+ admin2 = UserFactory()
40
+ editor = UserFactory()
41
+ applicant = UserFactory()
42
+ members = [
43
+ Member(user=editor, role="editor"),
44
+ Member(user=admin1, role="admin"),
45
+ Member(user=admin2, role="admin"),
46
+ ]
47
+ org = OrganizationFactory(members=members)
48
+
49
+ request = MembershipRequest(user=applicant, comment="test")
50
+ org.add_membership_request(request)
51
+
52
+ notifications = Notification.objects.all()
53
+ assert len(notifications) == 2
54
+
55
+ admin_users = [notif.user for notif in notifications]
56
+ self.assertIn(admin1, admin_users)
57
+ self.assertIn(admin2, admin_users)
58
+
59
+ for notification in notifications:
60
+ assert notification.details.request_organization == org
61
+ assert notification.details.request_user == applicant
62
+ assert_equal_dates(notification.created_at, request.created)
63
+
64
+ def test_no_duplicate_notifications(self):
65
+ """Duplicate notifications are not created on subsequent saves"""
66
+ admin = UserFactory()
67
+ applicant = UserFactory()
68
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
69
+
70
+ request = MembershipRequest(user=applicant, comment="test")
71
+ org.add_membership_request(request)
72
+ org.add_membership_request(request)
73
+
74
+ assert Notification.objects.count() == 1
75
+
76
+ def test_multiple_requests_create_separate_notifications(self):
77
+ """Multiple requests from different users create separate notifications"""
78
+ admin = UserFactory()
79
+ applicant1 = UserFactory()
80
+ applicant2 = UserFactory()
81
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
82
+
83
+ request1 = MembershipRequest(user=applicant1, comment="test 1")
84
+ org.add_membership_request(request1)
85
+
86
+ request2 = MembershipRequest(user=applicant2, comment="test 2")
87
+ org.add_membership_request(request2)
88
+
89
+ notifications = Notification.objects.all()
90
+ assert len(notifications) == 2
91
+
92
+ request_users = [notif.details.request_user for notif in notifications]
93
+ self.assertIn(applicant1, request_users)
94
+ self.assertIn(applicant2, request_users)
udata/tests/plugin.py CHANGED
@@ -1,147 +1,20 @@
1
- import shlex
2
- from contextlib import contextmanager
3
-
4
1
  import pytest
5
- from flask import current_app, json, template_rendered, url_for
6
- from flask.testing import FlaskClient
7
- from flask_principal import Identity, identity_changed
8
- from lxml import etree
9
-
10
- from udata.core.user.factories import UserFactory
11
-
12
- from .helpers import assert200, assert_command_ok
13
-
14
-
15
- class TestClient(FlaskClient):
16
- """
17
- The goal of these `post`, `put` and `delete` functions is to
18
- switch from `data` in kwargs to `data` in args and be able to
19
- `client.post(url, data)` without doing `client.post(url, data=data)`
20
-
21
- Same as in :TestClientOverride
22
- """
23
-
24
- def post(self, url, data=None, **kwargs):
25
- return super(TestClient, self).post(url, data=data, **kwargs)
26
-
27
- def put(self, url, data=None, **kwargs):
28
- return super(TestClient, self).put(url, data=data, **kwargs)
29
-
30
- def delete(self, url, data=None, **kwargs):
31
- return super(TestClient, self).delete(url, data=data, **kwargs)
32
-
33
- def login(self, user=None):
34
- user = user or UserFactory()
35
- with self.session_transaction() as session:
36
- # Since flask-security-too 4.0.0, the user.fs_uniquifier is used instead of user.id for auth
37
- user_id = getattr(user, current_app.login_manager.id_attribute)()
38
- session["user_id"] = user_id
39
- session["_fresh"] = True
40
- session["_id"] = current_app.login_manager._session_identifier_generator()
41
- current_app.login_manager._update_request_context_with_user(user)
42
- identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
43
- return user
44
-
45
- def logout(self):
46
- with self.session_transaction() as session:
47
- del session["user_id"]
48
- del session["_fresh"]
49
- del session["_id"]
50
2
 
51
3
 
52
4
  @pytest.fixture
53
- def client(app):
54
- """
55
- Fixes https://github.com/pytest-dev/pytest-flask/issues/42
56
- """
57
- return app.test_client()
58
-
59
-
60
- class ApiClient(object):
61
- def __init__(self, client):
62
- self.client = client
63
- self._user = None
64
-
65
- def login(self, *args, **kwargs):
66
- return self.client.login(*args, **kwargs)
67
-
68
- @contextmanager
69
- def user(self, user=None):
70
- self._user = user or UserFactory()
71
- if not self._user.apikey:
72
- self._user.generate_api_key()
73
- self._user.save()
74
- yield self._user
75
-
76
- def perform(self, verb, url, **kwargs):
77
- headers = kwargs.pop("headers", {})
78
- headers["Content-Type"] = "application/json"
79
-
80
- data = kwargs.get("data")
81
- if data is not None:
82
- data = json.dumps(data)
83
- headers["Content-Length"] = len(data)
84
- kwargs["data"] = data
85
-
86
- if self._user:
87
- headers["X-API-KEY"] = kwargs.get("X-API-KEY", self._user.apikey)
88
-
89
- kwargs["headers"] = headers
90
- method = getattr(self.client, verb)
91
- return method(url, **kwargs)
92
-
93
- def get(self, url, *args, **kwargs):
94
- return self.perform("get", url, *args, **kwargs)
95
-
96
- def post(self, url, data=None, json=True, *args, **kwargs):
97
- if not json:
98
- return self.client.post(url, data or {}, *args, **kwargs)
99
- return self.perform("post", url, data=data or {}, *args, **kwargs)
100
-
101
- def put(self, url, data=None, json=True, *args, **kwargs):
102
- if not json:
103
- return self.client.put(url, data or {}, *args, **kwargs)
104
- return self.perform("put", url, data=data or {}, *args, **kwargs)
105
-
106
- def patch(self, url, data=None, json=True, *args, **kwargs):
107
- if not json:
108
- return self.client.patch(url, data or {}, *args, **kwargs)
109
- return self.perform("patch", url, data=data or {}, *args, **kwargs)
110
-
111
- def delete(self, url, data=None, *args, **kwargs):
112
- return self.perform("delete", url, data=data or {}, *args, **kwargs)
113
-
114
- def options(self, url, data=None, *args, **kwargs):
115
- return self.perform("options", url, data=data or {}, *args, **kwargs)
116
-
117
-
118
- @pytest.fixture
119
- def api(client):
120
- api_client = ApiClient(client)
121
- return api_client
122
-
123
-
124
- @pytest.fixture(name="cli")
125
- def cli_fixture(app):
126
- def mock_runner(*args, **kwargs):
127
- from udata.commands import cli
128
-
129
- if len(args) == 1 and " " in args[0]:
130
- args = shlex.split(args[0])
131
- runner = app.test_cli_runner()
132
- result = runner.invoke(cli, args, catch_exceptions=False)
133
- if kwargs.get("check", True):
134
- assert_command_ok(result)
135
- return result
5
+ def rmock():
6
+ """A requests-mock fixture"""
7
+ import requests_mock
136
8
 
137
- return mock_runner
9
+ with requests_mock.Mocker() as m:
10
+ m.ANY = requests_mock.ANY
11
+ yield m
138
12
 
139
13
 
140
14
  @pytest.fixture
141
15
  def instance_path(app, tmpdir):
142
16
  """Use temporary application instance_path"""
143
17
  from udata.core import storages
144
- from udata.core.storages.views import blueprint
145
18
 
146
19
  app.instance_path = str(tmpdir)
147
20
  app.config["FS_ROOT"] = str(tmpdir / "fs")
@@ -152,133 +25,5 @@ def instance_path(app, tmpdir):
152
25
  app.config.pop(key.format("ROOT"), None)
153
26
 
154
27
  storages.init_app(app)
155
- app.register_blueprint(blueprint, name="test-storage")
156
28
 
157
29
  return tmpdir
158
-
159
-
160
- class ContextVariableDoesNotExist(Exception):
161
- pass
162
-
163
-
164
- class TemplateRecorder:
165
- @contextmanager
166
- def capture(self):
167
- self.templates = []
168
- template_rendered.connect(self._add_template)
169
- yield
170
- template_rendered.disconnect(self._add_template)
171
-
172
- def _add_template(self, app, template, context):
173
- self.templates.append((template, context))
174
-
175
- def assert_used(self, name):
176
- """
177
- Checks if a given template is used in the request.
178
-
179
- :param name: template name
180
- """
181
- __tracebackhide__ = True
182
-
183
- used_templates = []
184
-
185
- for template, context in self.templates:
186
- if template.name == name:
187
- return True
188
-
189
- used_templates.append(template)
190
-
191
- msg = "Template %s not used. Templates were used: %s" % (
192
- name,
193
- " ".join(repr(used_templates)),
194
- )
195
- raise AssertionError(msg)
196
-
197
- def get_context_variable(self, name):
198
- """
199
- Returns a variable from the context passed to the template.
200
-
201
- :param name: name of variable
202
- :raises ContextVariableDoesNotExist: if does not exist.
203
- """
204
- for template, context in self.templates:
205
- if name in context:
206
- return context[name]
207
- raise ContextVariableDoesNotExist()
208
-
209
-
210
- @pytest.fixture
211
- def templates():
212
- recorder = TemplateRecorder()
213
- with recorder.capture():
214
- yield recorder
215
-
216
-
217
- @pytest.fixture
218
- def httpretty():
219
- import httpretty
220
-
221
- httpretty.reset()
222
- httpretty.enable()
223
- yield httpretty
224
- httpretty.disable()
225
-
226
-
227
- @pytest.fixture
228
- def rmock():
229
- """A requests-mock fixture"""
230
- import requests_mock
231
-
232
- with requests_mock.Mocker() as m:
233
- m.ANY = requests_mock.ANY
234
- yield m
235
-
236
-
237
- class SitemapClient:
238
- # Needed for lxml XPath not supporting default namespace
239
- NAMESPACES = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
240
- MISMATCH = 'URL "{0}" {1} mismatch: expected "{2}" found "{3}"'
241
-
242
- def __init__(self, client):
243
- self.client = client
244
- self._sitemap = None
245
-
246
- def fetch(self, secure=False):
247
- base_url = "{0}://local.test".format("https" if secure else "http")
248
- response = self.client.get("sitemap.xml", base_url=base_url)
249
- assert200(response)
250
- self._sitemap = etree.fromstring(response.data)
251
- return self._sitemap
252
-
253
- def xpath(self, query):
254
- return self._sitemap.xpath(query, namespaces=self.NAMESPACES)
255
-
256
- def get_by_url(self, endpoint, **kwargs):
257
- url = url_for(endpoint, _external=True, **kwargs)
258
- query = 's:url[s:loc="{url}"]'.format(url=url)
259
- result = self.xpath(query)
260
- return result[0] if result else None
261
-
262
- def assert_url(self, url, priority, changefreq):
263
- """
264
- Check than a URL is present in the sitemap
265
- with given `priority` and `changefreq`
266
- """
267
- __tracebackhide__ = True
268
- r = url.xpath("s:priority", namespaces=self.NAMESPACES)
269
- assert len(r) == 1, 'URL "{0}" should have one priority'.format(url)
270
- found = r[0].text
271
- msg = self.MISMATCH.format(url, "priority", priority, found)
272
- assert found == str(priority), msg
273
-
274
- r = url.xpath("s:changefreq", namespaces=self.NAMESPACES)
275
- assert len(r) == 1, 'URL "{0}" should have one changefreq'.format(url)
276
- found = r[0].text
277
- msg = self.MISMATCH.format(url, "changefreq", changefreq, found)
278
- assert found == changefreq, msg
279
-
280
-
281
- @pytest.fixture
282
- def sitemap(client):
283
- sitemap_client = SitemapClient(client)
284
- return sitemap_client
@@ -7,6 +7,7 @@ from flask import url_for
7
7
  from udata.core import csv
8
8
  from udata.core.dataservices.factories import DataserviceFactory
9
9
  from udata.core.dataset import tasks as dataset_tasks
10
+ from udata.core.dataset.constants import SPD
10
11
  from udata.core.dataset.factories import DatasetFactory, ResourceFactory
11
12
  from udata.core.organization.factories import OrganizationFactory
12
13
  from udata.core.reuse.factories import ReuseFactory
@@ -25,7 +26,6 @@ class SiteCsvExportsTest(APITestCase):
25
26
 
26
27
  self.assert200(response)
27
28
  self.assertEqual(response.mimetype, "text/csv")
28
- self.assertEqual(response.charset, "utf-8")
29
29
 
30
30
  csvfile = StringIO(response.data.decode("utf8"))
31
31
  reader = csv.get_reader(csvfile)
@@ -73,7 +73,6 @@ class SiteCsvExportsTest(APITestCase):
73
73
 
74
74
  self.assert200(response)
75
75
  self.assertEqual(response.mimetype, "text/csv")
76
- self.assertEqual(response.charset, "utf-8")
77
76
 
78
77
  csvfile = StringIO(response.data.decode("utf8"))
79
78
  reader = csv.get_reader(csvfile)
@@ -99,6 +98,27 @@ class SiteCsvExportsTest(APITestCase):
99
98
  self.assertNotIn(str(dataset.id), ids)
100
99
  self.assertNotIn(str(hidden_dataset.id), ids)
101
100
 
101
+ def test_datasets_csv_with_badge_filter(self):
102
+ self.app.config["EXPORT_CSV_MODELS"] = []
103
+ dataset_with_badge = DatasetFactory(resources=[ResourceFactory()])
104
+ dataset_with_badge.add_badge(SPD)
105
+ dataset_without_badge = DatasetFactory(resources=[ResourceFactory()])
106
+
107
+ response = self.get(url_for("api.site_datasets_csv", badge=SPD))
108
+
109
+ self.assert200(response)
110
+
111
+ csvfile = StringIO(response.data.decode("utf8"))
112
+ reader = csv.get_reader(csvfile)
113
+ next(reader) # skip header
114
+
115
+ rows = list(reader)
116
+ ids = [row[0] for row in rows]
117
+
118
+ self.assertEqual(len(rows), 1)
119
+ self.assertIn(str(dataset_with_badge.id), ids)
120
+ self.assertNotIn(str(dataset_without_badge.id), ids)
121
+
102
122
  def test_resources_csv(self):
103
123
  self.app.config["EXPORT_CSV_MODELS"] = []
104
124
  datasets = [
@@ -110,7 +130,6 @@ class SiteCsvExportsTest(APITestCase):
110
130
 
111
131
  self.assert200(response)
112
132
  self.assertEqual(response.mimetype, "text/csv")
113
- self.assertEqual(response.charset, "utf-8")
114
133
 
115
134
  csvfile = StringIO(response.data.decode("utf8"))
116
135
  reader = csv.get_reader(csvfile)
@@ -164,7 +183,6 @@ class SiteCsvExportsTest(APITestCase):
164
183
 
165
184
  self.assert200(response)
166
185
  self.assertEqual(response.mimetype, "text/csv")
167
- self.assertEqual(response.charset, "utf-8")
168
186
 
169
187
  csvfile = StringIO(response.data.decode("utf8"))
170
188
  reader = csv.get_reader(csvfile)
@@ -200,7 +218,6 @@ class SiteCsvExportsTest(APITestCase):
200
218
 
201
219
  self.assert200(response)
202
220
  self.assertEqual(response.mimetype, "text/csv")
203
- self.assertEqual(response.charset, "utf-8")
204
221
 
205
222
  csvfile = StringIO(response.data.decode("utf8"))
206
223
  reader = csv.get_reader(csvfile)
@@ -245,7 +262,6 @@ class SiteCsvExportsTest(APITestCase):
245
262
 
246
263
  self.assert200(response)
247
264
  self.assertEqual(response.mimetype, "text/csv")
248
- self.assertEqual(response.charset, "utf-8")
249
265
 
250
266
  csvfile = StringIO(response.data.decode("utf8"))
251
267
  reader = csv.get_reader(csvfile)
@@ -293,7 +309,6 @@ class SiteCsvExportsTest(APITestCase):
293
309
 
294
310
  self.assert200(response)
295
311
  self.assertEqual(response.mimetype, "text/csv")
296
- self.assertEqual(response.charset, "utf-8")
297
312
 
298
313
  csvfile = StringIO(response.data.decode("utf8"))
299
314
  reader = csv.get_reader(csvfile)
@@ -332,7 +347,6 @@ class SiteCsvExportsTest(APITestCase):
332
347
 
333
348
  self.assert200(response)
334
349
  self.assertEqual(response.mimetype, "text/csv")
335
- self.assertEqual(response.charset, "utf-8")
336
350
 
337
351
  csvfile = StringIO(response.data.decode("utf8"))
338
352
  reader = csv.get_reader(csvfile)
@@ -379,7 +393,6 @@ class SiteCsvExportsTest(APITestCase):
379
393
 
380
394
  self.assert200(response)
381
395
  self.assertEqual(response.mimetype, "text/csv")
382
- self.assertEqual(response.charset, "utf-8")
383
396
 
384
397
  csvfile = StringIO(response.data.decode("utf8"))
385
398
  reader = csv.get_reader(csvfile)
@@ -424,7 +437,6 @@ class SiteCsvExportsTest(APITestCase):
424
437
 
425
438
  self.assert200(response)
426
439
  self.assertEqual(response.mimetype, "text/csv")
427
- self.assertEqual(response.charset, "utf-8")
428
440
 
429
441
  csvfile = StringIO(response.data.decode("utf8"))
430
442
  reader = csv.get_reader(csvfile)
@@ -211,9 +211,9 @@ class AuditableTest(APITestCase):
211
211
  not_auditable="original",
212
212
  )
213
213
 
214
- def check_signal_update(args):
214
+ def check_signal_update(kwargs):
215
215
  self.assertEqual(
216
- args[1]["changed_fields"],
216
+ kwargs["changed_fields"],
217
217
  [
218
218
  "name",
219
219
  "tags",
@@ -224,13 +224,13 @@ class AuditableTest(APITestCase):
224
224
  "embedded_list.1.name",
225
225
  ],
226
226
  )
227
- self.assertEqual(args[1]["previous"]["name"], "fake")
228
- self.assertEqual(args[1]["previous"]["tags"], ["some", "tags"])
229
- self.assertEqual(args[1]["previous"]["some_date"], date(2020, 1, 1))
230
- self.assertEqual(args[1]["previous"]["daterange_embedded.start"], date(2020, 1, 1))
231
- self.assertEqual(args[1]["previous"]["daterange_embedded.end"], date(2020, 12, 31))
232
- self.assertEqual(args[1]["previous"]["some_list"], ["some", "list"])
233
- self.assertEqual(args[1]["previous"]["embedded_list.1.name"], "fake_embedded_1")
227
+ self.assertEqual(kwargs["previous"]["name"], "fake")
228
+ self.assertEqual(kwargs["previous"]["tags"], ["some", "tags"])
229
+ self.assertEqual(kwargs["previous"]["some_date"], date(2020, 1, 1))
230
+ self.assertEqual(kwargs["previous"]["daterange_embedded.start"], date(2020, 1, 1))
231
+ self.assertEqual(kwargs["previous"]["daterange_embedded.end"], date(2020, 12, 31))
232
+ self.assertEqual(kwargs["previous"]["some_list"], ["some", "list"])
233
+ self.assertEqual(kwargs["previous"]["embedded_list.1.name"], "fake_embedded_1")
234
234
 
235
235
  with assert_emit(FakeAuditableSubject.on_update, assertions_callback=check_signal_update):
236
236
  fake.name = "different"
udata/tests/test_cors.py CHANGED
@@ -27,7 +27,7 @@ class CorsTest(APITestCase):
27
27
  assert "Access-Control-Allow-Origin" in response.headers
28
28
 
29
29
  # Resource permalink
30
- response = self.get(f"/fr/datasets/r/{dataset.resources[0].id}", headers=cors_headers)
30
+ response = self.get(f"/datasets/r/{dataset.resources[0].id}", headers=cors_headers)
31
31
  assert_status(response, 404) # The route is defined in udata-front
32
32
  assert "Access-Control-Allow-Origin" in response.headers
33
33
 
@@ -2,7 +2,7 @@ from udata.tests.api import PytestOnlyDBTestCase
2
2
 
3
3
 
4
4
  class ParseUrlCommandTest(PytestOnlyDBTestCase):
5
- def test_parse_url(self, cli, requests_mock, caplog) -> None:
5
+ def test_parse_url(self, requests_mock, caplog) -> None:
6
6
  logs = []
7
7
 
8
8
  def mock_echo(message: str) -> None:
@@ -15,7 +15,7 @@ class ParseUrlCommandTest(PytestOnlyDBTestCase):
15
15
  requests_mock.get(mock_url, text=test_rdf_file.read())
16
16
  requests_mock.head(mock_url, text="sig.oreme.rdf")
17
17
  dataset_id = "0437a976-cff1-4fa6-807a-c23006df2f8f"
18
- result = cli(
18
+ result = self.cli(
19
19
  "dcat",
20
20
  "parse-url",
21
21
  mock_url,
@@ -142,11 +142,11 @@ class DiscussionsTest(APITestCase):
142
142
  with assert_not_emit(on_new_discussion):
143
143
  discussion_id = None
144
144
 
145
- def check_signal(args):
145
+ def check_signal(kwargs):
146
146
  self.assertIsNotNone(discussion_id)
147
147
  self.assertIn(
148
- f"https://data.gouv.fr/datasets/{dataset.slug}/discussions/?discussion_id={discussion_id}",
149
- args[1]["message"],
148
+ f"https://data.gouv.fr/datasets/{dataset.slug}/discussions?discussion_id={discussion_id}",
149
+ kwargs["message"],
150
150
  )
151
151
 
152
152
  with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
@@ -620,8 +620,8 @@ class DiscussionsTest(APITestCase):
620
620
  self.login()
621
621
  with assert_not_emit(on_new_discussion_comment):
622
622
 
623
- def check_signal(args):
624
- self.assertIn(discussion.url_for(), args[1]["message"])
623
+ def check_signal(kwargs):
624
+ self.assertIn(discussion.url_for(), kwargs["message"])
625
625
 
626
626
  with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
627
627
  response = self.post(