udata 9.1.4__py2.py3-none-any.whl → 9.1.4.dev30965__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 (112) hide show
  1. tasks/__init__.py +2 -2
  2. udata/__init__.py +1 -1
  3. udata/api/__init__.py +3 -2
  4. udata/api/commands.py +1 -0
  5. udata/api/fields.py +1 -22
  6. udata/api_fields.py +37 -140
  7. udata/app.py +1 -1
  8. udata/auth/__init__.py +12 -8
  9. udata/commands/db.py +3 -3
  10. udata/commands/dcat.py +1 -1
  11. udata/commands/fixtures.py +40 -60
  12. udata/commands/purge.py +1 -2
  13. udata/commands/tests/test_fixtures.py +11 -44
  14. udata/core/activity/api.py +1 -14
  15. udata/core/activity/tasks.py +1 -1
  16. udata/core/badges/models.py +2 -6
  17. udata/core/contact_point/api.py +3 -1
  18. udata/core/dataservices/api.py +1 -37
  19. udata/core/dataservices/models.py +0 -38
  20. udata/core/dataservices/tasks.py +1 -1
  21. udata/core/dataset/events.py +1 -4
  22. udata/core/dataset/forms.py +2 -0
  23. udata/core/dataset/models.py +10 -12
  24. udata/core/dataset/rdf.py +1 -1
  25. udata/core/discussions/api.py +1 -1
  26. udata/core/discussions/models.py +2 -2
  27. udata/core/discussions/tasks.py +1 -1
  28. udata/core/metrics/models.py +1 -4
  29. udata/core/organization/api.py +7 -11
  30. udata/core/organization/api_fields.py +4 -10
  31. udata/core/organization/apiv2.py +1 -1
  32. udata/core/organization/csv.py +0 -1
  33. udata/core/organization/rdf.py +1 -4
  34. udata/core/owned.py +2 -4
  35. udata/core/post/api.py +2 -2
  36. udata/core/reuse/api.py +25 -32
  37. udata/core/reuse/api_fields.py +101 -2
  38. udata/core/reuse/apiv2.py +4 -4
  39. udata/core/reuse/forms.py +45 -0
  40. udata/core/reuse/models.py +16 -98
  41. udata/core/site/api.py +29 -3
  42. udata/core/spatial/commands.py +3 -3
  43. udata/core/spatial/factories.py +1 -1
  44. udata/core/spatial/forms.py +1 -1
  45. udata/core/spatial/models.py +2 -2
  46. udata/core/spatial/tests/test_models.py +1 -1
  47. udata/core/spatial/translations.py +1 -3
  48. udata/core/topic/api.py +2 -2
  49. udata/core/topic/apiv2.py +2 -1
  50. udata/core/user/api.py +8 -28
  51. udata/core/user/metrics.py +1 -1
  52. udata/cors.py +4 -4
  53. udata/features/transfer/api.py +2 -1
  54. udata/harvest/actions.py +1 -1
  55. udata/harvest/backends/__init__.py +1 -1
  56. udata/harvest/tasks.py +1 -0
  57. udata/harvest/tests/factories.py +2 -0
  58. udata/harvest/tests/test_base_backend.py +1 -0
  59. udata/harvest/tests/test_dcat_backend.py +17 -16
  60. udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +1 -1
  61. udata/migrations/2021-07-05-remove-unused-badges.py +1 -0
  62. udata/migrations/2023-02-08-rename-internal-dates.py +2 -0
  63. udata/migrations/2024-06-11-fix-reuse-datasets-references.py +1 -0
  64. udata/mongo/datetime_fields.py +4 -11
  65. udata/mongo/document.py +0 -2
  66. udata/mongo/taglist_field.py +0 -26
  67. udata/search/commands.py +1 -1
  68. udata/search/query.py +1 -1
  69. udata/settings.py +0 -1
  70. udata/static/admin.js +36 -36
  71. udata/static/admin.js.map +1 -1
  72. udata/static/chunks/{12.576e63b7a990f8eab784.js → 12.5b900cac4417e10ef3a0.js} +2 -2
  73. udata/static/chunks/12.5b900cac4417e10ef3a0.js.map +1 -0
  74. udata/static/chunks/{28.1ef31a46255dc2bf56d1.js → 28.1759a7f57d526e6db574.js} +2 -2
  75. udata/static/chunks/28.1759a7f57d526e6db574.js.map +1 -0
  76. udata/static/common.js +1 -1
  77. udata/static/common.js.map +1 -1
  78. udata/tests/api/test_base_api.py +1 -1
  79. udata/tests/api/test_contact_points.py +4 -4
  80. udata/tests/api/test_dataservices_api.py +0 -59
  81. udata/tests/api/test_datasets_api.py +10 -10
  82. udata/tests/api/test_organizations_api.py +39 -39
  83. udata/tests/api/test_reuses_api.py +0 -49
  84. udata/tests/api/test_tags_api.py +4 -4
  85. udata/tests/api/test_transfer_api.py +1 -1
  86. udata/tests/api/test_user_api.py +0 -11
  87. udata/tests/apiv2/test_datasets.py +4 -4
  88. udata/tests/dataset/test_dataset_events.py +0 -28
  89. udata/tests/dataset/test_dataset_model.py +3 -3
  90. udata/tests/frontend/__init__.py +2 -0
  91. udata/tests/frontend/test_auth.py +1 -0
  92. udata/tests/organization/test_csv_adapter.py +2 -0
  93. udata/tests/organization/test_notifications.py +3 -3
  94. udata/tests/organization/test_organization_rdf.py +6 -31
  95. udata/tests/reuse/test_reuse_model.py +1 -0
  96. udata/tests/site/test_site_rdf.py +3 -1
  97. udata/tests/test_cors.py +3 -0
  98. udata/tests/test_owned.py +4 -4
  99. udata/tests/test_routing.py +1 -1
  100. udata/tests/test_tags.py +1 -1
  101. udata/tests/test_transfer.py +2 -1
  102. udata/tests/workers/test_jobs_commands.py +1 -1
  103. udata/utils.py +0 -12
  104. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/METADATA +4 -16
  105. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/RECORD +109 -109
  106. udata/static/chunks/12.576e63b7a990f8eab784.js.map +0 -1
  107. udata/static/chunks/28.1ef31a46255dc2bf56d1.js.map +0 -1
  108. udata/tests/api/test_activities_api.py +0 -69
  109. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/LICENSE +0 -0
  110. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/WHEEL +0 -0
  111. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/entry_points.txt +0 -0
  112. {udata-9.1.4.dist-info → udata-9.1.4.dev30965.dist-info}/top_level.txt +0 -0
tasks/__init__.py CHANGED
@@ -194,10 +194,10 @@ def qa(ctx):
194
194
 
195
195
 
196
196
  @task
197
- def serve(ctx, host="localhost", port="7000"):
197
+ def serve(ctx, host="localhost"):
198
198
  """Run a development server"""
199
199
  with ctx.cd(ROOT):
200
- ctx.run(f"python manage.py serve -d -r -h {host} -p {port}")
200
+ ctx.run("python manage.py serve -d -r -h %s" % host)
201
201
 
202
202
 
203
203
  @task
udata/__init__.py CHANGED
@@ -4,5 +4,5 @@
4
4
  udata
5
5
  """
6
6
 
7
- __version__ = "9.1.4"
7
+ __version__ = "9.1.4.dev"
8
8
  __description__ = "Open data portal"
udata/api/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import itertools
2
3
  import logging
3
4
  import urllib.parse
4
5
  from functools import wraps
@@ -17,7 +18,7 @@ from flask import (
17
18
  from flask_restx import Api, Resource
18
19
  from flask_storage import UnauthorizedFileType
19
20
 
20
- from udata import entrypoints, tracking
21
+ from udata import cors, entrypoints, tracking
21
22
  from udata.app import csrf
22
23
  from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
23
24
  from udata.i18n import get_locale
@@ -324,7 +325,7 @@ def init_app(app):
324
325
  import udata.harvest.api # noqa
325
326
 
326
327
  for module in entrypoints.get_enabled("udata.apis", app).values():
327
- module if inspect.ismodule(module) else import_module(module)
328
+ api_module = module if inspect.ismodule(module) else import_module(module)
328
329
 
329
330
  # api.init_app(app)
330
331
  app.register_blueprint(apiv1_blueprint)
udata/api/commands.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import click
5
5
  from flask import current_app, json
6
6
  from flask_restx import schemas
7
+ from werkzeug.security import gen_salt
7
8
 
8
9
  from udata.api import api
9
10
  from udata.api.oauth2 import OAuth2Client
udata/api/fields.py CHANGED
@@ -4,28 +4,7 @@ import logging
4
4
  import pytz
5
5
  from dateutil.parser import parse
6
6
  from flask import request, url_for
7
-
8
- # Explicitly import all of flask_restx fields so they're available throughout the codebase as api.fields
9
- from flask_restx.fields import Arbitrary as Arbitrary
10
- from flask_restx.fields import Boolean as Boolean
11
- from flask_restx.fields import ClassName as ClassName
12
- from flask_restx.fields import Date as Date
13
- from flask_restx.fields import DateTime as DateTime
14
- from flask_restx.fields import Fixed as Fixed
15
- from flask_restx.fields import Float as Float
16
- from flask_restx.fields import FormattedString as FormattedString
17
- from flask_restx.fields import Integer as Integer
18
- from flask_restx.fields import List as List
19
- from flask_restx.fields import MarshallingError as MarshallingError
20
- from flask_restx.fields import MinMaxMixin as MinMaxMixin
21
- from flask_restx.fields import Nested as Nested
22
- from flask_restx.fields import NumberMixin as NumberMixin
23
- from flask_restx.fields import Polymorph as Polymorph
24
- from flask_restx.fields import Raw as Raw
25
- from flask_restx.fields import String as String
26
- from flask_restx.fields import StringMixin as StringMixin
27
- from flask_restx.fields import Url as Url
28
- from flask_restx.fields import Wildcard as Wildcard
7
+ from flask_restx.fields import * # noqa
29
8
 
30
9
  from udata.uris import endpoint_for
31
10
  from udata.utils import multi_to_dict
udata/api_fields.py CHANGED
@@ -2,10 +2,10 @@ import flask_restx.fields as restx_fields
2
2
  import mongoengine
3
3
  import mongoengine.fields as mongo_fields
4
4
  from bson import ObjectId
5
- from flask_storage.mongo import ImageField as FlaskStorageImageField
6
5
 
7
6
  import udata.api.fields as custom_restx_fields
8
- from udata.api import api, base_reference
7
+ from udata.api import api
8
+ from udata.mongo.engine import db
9
9
  from udata.mongo.errors import FieldValidationError
10
10
 
11
11
  lazy_reference = api.model(
@@ -17,7 +17,7 @@ lazy_reference = api.model(
17
17
  )
18
18
 
19
19
 
20
- def convert_db_to_field(key, field, info):
20
+ def convert_db_to_field(key, field, info={}):
21
21
  """
22
22
  This function maps a Mongo field to a Flask RestX field.
23
23
  Most of the types are a simple 1-to-1 mapping except lists and references that requires
@@ -28,6 +28,8 @@ def convert_db_to_field(key, field, info):
28
28
  params. Since merging the params involve a litte bit of work (merging default params with read/write params and then with
29
29
  user-supplied overrides, setting the readonly flag…), it's easier to have do this one time at the end of the function.
30
30
  """
31
+ info = {**getattr(field, "__additional_field_info__", {}), **info}
32
+
31
33
  params = {}
32
34
  params["required"] = field.required
33
35
 
@@ -43,9 +45,7 @@ def convert_db_to_field(key, field, info):
43
45
  # is always good enough.
44
46
  return info.get("convert_to"), info.get("convert_to")
45
47
  elif isinstance(field, mongo_fields.StringField):
46
- constructor = (
47
- custom_restx_fields.Markdown if info.get("markdown", False) else restx_fields.String
48
- )
48
+ constructor = restx_fields.String
49
49
  params["min_length"] = field.min_length
50
50
  params["max_length"] = field.max_length
51
51
  params["enum"] = field.choices
@@ -61,42 +61,18 @@ def convert_db_to_field(key, field, info):
61
61
  constructor = custom_restx_fields.ISODateTime
62
62
  elif isinstance(field, mongo_fields.DictField):
63
63
  constructor = restx_fields.Raw
64
- elif isinstance(field, mongo_fields.ImageField) or isinstance(field, FlaskStorageImageField):
65
- size = info.get("size", None)
66
- if size:
67
- params["description"] = f"URL of the cropped and squared image ({size}x{size})"
68
- else:
69
- params["description"] = "URL of the image"
70
-
71
- if info.get("is_thumbnail", False):
72
- constructor_read = custom_restx_fields.ImageField
73
- write_params["read_only"] = True
74
- else:
75
- constructor = custom_restx_fields.ImageField
76
-
77
64
  elif isinstance(field, mongo_fields.ListField):
78
65
  # For lists, we convert the inner value from Mongo to RestX then we create
79
66
  # the `List` RestX type with this converted inner value.
80
- # There is three level of information, from most important to least
81
- # 1. `inner_field_info` inside `__additional_field_info__` on the parent
82
- # 2. `__additional_field_info__` of the inner field
83
- # 3. `__additional_field_info__` of the parent
84
- inner_info = getattr(field.field, "__additional_field_info__", {})
85
67
  field_read, field_write = convert_db_to_field(
86
- f"{key}.inner", field.field, {**info, **inner_info, **info.get("inner_field_info", {})}
68
+ f"{key}.inner", field.field, info.get("inner_field_info", {})
87
69
  )
88
-
89
- def constructor_read(**kwargs):
90
- return restx_fields.List(field_read, **kwargs)
91
-
92
- def constructor_write(**kwargs):
93
- return restx_fields.List(field_write, **kwargs)
70
+ constructor_read = lambda **kwargs: restx_fields.List(field_read, **kwargs)
71
+ constructor_write = lambda **kwargs: restx_fields.List(field_write, **kwargs)
94
72
  elif isinstance(
95
73
  field, (mongo_fields.GenericReferenceField, mongoengine.fields.GenericLazyReferenceField)
96
74
  ):
97
-
98
- def constructor(**kwargs):
99
- return restx_fields.Nested(lazy_reference, **kwargs)
75
+ constructor = lambda **kwargs: restx_fields.Nested(lazy_reference, **kwargs)
100
76
  elif isinstance(field, mongo_fields.ReferenceField):
101
77
  # For reference we accept while writing a String representing the ID of the referenced model.
102
78
  # For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
@@ -107,38 +83,34 @@ def convert_db_to_field(key, field, info):
107
83
  # If there is no `nested_fields` convert the object to the string representation.
108
84
  constructor_read = restx_fields.String
109
85
  else:
110
-
111
- def constructor_read(**kwargs):
112
- return restx_fields.Nested(nested_fields, **kwargs)
86
+ constructor_read = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
113
87
 
114
88
  write_params["description"] = "ID of the reference"
115
89
  constructor_write = restx_fields.String
116
90
  elif isinstance(field, mongo_fields.EmbeddedDocumentField):
117
91
  nested_fields = info.get("nested_fields")
118
92
  if nested_fields is not None:
119
-
120
- def constructor(**kwargs):
121
- return restx_fields.Nested(nested_fields, **kwargs)
93
+ constructor = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
122
94
  elif hasattr(field.document_type_obj, "__read_fields__"):
123
-
124
- def constructor_read(**kwargs):
125
- return restx_fields.Nested(field.document_type_obj.__read_fields__, **kwargs)
126
-
127
- def constructor_write(**kwargs):
128
- return restx_fields.Nested(field.document_type_obj.__write_fields__, **kwargs)
95
+ constructor_read = lambda **kwargs: restx_fields.Nested(
96
+ field.document_type_obj.__read_fields__, **kwargs
97
+ )
98
+ constructor_write = lambda **kwargs: restx_fields.Nested(
99
+ field.document_type_obj.__write_fields__, **kwargs
100
+ )
129
101
  else:
130
102
  raise ValueError(
131
103
  f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize or a `@generate_fields()` definition."
132
104
  )
133
105
 
134
106
  else:
135
- raise ValueError(f"Unsupported MongoEngine field type {field.__class__}")
107
+ raise ValueError(f"Unsupported MongoEngine field type {field.__class__.__name__}")
136
108
 
137
109
  read_params = {**params, **read_params, **info}
138
110
  write_params = {**params, **write_params, **info}
139
111
 
140
112
  read = constructor_read(**read_params) if constructor_read else constructor(**read_params)
141
- if write_params.get("readonly", False) or (constructor_write is None and constructor is None):
113
+ if write_params.get("readonly", False):
142
114
  write = None
143
115
  else:
144
116
  write = (
@@ -147,26 +119,6 @@ def convert_db_to_field(key, field, info):
147
119
  return read, write
148
120
 
149
121
 
150
- def get_fields(cls):
151
- """
152
- Returns all the exposed fields of the class (fields decorated with `field()`)
153
- It also expends image fields to add thumbnail fields.
154
- """
155
- for key, field in cls._fields.items():
156
- info: dict | None = getattr(field, "__additional_field_info__", None)
157
- if info is None:
158
- continue
159
-
160
- yield key, field, info
161
-
162
- if isinstance(field, mongo_fields.ImageField) or isinstance(field, FlaskStorageImageField):
163
- yield (
164
- f"{key}_thumbnail",
165
- field,
166
- {**info, **info.get("thumbnail_info", {}), "is_thumbnail": True, "attribute": key},
167
- )
168
-
169
-
170
122
  def generate_fields(**kwargs):
171
123
  """
172
124
  This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
@@ -176,18 +128,18 @@ def generate_fields(**kwargs):
176
128
  def wrapper(cls):
177
129
  read_fields = {}
178
130
  write_fields = {}
179
- ref_fields = {}
180
- sortables = kwargs.get("additionalSorts", [])
131
+ sortables = []
181
132
  filterables = []
182
133
 
183
- read_fields["id"] = restx_fields.String(required=True, readonly=True)
134
+ read_fields["id"] = restx_fields.String(required=True)
184
135
 
185
- for key, field, info in get_fields(cls):
186
- sortable_key = info.get("sortable", False)
187
- if sortable_key:
188
- sortables.append(
189
- {"key": sortable_key if isinstance(sortable_key, str) else key, "value": key}
190
- )
136
+ for key, field in cls._fields.items():
137
+ info = getattr(field, "__additional_field_info__", None)
138
+ if info is None:
139
+ continue
140
+
141
+ if info.get("sortable", False):
142
+ sortables.append(key)
191
143
 
192
144
  filterable = info.get("filterable", None)
193
145
  if filterable is not None:
@@ -204,26 +156,18 @@ def generate_fields(**kwargs):
204
156
  ):
205
157
  filterable["constraints"].append("objectid")
206
158
 
207
- if "type" not in filterable:
208
- filterable["type"] = str
209
- if isinstance(field, mongo_fields.BooleanField):
210
- filterable["type"] = bool
211
-
212
159
  # We may add more information later here:
213
160
  # - type of mongo query to execute (right now only simple =)
214
161
 
215
162
  filterables.append(filterable)
216
163
 
217
- read, write = convert_db_to_field(key, field, info)
164
+ read, write = convert_db_to_field(key, field)
218
165
 
219
166
  if read:
220
167
  read_fields[key] = read
221
168
  if write:
222
169
  write_fields[key] = write
223
170
 
224
- if read and info.get("show_as_ref", False):
225
- ref_fields[key] = read
226
-
227
171
  # The goal of this loop is to fetch all functions (getters) of the class
228
172
  # If a function has an `__additional_field_info__` attribute it means
229
173
  # it has been decorated with `@function_field()` and should be included
@@ -255,12 +199,9 @@ def generate_fields(**kwargs):
255
199
  read_fields[method_name] = restx_fields.String(
256
200
  attribute=make_lambda(method), **{"readonly": True, **info}
257
201
  )
258
- if info.get("show_as_ref", False):
259
- ref_fields[key] = read_fields[method_name]
260
202
 
261
203
  cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
262
204
  cls.__write_fields__ = api.model(f"{cls.__name__} (write)", write_fields, **kwargs)
263
- cls.__ref_fields__ = api.inherit(f"{cls.__name__}Reference", base_reference, ref_fields)
264
205
 
265
206
  mask = kwargs.pop("mask", None)
266
207
  if mask is not None:
@@ -285,9 +226,7 @@ def generate_fields(**kwargs):
285
226
  )
286
227
 
287
228
  if sortables:
288
- choices = [sortable["key"] for sortable in sortables] + [
289
- "-" + sortable["key"] for sortable in sortables
290
- ]
229
+ choices = sortables + ["-" + k for k in sortables]
291
230
  parser.add_argument(
292
231
  "sort",
293
232
  type=str,
@@ -296,12 +235,8 @@ def generate_fields(**kwargs):
296
235
  help="The field (and direction) on which sorting apply",
297
236
  )
298
237
 
299
- searchable = kwargs.pop("searchable", False)
300
- if searchable:
301
- parser.add_argument("q", type=str, location="args")
302
-
303
238
  for filterable in filterables:
304
- parser.add_argument(filterable["key"], type=filterable["type"], location="args")
239
+ parser.add_argument(filterable["key"], type=str, location="args")
305
240
 
306
241
  cls.__index_parser__ = parser
307
242
 
@@ -309,23 +244,7 @@ def generate_fields(**kwargs):
309
244
  args = cls.__index_parser__.parse_args()
310
245
 
311
246
  if sortables and args["sort"]:
312
- negate = args["sort"].startswith("-")
313
- sort_key = args["sort"][1:] if negate else args["sort"]
314
-
315
- sort_by = next(
316
- (sortable["value"] for sortable in sortables if sortable["key"] == sort_key),
317
- None,
318
- )
319
-
320
- if sort_by:
321
- if negate:
322
- sort_by = "-" + sort_by
323
-
324
- base_query = base_query.order_by(sort_by)
325
-
326
- if searchable and args.get("q"):
327
- phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
328
- base_query = base_query.search_text(phrase_query)
247
+ base_query = base_query.order_by(args["sort"])
329
248
 
330
249
  for filterable in filterables:
331
250
  if args.get(filterable["key"]):
@@ -374,16 +293,11 @@ def patch(obj, request):
374
293
  Patch the object with the data from the request.
375
294
  Only fields decorated with the `field()` decorator will be read (and not readonly).
376
295
  """
377
- from udata.mongo.engine import db
378
-
379
296
  for key, value in request.json.items():
380
297
  field = obj.__write_fields__.get(key)
381
298
  if field is not None and not field.readonly:
382
299
  model_attribute = getattr(obj.__class__, key)
383
-
384
- if hasattr(model_attribute, "from_input"):
385
- value = model_attribute.from_input(value)
386
- elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
300
+ if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
387
301
  model_attribute.field, mongoengine.fields.ReferenceField
388
302
  ):
389
303
  # TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
@@ -409,7 +323,7 @@ def patch(obj, request):
409
323
  # `check` field attribute allows to do validation from the request before setting
410
324
  # the attribute
411
325
  check = info.get("check", None)
412
- if check is not None and value != getattr(obj, key):
326
+ if check is not None:
413
327
  check(**{key: value}) # TODO add other model attributes in function parameters
414
328
 
415
329
  setattr(obj, key, value)
@@ -417,21 +331,10 @@ def patch(obj, request):
417
331
  return obj
418
332
 
419
333
 
420
- def patch_and_save(obj, request):
421
- obj = patch(obj, request)
422
-
423
- try:
424
- obj.save()
425
- except mongoengine.errors.ValidationError as e:
426
- api.abort(400, e.message)
427
-
428
- return obj
429
-
430
-
431
334
  def wrap_primary_key(
432
335
  field_name: str,
433
336
  foreign_field: mongoengine.fields.ReferenceField | mongoengine.fields.GenericReferenceField,
434
- value: str | None,
337
+ value: str,
435
338
  document_type=None,
436
339
  ):
437
340
  """
@@ -440,12 +343,6 @@ def wrap_primary_key(
440
343
 
441
344
  TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
442
345
  """
443
- if value is None:
444
- return value
445
-
446
- if isinstance(value, dict) and "id" in value:
447
- return wrap_primary_key(field_name, foreign_field, value["id"], document_type)
448
-
449
346
  document_type = document_type or foreign_field.document_type().__class__
450
347
  id_field_name = document_type._meta["id_field"]
451
348
 
@@ -468,7 +365,7 @@ def wrap_primary_key(
468
365
  return foreign_document
469
366
 
470
367
  if isinstance(id_field, mongoengine.fields.ObjectIdField):
471
- return foreign_document.to_dbref()
368
+ return ObjectId(value)
472
369
  elif isinstance(id_field, mongoengine.fields.StringField):
473
370
  # Right now I didn't find a simpler way to make mongoengine happy.
474
371
  # For references, it expects `ObjectId`, `DBRef`, `LazyReference` or `document` but since
udata/app.py CHANGED
@@ -206,7 +206,7 @@ def register_extensions(app):
206
206
  mail,
207
207
  models,
208
208
  mongo,
209
- notifications, # noqa
209
+ notifications,
210
210
  routing,
211
211
  search,
212
212
  sentry,
udata/auth/__init__.py CHANGED
@@ -2,14 +2,18 @@ import logging
2
2
 
3
3
  from flask import current_app, render_template
4
4
  from flask_principal import Permission as BasePermission
5
- from flask_principal import PermissionDenied as PermissionDenied
6
- from flask_principal import RoleNeed as RoleNeed
7
- from flask_principal import UserNeed as UserNeed
8
- from flask_principal import identity_loaded as identity_loaded
9
- from flask_security import Security as Security
10
- from flask_security import current_user as current_user
11
- from flask_security import login_required as login_required
12
- from flask_security import login_user as login_user
5
+ from flask_principal import (
6
+ PermissionDenied, # noqa: facade pattern
7
+ RoleNeed,
8
+ UserNeed, # noqa: facade pattern
9
+ identity_loaded, # noqa: facade pattern
10
+ )
11
+ from flask_security import ( # noqa
12
+ Security, # noqa
13
+ current_user,
14
+ login_required,
15
+ login_user,
16
+ )
13
17
  from werkzeug.utils import import_string
14
18
 
15
19
  log = logging.getLogger(__name__)
udata/commands/db.py CHANGED
@@ -159,10 +159,10 @@ def check_references(models_to_check):
159
159
  references = []
160
160
  for model in set(_models):
161
161
  if model.__name__ == "Activity":
162
- print("Skipping Activity model, scheduled for deprecation")
162
+ print(f"Skipping Activity model, scheduled for deprecation")
163
163
  continue
164
164
  if model.__name__ == "GeoLevel":
165
- print("Skipping GeoLevel model, scheduled for deprecation")
165
+ print(f"Skipping GeoLevel model, scheduled for deprecation")
166
166
  continue
167
167
 
168
168
  if models_to_check and model.__name__ not in models_to_check:
@@ -367,7 +367,7 @@ def check_references(models_to_check):
367
367
  )
368
368
  else:
369
369
  print_and_save(f'Unknown ref type {reference["type"]}')
370
- except mongoengine.errors.FieldDoesNotExist:
370
+ except mongoengine.errors.FieldDoesNotExist as e:
371
371
  print_and_save(
372
372
  f"[ERROR for {model.__name__} {obj.id}] {traceback.format_exc()}"
373
373
  )
udata/commands/dcat.py CHANGED
@@ -33,7 +33,7 @@ def parse_url(url, csw, iso, quiet=False, rid=""):
33
33
  """Parse the datasets in a DCAT format located at URL (debug)"""
34
34
  if quiet:
35
35
  verbose_loggers = ["rdflib", "udata.core.dataset"]
36
- [logging.getLogger(logger).setLevel(logging.ERROR) for logger in verbose_loggers]
36
+ [logging.getLogger(l).setLevel(logging.ERROR) for l in verbose_loggers]
37
37
 
38
38
  class MockSource:
39
39
  url = ""
@@ -16,6 +16,7 @@ from udata.commands import cli
16
16
  from udata.core.contact_point.factories import ContactPointFactory
17
17
  from udata.core.contact_point.models import ContactPoint
18
18
  from udata.core.dataservices.factories import DataserviceFactory
19
+ from udata.core.dataservices.models import Dataservice
19
20
  from udata.core.dataset.factories import (
20
21
  CommunityResourceFactory,
21
22
  DatasetFactory,
@@ -26,7 +27,6 @@ from udata.core.organization.factories import OrganizationFactory
26
27
  from udata.core.organization.models import Member, Organization
27
28
  from udata.core.reuse.factories import ReuseFactory
28
29
  from udata.core.user.factories import UserFactory
29
- from udata.core.user.models import User
30
30
 
31
31
  log = logging.getLogger(__name__)
32
32
 
@@ -56,21 +56,22 @@ UNWANTED_KEYS: dict[str, list[str]] = {
56
56
  "quality",
57
57
  ],
58
58
  "resource": ["latest", "preview_url", "last_modified"],
59
- "organization": ["class", "page", "uri", "logo_thumbnail"],
60
- "reuse": ["datasets", "image_thumbnail", "page", "uri", "owner"],
59
+ "organization": ["members", "page", "uri", "logo_thumbnail"],
60
+ "reuse": ["datasets", "image_thumbnail", "page", "uri", "organization", "owner"],
61
61
  "community": [
62
62
  "dataset",
63
+ "organization",
63
64
  "owner",
64
65
  "latest",
65
66
  "last_modified",
66
67
  "preview_url",
67
68
  ],
68
- "discussion": ["subject", "url", "class"],
69
- "user": ["uri", "page", "class", "avatar_thumbnail", "email"],
70
- "posted_by": ["uri", "page", "class", "avatar_thumbnail", "email"],
69
+ "discussion": ["subject", "user", "url", "class"],
70
+ "message": ["posted_by"],
71
71
  "dataservice": [
72
72
  "datasets",
73
73
  "license",
74
+ "organization",
74
75
  "owner",
75
76
  "self_api_url",
76
77
  "self_web_url",
@@ -80,8 +81,6 @@ UNWANTED_KEYS: dict[str, list[str]] = {
80
81
 
81
82
  def remove_unwanted_keys(obj: dict, filter_type: str) -> dict:
82
83
  """Remove UNWANTED_KEYS from a dict."""
83
- if filter_type not in UNWANTED_KEYS:
84
- return obj
85
84
  for unwanted_key in UNWANTED_KEYS[filter_type]:
86
85
  if unwanted_key in obj:
87
86
  del obj[unwanted_key]
@@ -152,29 +151,6 @@ def generate_fixtures_file(data_source: str, results_filename: str) -> None:
152
151
  print(f"Fixtures saved to file {results_filename}")
153
152
 
154
153
 
155
- def get_or_create(data, key, model, factory):
156
- """Try getting the object. If it doesn't exist yet, create it with the provided factory."""
157
- if key not in data or data[key] is None:
158
- return
159
- data[key] = remove_unwanted_keys(data[key], key)
160
- obj = model.objects(id=data[key]["id"]).first()
161
- if not obj:
162
- obj = factory(**data[key])
163
- return obj
164
-
165
-
166
- def get_or_create_organization(data):
167
- return get_or_create(data, "organization", Organization, OrganizationFactory)
168
-
169
-
170
- def get_or_create_owner(data):
171
- return get_or_create(data, "owner", User, UserFactory)
172
-
173
-
174
- def get_or_create_user(data):
175
- return get_or_create(data, "user", User, UserFactory)
176
-
177
-
178
154
  @cli.command()
179
155
  @click.argument("source", default=DEFAULT_FIXTURE_FILE)
180
156
  def import_fixtures(source):
@@ -190,45 +166,49 @@ def import_fixtures(source):
190
166
  user = UserFactory()
191
167
  dataset = fixture["dataset"]
192
168
  dataset = remove_unwanted_keys(dataset, "dataset")
193
- if fixture["organization"]:
194
- organization = fixture["organization"]
195
- organization["members"] = [
196
- Member(user=get_or_create_user(member), role=member["role"])
197
- for member in organization["members"]
198
- ]
199
- fixture["organization"] = organization
200
- org = get_or_create_organization(fixture)
201
- dataset = DatasetFactory(**dataset, organization=org)
202
- else:
169
+ if not fixture["organization"]:
203
170
  dataset = DatasetFactory(**dataset, owner=user)
171
+ else:
172
+ org = Organization.objects(id=fixture["organization"]["id"]).first()
173
+ if not org:
174
+ organization = fixture["organization"]
175
+ organization = remove_unwanted_keys(organization, "organization")
176
+ org = OrganizationFactory(**organization, members=[Member(user=user)])
177
+ dataset = DatasetFactory(**dataset, organization=org)
204
178
  for resource in fixture["resources"]:
205
179
  resource = remove_unwanted_keys(resource, "resource")
206
180
  res = ResourceFactory(**resource)
207
181
  dataset.add_resource(res)
208
182
  for reuse in fixture["reuses"]:
209
183
  reuse = remove_unwanted_keys(reuse, "reuse")
210
- reuse["owner"] = get_or_create_owner(reuse)
211
- reuse["organization"] = get_or_create_organization(reuse)
212
- ReuseFactory(**reuse, datasets=[dataset])
184
+ ReuseFactory(**reuse, datasets=[dataset], owner=user)
213
185
  for community in fixture["community_resources"]:
214
186
  community = remove_unwanted_keys(community, "community")
215
- community["owner"] = get_or_create_owner(community)
216
- community["organization"] = get_or_create_organization(community)
217
- CommunityResourceFactory(**community, dataset=dataset)
187
+ CommunityResourceFactory(**community, dataset=dataset, owner=user)
218
188
  for discussion in fixture["discussions"]:
219
189
  discussion = remove_unwanted_keys(discussion, "discussion")
220
- discussion["closed_by"] = get_or_create(discussion, "closed_by", User, UserFactory)
221
- for message in discussion["discussion"]:
222
- message["posted_by"] = get_or_create(message, "posted_by", User, UserFactory)
223
- discussion["discussion"] = [
224
- MessageDiscussionFactory(**message) for message in discussion["discussion"]
225
- ]
226
- discussion["user"] = get_or_create_user(discussion)
227
- DiscussionFactory(**discussion, subject=dataset)
190
+ messages = discussion.pop("discussion")
191
+ for message in messages:
192
+ message = remove_unwanted_keys(message, "message")
193
+ DiscussionFactory(
194
+ **discussion,
195
+ subject=dataset,
196
+ user=user,
197
+ discussion=[
198
+ MessageDiscussionFactory(**message, posted_by=user) for message in messages
199
+ ],
200
+ )
228
201
  for dataservice in fixture["dataservices"]:
229
202
  dataservice = remove_unwanted_keys(dataservice, "dataservice")
230
- dataservice["contact_point"] = get_or_create(
231
- dataservice, "contact_point", ContactPoint, ContactPointFactory
232
- )
233
- dataservice["organization"] = get_or_create_organization(dataservice)
234
- DataserviceFactory(**dataservice, datasets=[dataset])
203
+ if not dataservice["contact_point"]:
204
+ DataserviceFactory(**dataservice, datasets=[dataset])
205
+ else:
206
+ contact_point = ContactPoint.objects(
207
+ id=dataservice["contact_point"]["id"]
208
+ ).first()
209
+ if not contact_point:
210
+ contact_point = ContactPointFactory(**dataservice["contact_point"])
211
+ dataservice.pop("contact_point")
212
+ DataserviceFactory(
213
+ **dataservice, datasets=[dataset], contact_point=contact_point
214
+ )