udata 10.9.1.dev37462__py2.py3-none-any.whl → 10.9.1.dev37604__py2.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 (45) hide show
  1. udata/api/__init__.py +0 -1
  2. udata/core/dataset/api.py +1 -1
  3. udata/core/dataset/search.py +5 -2
  4. udata/core/dataset/tasks.py +2 -5
  5. udata/core/reuse/tasks.py +3 -0
  6. udata/core/topic/__init__.py +1 -0
  7. udata/core/topic/api_fields.py +87 -0
  8. udata/core/topic/apiv2.py +116 -194
  9. udata/core/topic/factories.py +69 -8
  10. udata/core/topic/forms.py +58 -4
  11. udata/core/topic/models.py +65 -20
  12. udata/core/topic/parsers.py +40 -0
  13. udata/core/topic/tasks.py +11 -0
  14. udata/forms/fields.py +8 -1
  15. udata/harvest/backends/dcat.py +41 -20
  16. udata/harvest/tests/test_dcat_backend.py +89 -0
  17. udata/migrations/2025-05-26-migrate-topics-to-elements.py +59 -0
  18. udata/migrations/2025-06-02-delete-topic-name-index.py +19 -0
  19. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.822f6ccb39c92c796d13.js} +3 -3
  20. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.822f6ccb39c92c796d13.js.map} +1 -1
  21. udata/static/chunks/{13.f29411b06be1883356a3.js → 13.d9c1735d14038b94c17e.js} +2 -2
  22. udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.d9c1735d14038b94c17e.js.map} +1 -1
  23. udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.81c57c0dedf812e43013.js} +2 -2
  24. udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.81c57c0dedf812e43013.js.map} +1 -1
  25. udata/static/chunks/{8.b966402f5d680d4bdf4a.js → 8.0f42630e6d8ff782928e.js} +2 -2
  26. udata/static/chunks/{8.b966402f5d680d4bdf4a.js.map → 8.0f42630e6d8ff782928e.js.map} +1 -1
  27. udata/static/common.js +1 -1
  28. udata/static/common.js.map +1 -1
  29. udata/tasks.py +1 -0
  30. udata/tests/api/test_datasets_api.py +3 -2
  31. udata/tests/apiv2/test_me_api.py +2 -2
  32. udata/tests/apiv2/test_topics.py +457 -127
  33. udata/tests/dataset/test_dataset_tasks.py +7 -2
  34. udata/tests/reuse/test_reuse_task.py +9 -0
  35. udata/tests/search/test_adapter.py +43 -0
  36. udata/tests/test_topics.py +19 -8
  37. udata/tests/topic/test_topic_tasks.py +27 -0
  38. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/METADATA +4 -2
  39. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/RECORD +43 -40
  40. udata/core/topic/api.py +0 -145
  41. udata/tests/api/test_topics_api.py +0 -284
  42. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/LICENSE +0 -0
  43. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/WHEEL +0 -0
  44. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/entry_points.txt +0 -0
  45. {udata-10.9.1.dev37462.dist-info → udata-10.9.1.dev37604.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,29 @@
1
+ import pytest
1
2
  from flask import url_for
2
3
 
3
4
  from udata.core.dataset.factories import DatasetFactory
5
+ from udata.core.discussions.models import Discussion
4
6
  from udata.core.organization.factories import OrganizationFactory
7
+ from udata.core.organization.models import Member
5
8
  from udata.core.reuse.factories import ReuseFactory
6
9
  from udata.core.spatial.factories import SpatialCoverageFactory
7
- from udata.core.topic.factories import TopicFactory
10
+ from udata.core.spatial.models import spatial_granularities
11
+ from udata.core.topic import DEFAULT_PAGE_SIZE
12
+ from udata.core.topic.factories import (
13
+ TopicElementDatasetFactory,
14
+ TopicElementFactory,
15
+ TopicElementReuseFactory,
16
+ TopicFactory,
17
+ TopicWithElementsFactory,
18
+ )
19
+ from udata.core.topic.models import Topic, TopicElement
8
20
  from udata.core.user.factories import UserFactory
9
21
  from udata.tests.api import APITestCase
22
+ from udata.tests.api.test_datasets_api import SAMPLE_GEOM
10
23
  from udata.tests.features.territories import create_geozones_fixtures
11
24
 
12
25
 
13
- class TopicsAPITest(APITestCase):
26
+ class TopicsListAPITest(APITestCase):
14
27
  modules = []
15
28
 
16
29
  def test_topic_api_list(self):
@@ -21,7 +34,6 @@ class TopicsAPITest(APITestCase):
21
34
 
22
35
  tag_topic_1 = TopicFactory(tags=["my-tag-shared", "my-tag-1"])
23
36
  tag_topic_2 = TopicFactory(tags=["my-tag-shared", "my-tag-2"])
24
- name_topic = TopicFactory(name="topic-for-query")
25
37
  private_topic = TopicFactory(private=True)
26
38
  geozone_topic = TopicFactory(spatial=SpatialCoverageFactory(zones=[paca.id]))
27
39
  granularity_topic = TopicFactory(spatial=SpatialCoverageFactory(granularity="country"))
@@ -32,16 +44,10 @@ class TopicsAPITest(APITestCase):
32
44
  response = self.get(url_for("apiv2.topics_list"))
33
45
  assert response.status_code == 200
34
46
  data = response.json["data"]
35
- assert len(data) == 8
47
+ assert len(data) == 7
36
48
 
37
49
  hateoas_fields = ["rel", "href", "type", "total"]
38
- assert all(k in data[0]["datasets"] for k in hateoas_fields)
39
- assert all(k in data[0]["reuses"] for k in hateoas_fields)
40
-
41
- response = self.get(url_for("apiv2.topics_list", q="topic-for"))
42
- assert response.status_code == 200
43
- assert len(response.json["data"]) == 1
44
- assert response.json["data"][0]["id"] == str(name_topic.id)
50
+ assert all(k in data[0]["elements"] for k in hateoas_fields)
45
51
 
46
52
  response = self.get(url_for("apiv2.topics_list", tag=["my-tag-shared", "my-tag-1"]))
47
53
  assert response.status_code == 200
@@ -56,53 +62,94 @@ class TopicsAPITest(APITestCase):
56
62
  set([t["id"] for t in response.json["data"]]),
57
63
  )
58
64
 
59
- response = self.get(url_for("api.topics", include_private="true"))
65
+ response = self.get(url_for("apiv2.topics_list", include_private="true"))
60
66
  assert response.status_code == 200
61
- assert len(response.json["data"]) == 8
67
+ assert len(response.json["data"]) == 7
62
68
  # we're not logged in, so the private topic does not appear
63
69
  assert str(private_topic.id) not in [t["id"] for t in response.json["data"]]
64
70
 
65
- response = self.get(url_for("api.topics", geozone=paca.id))
71
+ response = self.get(url_for("apiv2.topics_list", geozone=paca.id))
66
72
  assert response.status_code == 200
67
73
  assert len(response.json["data"]) == 1
68
74
  assert str(geozone_topic.id) in [t["id"] for t in response.json["data"]]
69
75
 
70
- response = self.get(url_for("api.topics", granularity="country"))
76
+ response = self.get(url_for("apiv2.topics_list", granularity="country"))
71
77
  assert response.status_code == 200
72
78
  assert len(response.json["data"]) == 1
73
79
  assert str(granularity_topic.id) in [t["id"] for t in response.json["data"]]
74
80
 
75
- response = self.get(url_for("api.topics", featured="true"))
81
+ response = self.get(url_for("apiv2.topics_list", featured="true"))
76
82
  assert response.status_code == 200
77
83
  assert len(response.json["data"]) == 1
78
84
  assert str(featured_topic.id) in [t["id"] for t in response.json["data"]]
79
85
 
80
- response = self.get(url_for("api.topics", featured="false"))
86
+ response = self.get(url_for("apiv2.topics_list", featured="false"))
81
87
  assert response.status_code == 200
82
- assert len(response.json["data"]) == 7
88
+ assert len(response.json["data"]) == 6
83
89
  assert str(featured_topic.id) not in [t["id"] for t in response.json["data"]]
84
90
 
85
- response = self.get(url_for("api.topics", owner=owner.id))
91
+ response = self.get(url_for("apiv2.topics_list", owner=owner.id))
86
92
  assert response.status_code == 200
87
93
  assert len(response.json["data"]) == 1
88
94
  assert str(owner_topic.id) in [t["id"] for t in response.json["data"]]
89
95
 
90
- response = self.get(url_for("api.topics", organization=org.id))
96
+ response = self.get(url_for("apiv2.topics_list", organization=org.id))
91
97
  assert response.status_code == 200
92
98
  assert len(response.json["data"]) == 1
93
99
  assert str(org_topic.id) in [t["id"] for t in response.json["data"]]
94
100
 
101
+ def test_topic_api_list_search(self):
102
+ topic = TopicFactory(name="topic-for-test")
103
+ response = self.get(url_for("apiv2.topics_list", q="topic-for"))
104
+ assert response.status_code == 200
105
+ assert len(response.json["data"]) == 1
106
+ assert response.json["data"][0]["id"] == str(topic.id)
107
+
108
+ topic = TopicFactory(name="aménagement")
109
+ response = self.get(url_for("apiv2.topics_list", q="amenagement"))
110
+ assert response.status_code == 200
111
+ assert len(response.json["data"]) == 1
112
+ assert response.json["data"][0]["id"] == str(topic.id)
113
+
114
+ # this is because we have an "AND" clause in TopicApiParser for q
115
+ topic = TopicFactory(name="aménagement sols")
116
+ response = self.get(url_for("apiv2.topics_list", q="amenagement urbain"))
117
+ assert response.status_code == 200
118
+ assert len(response.json["data"]) == 0
119
+
120
+ def test_topic_api_list_search_description(self):
121
+ topic = TopicFactory(name="xxx", description="aménagement")
122
+ response = self.get(url_for("apiv2.topics_list", q="amenagement"))
123
+ assert response.status_code == 200
124
+ assert len(response.json["data"]) == 1
125
+ assert response.json["data"][0]["id"] == str(topic.id)
126
+
127
+ # TODO: this would work with the following index on Topic,
128
+ # but we need to find a way to inject the language from config:
129
+ # meta = {
130
+ # "indexes": [
131
+ # {"fields": ["$name", "$description"], 'default_language': 'fr'}
132
+ # ]
133
+ # }
134
+ @pytest.mark.skip()
135
+ def test_topic_api_list_search_advanced(self):
136
+ topic = TopicFactory(name="plans d'eau")
137
+ response = self.get(url_for("apiv2.topics_list", q="eau"))
138
+ assert response.status_code == 200
139
+ assert len(response.json["data"]) == 1
140
+ assert response.json["data"][0]["id"] == str(topic.id)
141
+
95
142
  def test_topic_api_list_authenticated(self):
96
143
  owner = self.login()
97
144
 
98
145
  private_topic = TopicFactory(private=True)
99
146
  private_topic_owner = TopicFactory(private=True, owner=owner)
100
147
 
101
- response = self.get(url_for("api.topics"))
148
+ response = self.get(url_for("apiv2.topics_list"))
102
149
  assert response.status_code == 200
103
150
  assert len(response.json["data"]) == 0
104
151
 
105
- response = self.get(url_for("api.topics", include_private="true"))
152
+ response = self.get(url_for("apiv2.topics_list", include_private="true"))
106
153
  assert response.status_code == 200
107
154
  assert len(response.json["data"]) == 1
108
155
  assert str(private_topic.id) not in [t["id"] for t in response.json["data"]]
@@ -110,7 +157,7 @@ class TopicsAPITest(APITestCase):
110
157
 
111
158
  def test_topic_api_get(self):
112
159
  """It should fetch a topic from the API"""
113
- topic = TopicFactory()
160
+ topic = TopicWithElementsFactory()
114
161
  topic_response = self.get(url_for("apiv2.topic", topic=topic))
115
162
  assert topic_response.status_code == 200
116
163
  assert "spatial" in topic_response.json
@@ -118,158 +165,441 @@ class TopicsAPITest(APITestCase):
118
165
  assert topic_response.json["created_at"] is not None
119
166
  assert topic_response.json["last_modified"] is not None
120
167
 
121
- response = self.get(topic_response.json["datasets"]["href"])
122
- data = response.json
123
- assert all(str(d.id) in (_d["id"] for _d in data["data"]) for d in topic.datasets)
124
-
125
- response = self.get(topic_response.json["reuses"]["href"])
168
+ response = self.get(topic_response.json["elements"]["href"])
126
169
  data = response.json
127
- assert all(str(r.id) in (_r["id"] for _r in data["data"]) for r in topic.reuses)
170
+ assert all(
171
+ str(elt.id) in (_elt["id"] for _elt in data["data"])
172
+ for elt in TopicElement.objects(topic=topic)
173
+ )
128
174
 
175
+ def test_topic_api_create(self):
176
+ """It should create a topic from the API"""
177
+ data = TopicWithElementsFactory.as_payload()
178
+ self.login()
179
+ response = self.post(url_for("apiv2.topics_list"), data)
180
+ self.assert201(response)
181
+ self.assertEqual(Topic.objects.count(), 1)
182
+ topic = Topic.objects.first()
183
+ for element in data["elements"]:
184
+ assert element["element"]["id"] in (
185
+ str(elt.element.id) for elt in TopicElement.objects(topic=topic)
186
+ )
187
+
188
+ def test_topic_api_create_as_org(self):
189
+ """It should create a topic as organization from the API"""
190
+ data = TopicWithElementsFactory.as_payload()
191
+ user = self.login()
192
+ member = Member(user=user, role="editor")
193
+ org = OrganizationFactory(members=[member])
194
+ data["organization"] = str(org.id)
195
+ response = self.post(url_for("apiv2.topics_list"), data)
196
+ self.assert201(response)
197
+ self.assertEqual(Topic.objects.count(), 1)
198
+
199
+ topic = Topic.objects.first()
200
+ assert topic.owner is None
201
+ assert topic.organization == org
202
+
203
+ def test_topic_api_create_spatial_zone(self):
204
+ paca, _, _ = create_geozones_fixtures()
205
+ granularity = spatial_granularities[0][0]
206
+ data = TopicWithElementsFactory.as_payload()
207
+ data["spatial"] = {
208
+ "zones": [paca.id],
209
+ "granularity": granularity,
210
+ }
211
+ self.login()
212
+ response = self.post(url_for("apiv2.topics_list"), data)
213
+ self.assert201(response)
214
+ self.assertEqual(Topic.objects.count(), 1)
215
+ topic = Topic.objects.first()
216
+ self.assertEqual([str(z) for z in topic.spatial.zones], [paca.id])
217
+ self.assertEqual(topic.spatial.granularity, granularity)
218
+
219
+ def test_topic_api_create_spatial_geom(self):
220
+ granularity = spatial_granularities[0][0]
221
+ data = TopicWithElementsFactory.as_payload()
222
+ data["spatial"] = {
223
+ "geom": SAMPLE_GEOM,
224
+ "granularity": granularity,
225
+ }
226
+ self.login()
227
+ response = self.post(url_for("apiv2.topics_list"), data)
228
+ self.assert201(response)
229
+ self.assertEqual(Topic.objects.count(), 1)
230
+ topic = Topic.objects.first()
231
+ self.assertEqual(topic.spatial.geom, SAMPLE_GEOM)
232
+ self.assertEqual(topic.spatial.granularity, granularity)
129
233
 
130
- class TopicDatasetsAPITest(APITestCase):
131
- def test_list(self):
132
- topic = TopicFactory()
133
- response = self.get(url_for("apiv2.topic_datasets", topic=topic))
134
- assert response.status_code == 200
135
- data = response.json["data"]
136
- assert len(data) == 3
137
- assert all(str(d.id) in (_d["id"] for _d in data) for d in topic.datasets)
138
234
 
139
- def test_add_datasets(self):
235
+ class TopicAPITest(APITestCase):
236
+ def test_topic_api_update(self):
237
+ """It should update a topic from the API"""
140
238
  owner = self.login()
141
239
  topic = TopicFactory(owner=owner)
142
- d1, d2 = DatasetFactory.create_batch(2)
143
- response = self.post(
144
- url_for("apiv2.topic_datasets", topic=topic), [{"id": d1.id}, {"id": d2.id}]
240
+ data = topic.to_dict()
241
+ data["description"] = "new description"
242
+ response = self.put(url_for("apiv2.topic", topic=topic), data)
243
+ self.assert200(response)
244
+ self.assertEqual(Topic.objects.count(), 1)
245
+ topic = Topic.objects.first()
246
+ self.assertEqual(topic.description, "new description")
247
+ self.assertGreater(topic.last_modified, topic.created_at)
248
+
249
+ def test_topic_api_update_perm(self):
250
+ """It should not update a topic from the API"""
251
+ owner = UserFactory()
252
+ topic = TopicFactory(owner=owner)
253
+ user = self.login()
254
+ data = topic.to_dict()
255
+ data["owner"] = user.to_dict()
256
+ response = self.put(url_for("apiv2.topic", topic=topic), data)
257
+ self.assert403(response)
258
+
259
+ def test_topic_api_update_with_elements(self):
260
+ """It should update a topic from the API with elements parameters"""
261
+ user = self.login()
262
+ topic = TopicFactory(owner=user)
263
+ initial_length = len(topic.elements)
264
+ data = topic.to_dict()
265
+ data["elements"] = [
266
+ TopicElementFactory.element_as_payload(elt.fetch()) for elt in topic.elements
267
+ ]
268
+ data["elements"].append(
269
+ TopicElementFactory.element_as_payload(TopicElementDatasetFactory())
145
270
  )
146
- assert response.status_code == 201
271
+ response = self.put(url_for("apiv2.topic", topic=topic), data)
272
+ self.assert200(response)
147
273
  topic.reload()
148
- assert len(topic.datasets) == 5
149
- assert all(d.id in (_d.id for _d in topic.datasets) for d in (d1, d2))
274
+ self.assertEqual(len(topic.elements), initial_length + 1)
150
275
 
151
- def test_add_datasets_double(self):
276
+ def test_topic_api_delete(self):
277
+ """It should delete a topic from the API"""
152
278
  owner = self.login()
153
279
  topic = TopicFactory(owner=owner)
154
- dataset = DatasetFactory()
155
- response = self.post(
156
- url_for("apiv2.topic_datasets", topic=topic), [{"id": dataset.id}, {"id": dataset.id}]
280
+
281
+ with self.api_user():
282
+ response = self.post(
283
+ url_for("api.discussions"),
284
+ {
285
+ "title": "test title",
286
+ "comment": "bla bla",
287
+ "subject": {
288
+ "class": "Topic",
289
+ "id": topic.id,
290
+ },
291
+ },
292
+ )
293
+ self.assert201(response)
294
+
295
+ discussions = Discussion.objects(subject=topic)
296
+ self.assertEqual(len(discussions), 1)
297
+
298
+ with self.api_user():
299
+ response = self.delete(url_for("apiv2.topic", topic=topic))
300
+ self.assertStatus(response, 204)
301
+
302
+ self.assertEqual(Topic.objects.count(), 0)
303
+ self.assertEqual(Discussion.objects.count(), 0)
304
+
305
+ def test_topic_api_delete_perm(self):
306
+ """It should not delete a topic from the API"""
307
+ owner = UserFactory()
308
+ topic = TopicFactory(owner=owner)
309
+ with self.api_user():
310
+ response = self.delete(url_for("apiv2.topic", topic=topic))
311
+ self.assertStatus(response, 403)
312
+
313
+
314
+ class TopicElementsAPITest(APITestCase):
315
+ def test_elements_list(self):
316
+ topic = TopicFactory()
317
+ reuse_elt = TopicElementReuseFactory(
318
+ topic=topic, tags=["foo", "bar"], extras={"foo": "bar"}
157
319
  )
158
- assert response.status_code == 201
159
- topic.reload()
160
- assert len(topic.datasets) == 4
161
- response = self.post(url_for("apiv2.topic_datasets", topic=topic), [{"id": dataset.id}])
162
- assert response.status_code == 201
163
- topic.reload()
164
- assert len(topic.datasets) == 4
320
+ dataset_elt = TopicElementDatasetFactory(
321
+ topic=topic, tags=["foo", "bar"], extras={"foo": "bar"}
322
+ )
323
+ TopicElementFactory(topic=topic, tags=["foo", "bar"], extras={"foo": "bar"})
324
+ response = self.get(url_for("apiv2.topic_elements", topic=topic))
325
+ assert response.status_code == 200
326
+ data = response.json["data"]
327
+ assert len(data) == 3
328
+ assert all(_elt["tags"] == ["foo", "bar"] for _elt in data)
329
+ assert all(_elt["extras"] == {"foo": "bar"} for _elt in data)
330
+ assert {"class": "Reuse", "id": str(reuse_elt.element.id)} in [
331
+ _elt["element"] for _elt in data
332
+ ]
333
+ assert {"class": "Dataset", "id": str(dataset_elt.element.id)} in [
334
+ _elt["element"] for _elt in data
335
+ ]
336
+ no_elt = next(_elt for _elt in data if not _elt["element"])
337
+ assert no_elt["element"] is None
338
+
339
+ def test_elements_list_pagination(self):
340
+ topic = TopicFactory()
341
+ for _ in range(DEFAULT_PAGE_SIZE + 1):
342
+ TopicElementFactory(topic=topic)
343
+ response = self.get(url_for("apiv2.topic_elements", topic=topic))
344
+ assert response.status_code == 200
345
+ assert response.json["next_page"] is not None
346
+ first_page_ids = [elt["id"] for elt in response.json["data"]]
347
+ response = self.get(response.json["next_page"])
348
+ assert response.status_code == 200
349
+ assert response.json["next_page"] is None
350
+ assert response.json["data"][0]["id"] not in first_page_ids
165
351
 
166
- def test_add_datasets_perm(self):
167
- user = UserFactory()
168
- topic = TopicFactory(owner=user)
169
- dataset = DatasetFactory()
170
- self.login()
171
- response = self.post(url_for("apiv2.topic_datasets", topic=topic), [{"id": dataset.id}])
172
- assert response.status_code == 403
352
+ def test_elements_list_search(self):
353
+ topic = TopicFactory()
354
+ matches_1 = [
355
+ TopicElementFactory(topic=topic, title="Apprentissage automatique et algorithmes"),
356
+ TopicElementFactory(
357
+ topic=topic,
358
+ description="Ceci concerne l'apprentissage automatique et les algorithmes d'intelligence artificielle",
359
+ ),
360
+ TopicElementFactory(topic=topic, title="algorithmes d'apprentissage"),
361
+ ]
362
+ # Diacritics test
363
+ TopicElementFactory(topic=topic, title="Système de données")
364
+ TopicElementFactory(topic=topic, description="Création d'un modèle")
365
+
366
+ # Create non-matching elements
367
+ TopicElementFactory(topic=topic, title="ne devrait pas apparaître")
368
+ TopicElementFactory(topic=topic, description="contenu non pertinent")
369
+ TopicElementFactory(topic=topic, title="appr algo") # Partial words that regex might catch
370
+
371
+ # Test with French phrase
372
+ response = self.get(
373
+ url_for("apiv2.topic_elements", topic=topic, q="apprentissage algorithmes")
374
+ )
375
+ assert response.status_code == 200
376
+ assert response.json["total"] == 3
377
+ assert all(elt["id"] in [str(m.id) for m in matches_1] for elt in response.json["data"])
173
378
 
174
- def test_add_datasets_wrong_payload(self):
175
- owner = self.login()
176
- topic = TopicFactory(owner=owner)
177
- response = self.post(url_for("apiv2.topic_datasets", topic=topic), [{"id": "xxx"}])
178
- assert response.status_code == 400
179
- response = self.post(url_for("apiv2.topic_datasets", topic=topic), [{"nain": "portekoi"}])
180
- assert response.status_code == 400
181
- response = self.post(url_for("apiv2.topic_datasets", topic=topic), {"non": "mais"})
182
- assert response.status_code == 400
379
+ # Test diacritics - search without accents should match content with accents
380
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, q="systeme donnees"))
381
+ assert response.status_code == 200
382
+ assert response.json["total"] >= 1
183
383
 
384
+ # Test reverse diacritics - search with accents should work
385
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, q="création modèle"))
386
+ assert response.status_code == 200
387
+ assert response.json["total"] >= 1
184
388
 
185
- class TopicDatasetAPITest(APITestCase):
186
- def test_delete_dataset(self):
187
- owner = self.login()
188
- topic = TopicFactory(owner=owner)
189
- dataset = topic.datasets[0]
190
- response = self.delete(url_for("apiv2.topic_dataset", topic=topic, dataset=dataset))
191
- assert response.status_code == 204
192
- topic.reload()
193
- assert len(topic.datasets) == 2
194
- assert dataset.id not in (d.id for d in topic.datasets)
389
+ def test_elements_list_class_filter(self):
390
+ topic = TopicFactory()
391
+ dataset_elt = TopicElementDatasetFactory(topic=topic)
392
+ reuse_elt = TopicElementReuseFactory(topic=topic)
393
+ no_elt = TopicElementFactory(topic=topic)
195
394
 
196
- def test_delete_dataset_perm(self):
197
- topic = TopicFactory(owner=UserFactory())
198
- dataset = topic.datasets[0]
199
- self.login()
200
- response = self.delete(url_for("apiv2.topic_dataset", topic=topic, dataset=dataset))
201
- assert response.status_code == 403
395
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, **{"class": "Dataset"}))
396
+ assert response.status_code == 200
397
+ assert response.json["total"] == 1
398
+ assert str(dataset_elt.id) == response.json["data"][0]["id"]
202
399
 
400
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, **{"class": "Reuse"}))
401
+ assert response.status_code == 200
402
+ assert response.json["total"] == 1
403
+ assert str(reuse_elt.id) == response.json["data"][0]["id"]
404
+
405
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, **{"class": "None"}))
406
+ assert response.status_code == 200
407
+ assert response.json["total"] == 1
408
+ assert str(no_elt.id) == response.json["data"][0]["id"]
409
+
410
+ response = self.get(url_for("apiv2.topic_elements", topic=topic, **{"class": "NotAModel"}))
411
+ assert response.status_code == 200
412
+ assert response.json["total"] == 0
203
413
 
204
- class TopicReusesAPITest(APITestCase):
205
- def test_list(self):
414
+ def test_elements_list_tags_filter(self):
206
415
  topic = TopicFactory()
207
- response = self.get(url_for("apiv2.topic_reuses", topic=topic))
416
+ match_tag = TopicElementFactory(topic=topic, tags=["is-a-match-1", "is-a-match-2"])
417
+ match_tag_2 = TopicElementFactory(topic=topic, tags=["is-a-match-2"])
418
+ no_match_tag = TopicElementFactory(topic=topic, tags=["is-not-a-match"])
419
+ response = self.get(
420
+ url_for("apiv2.topic_elements", topic=topic, tag=["is-a-match-1", "is-a-match-2"])
421
+ )
208
422
  assert response.status_code == 200
209
- data = response.json["data"]
210
- assert len(data) == 3
211
- assert all(str(r.id) in (_r["id"] for _r in data) for r in topic.reuses)
423
+ assert response.json["total"] == 1
424
+ assert str(match_tag.id) in [elt["id"] for elt in response.json["data"]]
425
+ assert str(no_match_tag.id) not in [elt["id"] for elt in response.json["data"]]
426
+ assert str(match_tag_2.id) not in [elt["id"] for elt in response.json["data"]]
212
427
 
213
- def test_add_reuses(self):
428
+ def test_add_elements(self):
214
429
  owner = self.login()
215
- topic = TopicFactory(owner=owner)
216
- r1, r2 = ReuseFactory.create_batch(2)
430
+ topic = TopicWithElementsFactory(owner=owner)
431
+ dataset = DatasetFactory()
432
+ reuse = ReuseFactory()
217
433
  response = self.post(
218
- url_for("apiv2.topic_reuses", topic=topic), [{"id": r1.id}, {"id": r2.id}]
434
+ url_for("apiv2.topic_elements", topic=topic),
435
+ [
436
+ {
437
+ "title": "A dataset",
438
+ "description": "A dataset description",
439
+ "tags": ["tag1", "tag2"],
440
+ "extras": {"extra": "value"},
441
+ "element": {"class": "Dataset", "id": dataset.id},
442
+ },
443
+ {
444
+ "title": "A reuse",
445
+ "description": "A reuse description",
446
+ "tags": ["tag1", "tag2"],
447
+ "extras": {"extra": "value"},
448
+ "element": {"class": "Reuse", "id": reuse.id},
449
+ },
450
+ {
451
+ "title": "An element without element",
452
+ "description": "An element description",
453
+ "tags": ["tag1", "tag2"],
454
+ "extras": {"extra": "value"},
455
+ "element": None,
456
+ },
457
+ ],
219
458
  )
220
459
  assert response.status_code == 201
221
460
  topic.reload()
222
- assert len(topic.reuses) == 5
223
- assert all(r.id in (_r.id for _r in topic.reuses) for r in (r1, r2))
461
+ assert len(topic.elements) == 6
462
+
463
+ dataset_elt = next(
464
+ elt for elt in topic.elements if elt.element and elt.element.id == dataset.id
465
+ )
466
+ assert dataset_elt.title == "A dataset"
467
+ assert dataset_elt.description == "A dataset description"
468
+ assert dataset_elt.tags == ["tag1", "tag2"]
469
+ assert dataset_elt.extras == {"extra": "value"}
470
+
471
+ reuse_elt = next(
472
+ elt for elt in topic.elements if elt.element and elt.element.id == reuse.id
473
+ )
474
+ assert reuse_elt.title == "A reuse"
475
+ assert reuse_elt.description == "A reuse description"
476
+ assert reuse_elt.tags == ["tag1", "tag2"]
477
+ assert reuse_elt.extras == {"extra": "value"}
478
+
479
+ no_elt_elt = next(
480
+ elt for elt in topic.elements if elt.title == "An element without element"
481
+ )
482
+ assert no_elt_elt.description == "An element description"
483
+ assert no_elt_elt.tags == ["tag1", "tag2"]
484
+ assert no_elt_elt.extras == {"extra": "value"}
485
+ assert no_elt_elt.element is None
224
486
 
225
- def test_add_reuses_double(self):
487
+ def test_add_element_wrong_class(self):
226
488
  owner = self.login()
227
489
  topic = TopicFactory(owner=owner)
228
- reuse = ReuseFactory()
490
+ dataset = DatasetFactory()
229
491
  response = self.post(
230
- url_for("apiv2.topic_reuses", topic=topic), [{"id": reuse.id}, {"id": reuse.id}]
492
+ url_for("apiv2.topic_elements", topic=topic),
493
+ [{"element": {"class": "Reuse", "id": dataset.id}}],
231
494
  )
232
- assert response.status_code == 201
233
- topic.reload()
234
- assert len(topic.reuses) == 4
235
- response = self.post(url_for("apiv2.topic_reuses", topic=topic), [{"id": reuse.id}])
236
- assert response.status_code == 201
237
- topic.reload()
238
- assert len(topic.reuses) == 4
495
+ assert response.status_code == 400
496
+ assert "n'existe pas" in response.json["errors"][0]["element"][0]
239
497
 
240
- def test_add_reuses_perm(self):
498
+ def test_add_empty_element(self):
499
+ owner = self.login()
500
+ topic = TopicFactory(owner=owner)
501
+ response = self.post(url_for("apiv2.topic_elements", topic=topic), [{}])
502
+ assert response.status_code == 400
503
+ assert (
504
+ response.json["errors"][0]["element"][0]
505
+ == "A topic element must have a title or an element."
506
+ )
507
+
508
+ def test_add_datasets_perm(self):
241
509
  user = UserFactory()
242
510
  topic = TopicFactory(owner=user)
243
- reuse = ReuseFactory()
511
+ dataset = DatasetFactory()
244
512
  self.login()
245
- response = self.post(url_for("apiv2.topic_reuses", topic=topic), [{"id": reuse.id}])
513
+ response = self.post(url_for("apiv2.topic_elements", topic=topic), [{"id": dataset.id}])
246
514
  assert response.status_code == 403
247
515
 
248
- def test_add_reuses_wrong_payload(self):
516
+ def test_add_datasets_wrong_payload(self):
249
517
  owner = self.login()
250
518
  topic = TopicFactory(owner=owner)
251
- response = self.post(url_for("apiv2.topic_reuses", topic=topic), [{"id": "xxx"}])
519
+ response = self.post(url_for("apiv2.topic_elements", topic=topic), [{"id": "xxx"}])
252
520
  assert response.status_code == 400
253
- response = self.post(url_for("apiv2.topic_reuses", topic=topic), [{"nain": "portekoi"}])
521
+ response = self.post(url_for("apiv2.topic_elements", topic=topic), [{"nain": "portekoi"}])
254
522
  assert response.status_code == 400
255
- response = self.post(url_for("apiv2.topic_reuses", topic=topic), {"non": "mais"})
523
+ response = self.post(url_for("apiv2.topic_elements", topic=topic), {"non": "mais"})
256
524
  assert response.status_code == 400
257
525
 
526
+ def test_clear_elements(self):
527
+ """It should remove all elements from a Topic"""
528
+ owner = self.login()
529
+ topic = TopicWithElementsFactory(owner=owner)
530
+ self.assertGreater(len(topic.elements), 0)
531
+ response = self.delete(url_for("apiv2.topic_elements", topic=topic))
532
+ self.assert204(response)
533
+ topic.reload()
534
+ self.assertEqual(len(topic.elements), 0)
258
535
 
259
- class TopicReuseAPITest(APITestCase):
260
- def test_delete_reuse(self):
536
+
537
+ class TopicElementAPITest(APITestCase):
538
+ def test_delete_element(self):
261
539
  owner = self.login()
262
- topic = TopicFactory(owner=owner)
263
- reuse = topic.reuses[0]
264
- response = self.delete(url_for("apiv2.topic_reuse", topic=topic, reuse=reuse))
540
+ topic = TopicWithElementsFactory(owner=owner)
541
+ element = topic.elements[0]
542
+ response = self.delete(url_for("apiv2.topic_element", topic=topic, element_id=element.id))
265
543
  assert response.status_code == 204
266
544
  topic.reload()
267
- assert len(topic.reuses) == 2
268
- assert reuse.id not in (d.id for d in topic.reuses)
545
+ assert len(topic.elements) == 2
546
+ assert element.id not in (elt.id for elt in topic.elements)
269
547
 
270
- def test_delete_reuse_perm(self):
271
- topic = TopicFactory(owner=UserFactory())
272
- reuse = topic.reuses[0]
548
+ def test_delete_element_perm(self):
549
+ topic = TopicWithElementsFactory(owner=UserFactory())
550
+ element = topic.elements[0]
273
551
  self.login()
274
- response = self.delete(url_for("apiv2.topic_reuse", topic=topic, reuse=reuse))
552
+ response = self.delete(url_for("apiv2.topic_element", topic=topic, element_id=element.id))
275
553
  assert response.status_code == 403
554
+
555
+ def test_update_element(self):
556
+ owner = self.login()
557
+ topic = TopicFactory(owner=owner)
558
+ dataset = DatasetFactory()
559
+ element = TopicElementFactory(topic=topic, title="foo")
560
+ response = self.put(
561
+ url_for("apiv2.topic_element", topic=topic, element_id=element.id),
562
+ {
563
+ "title": "bar",
564
+ "description": "baz",
565
+ "tags": ["baz"],
566
+ "extras": {"foo": "bar"},
567
+ "element": {"class": "Dataset", "id": str(dataset.id)},
568
+ },
569
+ )
570
+ assert response.status_code == 200
571
+ assert response.json["title"] == "bar"
572
+ topic.reload()
573
+ assert len(topic.elements) == 1
574
+ element = topic.elements.first()
575
+ assert element.title == "bar"
576
+ assert element.description == "baz"
577
+ assert element.tags == ["baz"]
578
+ assert element.extras == {"foo": "bar"}
579
+ assert element.element.id == dataset.id
580
+
581
+ def test_update_element_no_element(self):
582
+ owner = self.login()
583
+ topic = TopicFactory(owner=owner)
584
+ element = TopicElementFactory(topic=topic, title="foo")
585
+ response = self.put(
586
+ url_for("apiv2.topic_element", topic=topic, element_id=element.id),
587
+ {
588
+ "title": "bar",
589
+ "description": "baz",
590
+ "tags": ["baz"],
591
+ "extras": {"foo": "bar"},
592
+ "element": None,
593
+ },
594
+ )
595
+ assert response.status_code == 200
596
+ assert response.json["title"] == "bar"
597
+ assert response.json["element"] is None
598
+ topic.reload()
599
+ assert len(topic.elements) == 1
600
+ element = topic.elements.first()
601
+ assert element.title == "bar"
602
+ assert element.description == "baz"
603
+ assert element.tags == ["baz"]
604
+ assert element.extras == {"foo": "bar"}
605
+ assert element.element is None