udata 10.0.8.dev33570__py2.py3-none-any.whl → 10.0.8.dev33610__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 (31) hide show
  1. udata/api/__init__.py +5 -4
  2. udata/commands/db.py +7 -18
  3. udata/core/dataset/csv.py +2 -1
  4. udata/core/post/api.py +12 -4
  5. udata/core/post/models.py +5 -0
  6. udata/core/post/tests/test_api.py +48 -31
  7. udata/mongo/document.py +14 -0
  8. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.822f6ccb39c92c796d13.js} +3 -3
  9. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.822f6ccb39c92c796d13.js.map} +1 -1
  10. udata/static/chunks/{13.f29411b06be1883356a3.js → 13.d9c1735d14038b94c17e.js} +2 -2
  11. udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.d9c1735d14038b94c17e.js.map} +1 -1
  12. udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.81c57c0dedf812e43013.js} +2 -2
  13. udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.81c57c0dedf812e43013.js.map} +1 -1
  14. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.ba0bb2baa40e899d440b.js} +3 -3
  15. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.ba0bb2baa40e899d440b.js.map} +1 -1
  16. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.0652a860afda96795a53.js} +3 -3
  17. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.0652a860afda96795a53.js.map} +1 -1
  18. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.92d7c2ec6d20005774ef.js} +3 -3
  19. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.92d7c2ec6d20005774ef.js.map} +1 -1
  20. udata/static/chunks/{8.54e44b102164ae5e7a67.js → 8.0f42630e6d8ff782928e.js} +2 -2
  21. udata/static/chunks/{8.54e44b102164ae5e7a67.js.map → 8.0f42630e6d8ff782928e.js.map} +1 -1
  22. udata/static/common.js +1 -1
  23. udata/static/common.js.map +1 -1
  24. udata/tests/features/territories/test_territories_api.py +5 -5
  25. udata/tests/plugin.py +12 -0
  26. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/METADATA +3 -1
  27. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/RECORD +31 -31
  28. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/LICENSE +0 -0
  29. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/WHEEL +0 -0
  30. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/entry_points.txt +0 -0
  31. {udata-10.0.8.dev33570.dist-info → udata-10.0.8.dev33610.dist-info}/top_level.txt +0 -0
udata/api/__init__.py CHANGED
@@ -15,6 +15,7 @@ from flask import (
15
15
  url_for,
16
16
  )
17
17
  from flask_restx import Api, Resource
18
+ from flask_restx.reqparse import RequestParser
18
19
  from flask_storage import UnauthorizedFileType
19
20
 
20
21
  from udata import entrypoints, tracking
@@ -138,7 +139,7 @@ class UDataApi(Api):
138
139
  response.headers["WWW-Authenticate"] = challenge
139
140
  return response
140
141
 
141
- def page_parser(self):
142
+ def page_parser(self) -> RequestParser:
142
143
  parser = self.parser()
143
144
  parser.add_argument("page", type=int, default=1, location="args", help="The page to fetch")
144
145
  parser.add_argument(
@@ -251,9 +252,9 @@ def handle_value_error(error):
251
252
  def handle_unauthorized_file_type(error):
252
253
  """Error occuring when the user try to upload a non-allowed file type"""
253
254
  url = url_for("api.allowed_extensions", _external=True)
254
- msg = (
255
- "This file type is not allowed." "The allowed file type list is available at {url}"
256
- ).format(url=url)
255
+ msg = ("This file type is not allowed.The allowed file type list is available at {url}").format(
256
+ url=url
257
+ )
257
258
  return {"message": msg}, 400
258
259
 
259
260
 
udata/commands/db.py CHANGED
@@ -9,11 +9,8 @@ import mongoengine
9
9
  from bson import DBRef
10
10
 
11
11
  from udata import migrations
12
- from udata import models as core_models
13
- from udata.api import oauth2 as oauth2_models
14
12
  from udata.commands import cli, cyan, echo, green, magenta, red, white, yellow
15
- from udata.harvest import models as harvest_models
16
- from udata.mongo import db
13
+ from udata.mongo.document import get_all_models
17
14
 
18
15
  # Date format used to for display
19
16
  DATE_FORMAT = "%Y-%m-%d %H:%M"
@@ -148,16 +145,8 @@ def check_references(models_to_check):
148
145
 
149
146
  errors = collections.defaultdict(int)
150
147
 
151
- _models = []
152
- for models in core_models, harvest_models, oauth2_models:
153
- _models += [
154
- elt
155
- for _, elt in models.__dict__.items()
156
- if isinstance(elt, type) and issubclass(elt, (db.Document))
157
- ]
158
-
159
148
  references = []
160
- for model in set(_models):
149
+ for model in get_all_models():
161
150
  if model.__name__ == "Activity":
162
151
  print("Skipping Activity model, scheduled for deprecation")
163
152
  continue
@@ -291,7 +280,7 @@ def check_references(models_to_check):
291
280
 
292
281
  print("Those references will be inspected:")
293
282
  for reference in references:
294
- print(f'- {reference["repr"]}({reference["destination"]}) — {reference["type"]}')
283
+ print(f"- {reference['repr']}({reference['destination']}) — {reference['type']}")
295
284
  print("")
296
285
 
297
286
  total = 0
@@ -305,7 +294,7 @@ def check_references(models_to_check):
305
294
  with click.progressbar(qs, length=count) as models:
306
295
  for obj in models:
307
296
  for reference in model_references:
308
- key = f'\t- {reference["repr"]}({reference["destination"]}) — {reference["type"]}…'
297
+ key = f"\t- {reference['repr']}({reference['destination']}) — {reference['type']}…"
309
298
  if key not in errors[model]:
310
299
  errors[model][key] = 0
311
300
 
@@ -316,7 +305,7 @@ def check_references(models_to_check):
316
305
  except mongoengine.errors.DoesNotExist:
317
306
  errors[model][key] += 1
318
307
  print_and_save(
319
- f'\t{model.__name__}#{obj.id} have a broken reference for `{reference["name"]}`'
308
+ f"\t{model.__name__}#{obj.id} have a broken reference for `{reference['name']}`"
320
309
  )
321
310
  elif reference["type"] == "list":
322
311
  attr_list = getattr(obj, reference["name"], [])
@@ -326,7 +315,7 @@ def check_references(models_to_check):
326
315
  if isinstance(sub, DBRef):
327
316
  errors[model][key] += 1
328
317
  print_and_save(
329
- f'\t{model.__name__}#{obj.id} have a broken reference for {reference["name"]}[{i}]'
318
+ f"\t{model.__name__}#{obj.id} have a broken reference for {reference['name']}[{i}]"
330
319
  )
331
320
  elif reference["type"] == "embed_list":
332
321
  p1, p2 = reference["name"].split("__")
@@ -366,7 +355,7 @@ def check_references(models_to_check):
366
355
  f"\t{model.__name__}#{obj.id} have a broken reference for {p1}.{p2}[{i}]"
367
356
  )
368
357
  else:
369
- print_and_save(f'Unknown ref type {reference["type"]}')
358
+ print_and_save(f"Unknown ref type {reference['type']}")
370
359
  except mongoengine.errors.FieldDoesNotExist:
371
360
  print_and_save(
372
361
  f"[ERROR for {model.__name__} {obj.id}] {traceback.format_exc()}"
udata/core/dataset/csv.py CHANGED
@@ -90,6 +90,7 @@ class ResourcesCsvAdapter(csv.NestedAdapter):
90
90
  ("harvest.modified_at", lambda o: o.harvest and o.harvest.modified_at),
91
91
  ("schema_name", "schema.name"),
92
92
  ("schema_version", "schema.version"),
93
- ("preview_url", lambda o: o.preview_url or False),
93
+ ("preview_url", lambda o: o.preview_url or None),
94
+ ("extras", lambda o: o.extras),
94
95
  )
95
96
  attribute = "resources"
udata/core/post/api.py CHANGED
@@ -15,6 +15,8 @@ from udata.core.user.api_fields import user_ref_fields
15
15
  from .forms import PostForm
16
16
  from .models import Post
17
17
 
18
+ DEFAULT_SORTING = "-published"
19
+
18
20
  ns = api.namespace("posts", "Posts related operations")
19
21
 
20
22
  post_fields = api.model(
@@ -58,9 +60,7 @@ post_page_fields = api.model("PostPage", fields.pager(post_fields))
58
60
 
59
61
  parser = api.page_parser()
60
62
 
61
- parser.add_argument(
62
- "sort", type=str, default="-created_at", location="args", help="The sorting attribute"
63
- )
63
+ parser.add_argument("sort", type=str, location="args", help="The sorting attribute")
64
64
  parser.add_argument(
65
65
  "with_drafts",
66
66
  type=bool,
@@ -68,6 +68,9 @@ parser.add_argument(
68
68
  location="args",
69
69
  help="`True` also returns the unpublished posts (only for super-admins)",
70
70
  )
71
+ parser.add_argument(
72
+ "q", type=str, location="args", help="query string to search through resources titles"
73
+ )
71
74
 
72
75
 
73
76
  @ns.route("/", endpoint="posts")
@@ -84,7 +87,12 @@ class PostsAPI(API):
84
87
  if not (AdminPermission().can() and args["with_drafts"]):
85
88
  posts = posts.published()
86
89
 
87
- return posts.order_by(args["sort"]).paginate(args["page"], args["page_size"])
90
+ if args["q"]:
91
+ phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
92
+ posts = posts.search_text(phrase_query)
93
+
94
+ sort = args["sort"] or ("$text_score" if args["q"] else None) or DEFAULT_SORTING
95
+ return posts.order_by(sort).paginate(args["page"], args["page_size"])
88
96
 
89
97
  @api.doc("create_post")
90
98
  @api.secure(admin_permission)
udata/core/post/models.py CHANGED
@@ -42,6 +42,11 @@ class Post(db.Datetimed, db.Document):
42
42
  "indexes": [
43
43
  "-created_at",
44
44
  "-published",
45
+ {
46
+ "fields": ["$name", "$headline", "$content"],
47
+ "default_language": "french",
48
+ "weights": {"name": 10, "headline": 5, "content": 4},
49
+ },
45
50
  ],
46
51
  "queryset_class": PostQuerySet,
47
52
  }
@@ -1,4 +1,3 @@
1
- import pytest
2
1
  from flask import url_for
3
2
 
4
3
  from udata.core.dataset.factories import DatasetFactory
@@ -6,50 +5,68 @@ from udata.core.post.factories import PostFactory
6
5
  from udata.core.post.models import Post
7
6
  from udata.core.reuse.factories import ReuseFactory
8
7
  from udata.core.user.factories import AdminFactory
8
+ from udata.tests.api import APITestCase
9
9
  from udata.tests.helpers import assert200, assert201, assert204
10
10
 
11
11
 
12
- @pytest.mark.usefixtures("clean_db")
13
- class PostsAPITest:
14
- modules = []
15
-
16
- def test_post_api_list(self, api):
12
+ class PostsAPITest(APITestCase):
13
+ def test_post_api_list(self):
17
14
  """It should fetch a post list from the API"""
18
15
  PostFactory.create_batch(3)
19
16
  draft = PostFactory(published=None)
20
17
 
21
- response = api.get(url_for("api.posts"))
18
+ response = self.get(url_for("api.posts"))
22
19
  assert200(response)
23
20
  # Response should not contain the unpublished post
24
21
  assert len(response.json["data"]) == 3
25
22
 
26
- api.login(AdminFactory())
23
+ self.login(AdminFactory())
27
24
 
28
- response = api.get(url_for("api.posts"))
25
+ response = self.get(url_for("api.posts"))
29
26
  assert200(response)
30
27
 
31
28
  assert len(response.json["data"]) == 3
32
29
  assert str(draft.id) not in [post["id"] for post in response.json["data"]]
33
30
 
34
- response = api.get(url_for("api.posts", with_drafts=True))
31
+ response = self.get(url_for("api.posts", with_drafts=True))
35
32
  assert200(response)
36
33
 
37
34
  assert len(response.json["data"]) == 4
38
35
  assert str(draft.id) in [post["id"] for post in response.json["data"]]
39
36
 
40
- def test_post_api_get(self, api):
37
+ def test_search_post(self):
38
+ """It should fetch a post list from the API"""
39
+ name_match = PostFactory(name="Foobar", published="2025-01-01")
40
+ content_match = PostFactory(content="Foobar", published="2025-01-02")
41
+ PostFactory(content="Something else")
42
+
43
+ response = self.get(url_for("api.posts", q="Foobar"))
44
+ assert200(response)
45
+ assert len(response.json["data"]) == 2
46
+
47
+ assert response.json["data"][0]["id"] == str(name_match.id)
48
+ assert response.json["data"][1]["id"] == str(content_match.id)
49
+
50
+ response = self.get(url_for("api.posts", q="Foobar", sort="-published"))
51
+ assert200(response)
52
+ assert len(response.json["data"]) == 2
53
+
54
+ assert response.json["data"][1]["id"] == str(name_match.id)
55
+ assert response.json["data"][0]["id"] == str(content_match.id)
56
+
57
+ def test_post_api_get(self):
41
58
  """It should fetch a post from the API"""
42
59
  post = PostFactory()
43
- response = api.get(url_for("api.post", post=post))
60
+ response = self.get(url_for("api.post", post=post))
44
61
  assert200(response)
45
62
 
46
- def test_post_api_create(self, api):
63
+ def test_post_api_create(self):
47
64
  """It should create a post from the API"""
48
65
  data = PostFactory.as_dict()
49
66
  data["datasets"] = [str(d.id) for d in data["datasets"]]
50
67
  data["reuses"] = [str(r.id) for r in data["reuses"]]
51
- api.login(AdminFactory())
52
- response = api.post(url_for("api.posts"), data)
68
+ self.login(AdminFactory())
69
+ response = self.post(url_for("api.posts"), data)
53
70
  assert201(response)
54
71
  assert Post.objects.count() == 1
55
72
  post = Post.objects.first()
@@ -58,62 +75,62 @@ class PostsAPITest:
58
75
  for reuse, expected in zip(post.reuses, data["reuses"]):
59
76
  assert str(reuse.id) == expected
60
77
 
61
- def test_post_api_update(self, api):
78
+ def test_post_api_update(self):
62
79
  """It should update a post from the API"""
63
80
  post = PostFactory()
64
81
  data = post.to_dict()
65
82
  data["content"] = "new content"
66
- api.login(AdminFactory())
67
- response = api.put(url_for("api.post", post=post), data)
83
+ self.login(AdminFactory())
84
+ response = self.put(url_for("api.post", post=post), data)
68
85
  assert200(response)
69
86
  assert Post.objects.count() == 1
70
87
  assert Post.objects.first().content == "new content"
71
88
 
72
- def test_post_api_update_with_related_dataset_and_reuse(self, api):
89
+ def test_post_api_update_with_related_dataset_and_reuse(self):
73
90
  """It should update a post from the API with related dataset and reuse"""
74
- api.login(AdminFactory())
91
+ self.login(AdminFactory())
75
92
  post = PostFactory()
76
93
  data = post.to_dict()
77
94
  data["content"] = "new content"
78
95
 
79
96
  # Add datasets
80
97
  data["datasets"] = [DatasetFactory().to_dict()]
81
- response = api.put(url_for("api.post", post=post), data)
98
+ response = self.put(url_for("api.post", post=post), data)
82
99
  assert200(response)
83
100
 
84
101
  # Add reuses to the post value returned by the previous api call
85
102
  data = response.json
86
103
  data["reuses"] = [ReuseFactory().to_dict()]
87
- response = api.put(url_for("api.post", post=post), data)
104
+ response = self.put(url_for("api.post", post=post), data)
88
105
  assert200(response)
89
106
 
90
107
  assert len(response.json["datasets"]) == 1
91
108
  assert len(response.json["reuses"]) == 1
92
109
 
93
- def test_post_api_delete(self, api):
110
+ def test_post_api_delete(self):
94
111
  """It should delete a post from the API"""
95
112
  post = PostFactory()
96
- with api.user(AdminFactory()):
97
- response = api.delete(url_for("api.post", post=post))
113
+ self.login(AdminFactory())
114
+ response = self.delete(url_for("api.post", post=post))
98
115
  assert204(response)
99
116
  assert Post.objects.count() == 0
100
117
 
101
- def test_post_api_publish(self, api):
118
+ def test_post_api_publish(self):
102
119
  """It should update a post from the API"""
103
120
  post = PostFactory(published=None)
104
- api.login(AdminFactory())
105
- response = api.post(url_for("api.publish_post", post=post))
121
+ self.login(AdminFactory())
122
+ response = self.post(url_for("api.publish_post", post=post))
106
123
  assert200(response)
107
124
  assert Post.objects.count() == 1
108
125
 
109
126
  post.reload()
110
127
  assert post.published is not None
111
128
 
112
- def test_post_api_unpublish(self, api):
129
+ def test_post_api_unpublish(self):
113
130
  """It should update a post from the API"""
114
131
  post = PostFactory()
115
- api.login(AdminFactory())
116
- response = api.delete(url_for("api.publish_post", post=post))
132
+ self.login(AdminFactory())
133
+ response = self.delete(url_for("api.publish_post", post=post))
117
134
  assert200(response)
118
135
  assert Post.objects.count() == 1
119
136
 
udata/mongo/document.py CHANGED
@@ -19,6 +19,20 @@ def serialize(value):
19
19
  return value
20
20
 
21
21
 
22
+ def get_all_models():
23
+ from udata import models as core_models
24
+ from udata.api import oauth2 as oauth2_models
25
+ from udata.harvest import models as harvest_models
26
+
27
+ all_models = set()
28
+ for models in core_models, harvest_models, oauth2_models:
29
+ for model in models.__dict__.values():
30
+ if isinstance(model, type) and issubclass(model, (UDataDocument)):
31
+ all_models.add(model)
32
+
33
+ return all_models
34
+
35
+
22
36
  class UDataDocument(Document):
23
37
  meta = {
24
38
  "abstract": True,