udata 9.1.2.dev30472__py2.py3-none-any.whl → 9.1.2.dev30483__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.

udata/api_fields.py CHANGED
@@ -5,8 +5,17 @@ from bson import ObjectId
5
5
 
6
6
  import udata.api.fields as custom_restx_fields
7
7
  from udata.api import api
8
+ from udata.mongo.engine import db
8
9
  from udata.mongo.errors import FieldValidationError
9
10
 
11
+ lazy_reference = api.model(
12
+ "LazyReference",
13
+ {
14
+ "class": restx_fields.Raw(attribute=lambda ref: ref.document_type.__name__),
15
+ "id": restx_fields.Raw(attribute=lambda ref: ref.pk),
16
+ },
17
+ )
18
+
10
19
 
11
20
  def convert_db_to_field(key, field, info={}):
12
21
  """
@@ -60,6 +69,10 @@ def convert_db_to_field(key, field, info={}):
60
69
  )
61
70
  constructor_read = lambda **kwargs: restx_fields.List(field_read, **kwargs)
62
71
  constructor_write = lambda **kwargs: restx_fields.List(field_write, **kwargs)
72
+ elif isinstance(
73
+ field, (mongo_fields.GenericReferenceField, mongoengine.fields.GenericLazyReferenceField)
74
+ ):
75
+ constructor = lambda **kwargs: restx_fields.Nested(lazy_reference, **kwargs)
63
76
  elif isinstance(field, mongo_fields.ReferenceField):
64
77
  # For reference we accept while writing a String representing the ID of the referenced model.
65
78
  # For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
@@ -289,8 +302,21 @@ def patch(obj, request):
289
302
  ):
290
303
  # TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
291
304
  value = [wrap_primary_key(key, model_attribute.field, id) for id in value]
292
- if isinstance(model_attribute, mongoengine.fields.ReferenceField):
305
+ elif isinstance(model_attribute, mongoengine.fields.ReferenceField):
293
306
  value = wrap_primary_key(key, model_attribute, value)
307
+ elif isinstance(
308
+ model_attribute,
309
+ (
310
+ mongoengine.fields.GenericReferenceField,
311
+ mongoengine.fields.GenericLazyReferenceField,
312
+ ),
313
+ ):
314
+ value = wrap_primary_key(
315
+ key,
316
+ model_attribute,
317
+ value["id"],
318
+ document_type=db.resolve_model(value["class"]),
319
+ )
294
320
 
295
321
  info = getattr(model_attribute, "__additional_field_info__", {})
296
322
 
@@ -305,24 +331,39 @@ def patch(obj, request):
305
331
  return obj
306
332
 
307
333
 
308
- def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.ReferenceField, value: str):
334
+ def wrap_primary_key(
335
+ field_name: str,
336
+ foreign_field: mongoengine.fields.ReferenceField | mongoengine.fields.GenericReferenceField,
337
+ value: str,
338
+ document_type: type = None,
339
+ ):
309
340
  """
310
341
  We need to wrap the `String` inside an `ObjectId` most of the time. If the foreign ID is a `String` we need to get
311
342
  a `DBRef` from the database.
312
343
 
313
344
  TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
314
345
  """
315
- document_type = foreign_field.document_type()
316
- id_field_name = document_type.__class__._meta["id_field"]
346
+ document_type = document_type or foreign_field.document_type().__class__
347
+ id_field_name = document_type._meta["id_field"]
317
348
 
318
- id_field = getattr(document_type.__class__, id_field_name)
349
+ id_field = getattr(document_type, id_field_name)
319
350
 
320
351
  # Get the foreign document from MongoDB because the othewise it fails during read
321
352
  # Also useful to get a DBRef for non ObjectId references (see below)
322
- foreign_document = document_type.__class__.objects(**{id_field_name: value}).first()
353
+ foreign_document = document_type.objects(**{id_field_name: value}).first()
323
354
  if foreign_document is None:
324
355
  raise FieldValidationError(field=field_name, message=f"Unknown reference '{value}'")
325
356
 
357
+ # GenericReferenceField only accepts document (not dbref / objectid)
358
+ if isinstance(
359
+ foreign_field,
360
+ (
361
+ mongoengine.fields.GenericReferenceField,
362
+ mongoengine.fields.GenericLazyReferenceField,
363
+ ),
364
+ ):
365
+ return foreign_document
366
+
326
367
  if isinstance(id_field, mongoengine.fields.ObjectIdField):
327
368
  return ObjectId(value)
328
369
  elif isinstance(id_field, mongoengine.fields.StringField):
@@ -336,5 +377,5 @@ def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.Referenc
336
377
  return foreign_document.to_dbref()
337
378
  else:
338
379
  raise ValueError(
339
- f"Unknown ID field type {id_field.__class__} for {document_type.__class__} (ID field name is {id_field_name}, value was {value})"
380
+ f"Unknown ID field type {id_field.__class__} for {document_type} (ID field name is {id_field_name}, value was {value})"
340
381
  )
@@ -1,4 +1,8 @@
1
+ from udata.core.dataservices.models import Dataservice
1
2
  from udata.core.dataset.models import Dataset
3
+ from udata.core.discussions.models import Discussion
4
+ from udata.core.organization.models import Organization
5
+ from udata.core.reuse.models import Reuse
2
6
  from udata.i18n import lazy_gettext as _
3
7
 
4
8
  REASON_PERSONAL_DATA = "personal_data"
@@ -25,4 +29,4 @@ def reports_reasons_translations() -> list:
25
29
 
26
30
 
27
31
  REPORT_REASONS_CHOICES: list[str] = [item["value"] for item in reports_reasons_translations()]
28
- REPORTABLE_MODELS = [Dataset]
32
+ REPORTABLE_MODELS = [Dataset, Reuse, Discussion, Organization, Dataservice]
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime
2
2
 
3
- from mongoengine import NULLIFY, signals
3
+ from bson import DBRef
4
+ from mongoengine import DO_NOTHING, NULLIFY, signals
4
5
 
5
6
  from udata.api_fields import field, generate_fields
6
7
  from udata.core.user.api_fields import user_ref_fields
@@ -20,9 +21,13 @@ class Report(db.Document):
20
21
  allow_null=True,
21
22
  )
22
23
 
23
- object_type = field(db.StringField(choices=[m.__name__ for m in REPORTABLE_MODELS]))
24
- object_id = field(db.ObjectIdField())
25
- object_deleted_at = field(
24
+ # Here we use the lazy version of `GenericReferenceField` because we could point to a
25
+ # non existant model (if it was deleted we want to keep the report data).
26
+ subject = field(
27
+ db.GenericLazyReferenceField(reverse_delete_rule=DO_NOTHING, choices=REPORTABLE_MODELS)
28
+ )
29
+
30
+ subject_deleted_at = field(
26
31
  db.DateTimeField(),
27
32
  allow_null=True,
28
33
  readonly=True,
@@ -42,15 +47,25 @@ class Report(db.Document):
42
47
 
43
48
  @classmethod
44
49
  def mark_as_deleted_soft_delete(cls, sender, document, **kwargs):
45
- if document.deleted:
46
- Report.objects(
47
- object_type=sender.__name__, object_id=document.id, object_deleted_at=None
48
- ).update(object_deleted_at=datetime.utcnow)
50
+ """
51
+ Called when updating a model (maybe updating the `deleted` date)
52
+ Some documents like Discussion do not have a `deleted` attribute.
53
+ """
54
+ if hasattr(document, "deleted") and document.deleted:
55
+ Report.objects(subject=document, subject_deleted_at=None).update(
56
+ subject_deleted_at=datetime.utcnow
57
+ )
49
58
 
50
- def mark_as_deleted_hard_delete(cls, document, **kwargs):
59
+ @classmethod
60
+ def mark_as_deleted_hard_delete(cls, sender, document, **kwargs):
61
+ """
62
+ Call when really deleting a model from the database.
63
+ """
64
+ # Here we are forced to do a manual `DBRef(sender.__name__.lower(), document.id)`
65
+ # because the document doesn't exist anymore…
51
66
  Report.objects(
52
- object_type=document.__class__.__name__, object_id=document.id, object_deleted_at=None
53
- ).update(object_deleted_at=datetime.utcnow)
67
+ subject=DBRef(sender.__name__.lower(), document.id), subject_deleted_at=None
68
+ ).update(subject_deleted_at=datetime.utcnow)
54
69
 
55
70
 
56
71
  for model in REPORTABLE_MODELS:
@@ -1,4 +1,5 @@
1
1
  from flask import url_for
2
+ from mongoengine.base.datastructures import LazyReference
2
3
 
3
4
  from udata.core.dataset.factories import DatasetFactory
4
5
  from udata.core.dataset.models import Dataset
@@ -35,8 +36,10 @@ class ReportsAPITest(APITestCase):
35
36
  response = self.post(
36
37
  url_for("api.reports"),
37
38
  {
38
- "object_type": "Dataset",
39
- "object_id": illegal_dataset.id,
39
+ "subject": {
40
+ "class": "Dataset",
41
+ "id": illegal_dataset.id,
42
+ },
40
43
  "message": "This is not appropriate",
41
44
  "reason": REASON_ILLEGAL_CONTENT,
42
45
  },
@@ -49,8 +52,10 @@ class ReportsAPITest(APITestCase):
49
52
  response = self.post(
50
53
  url_for("api.reports"),
51
54
  {
52
- "object_type": "Dataset",
53
- "object_id": spam_dataset.id,
55
+ "subject": {
56
+ "class": "Dataset",
57
+ "id": spam_dataset.id,
58
+ },
54
59
  "message": "This is spammy",
55
60
  "reason": REASON_SPAM,
56
61
  },
@@ -59,14 +64,14 @@ class ReportsAPITest(APITestCase):
59
64
  self.assertEqual(Report.objects.count(), 2)
60
65
 
61
66
  reports: list[Report] = list(Report.objects())
62
- self.assertEqual(Dataset.__name__, reports[0].object_type)
63
- self.assertEqual(illegal_dataset.id, reports[0].object_id)
67
+ self.assertEqual(Dataset, reports[0].subject.document_type)
68
+ self.assertEqual(illegal_dataset.id, reports[0].subject.pk)
64
69
  self.assertEqual("This is not appropriate", reports[0].message)
65
70
  self.assertEqual(REASON_ILLEGAL_CONTENT, reports[0].reason)
66
71
  self.assertIsNone(reports[0].by)
67
72
 
68
- self.assertEqual(Dataset.__name__, reports[1].object_type)
69
- self.assertEqual(spam_dataset.id, reports[1].object_id)
73
+ self.assertEqual(Dataset, reports[1].subject.document_type)
74
+ self.assertEqual(spam_dataset.id, reports[1].subject.pk)
70
75
  self.assertEqual("This is spammy", reports[1].message)
71
76
  self.assertEqual(REASON_SPAM, reports[1].reason)
72
77
  self.assertEqual(user.id, reports[1].by.id)
@@ -75,27 +80,42 @@ class ReportsAPITest(APITestCase):
75
80
  self.assert204(response)
76
81
 
77
82
  reports[0].reload()
78
- self.assertEqual(Dataset.__name__, reports[0].object_type)
79
- self.assertEqual(illegal_dataset.id, reports[0].object_id)
83
+ self.assertEqual(Dataset, reports[0].subject.document_type)
84
+ self.assertEqual(illegal_dataset.id, reports[0].subject.pk)
80
85
  self.assertEqual("This is not appropriate", reports[0].message)
81
86
  self.assertEqual(REASON_ILLEGAL_CONTENT, reports[0].reason)
82
87
  self.assertIsNone(reports[0].by)
83
- self.assertIsNotNone(reports[0].object_deleted_at)
88
+ self.assertIsNotNone(reports[0].subject_deleted_at)
84
89
 
85
90
  reports[1].reload()
86
- self.assertEqual(Dataset.__name__, reports[1].object_type)
87
- self.assertEqual(spam_dataset.id, reports[1].object_id)
91
+ self.assertEqual(Dataset, reports[1].subject.document_type)
92
+ self.assertEqual(spam_dataset.id, reports[1].subject.pk)
88
93
  self.assertEqual("This is spammy", reports[1].message)
89
94
  self.assertEqual(REASON_SPAM, reports[1].reason)
90
95
  self.assertEqual(user.id, reports[1].by.id)
91
- self.assertIsNone(reports[1].object_deleted_at)
96
+ self.assertIsNone(reports[1].subject_deleted_at)
92
97
 
93
- # We should take action on manual delete in the database too
94
98
  spam_dataset.delete()
95
99
 
96
100
  reports[1].reload()
97
- self.assertIsNotNone(reports[1].object_deleted_at)
101
+ self.assertIsNotNone(reports[1].subject_deleted_at)
98
102
 
99
103
  response = self.get(url_for("api.reports"))
100
104
  self.assert200(response)
101
- self.assertEqual(len(response.json["data"]), 2)
105
+
106
+ reports = response.json["data"]
107
+ self.assertEqual(len(reports), 2)
108
+
109
+ self.assertEqual("Dataset", reports[0]["subject"]["class"])
110
+ self.assertEqual(str(illegal_dataset.id), reports[0]["subject"]["id"])
111
+ self.assertEqual("This is not appropriate", reports[0]["message"])
112
+ self.assertEqual(REASON_ILLEGAL_CONTENT, reports[0]["reason"])
113
+ self.assertIsNone(reports[0]["by"])
114
+ self.assertIsNotNone(reports[0]["subject_deleted_at"])
115
+
116
+ self.assertEqual("Dataset", reports[1]["subject"]["class"])
117
+ self.assertEqual(str(spam_dataset.id), reports[1]["subject"]["id"])
118
+ self.assertEqual("This is spammy", reports[1]["message"])
119
+ self.assertEqual(REASON_SPAM, reports[1]["reason"])
120
+ self.assertEqual(str(user.id), reports[1]["by"]["id"])
121
+ self.assertIsNotNone(reports[1]["subject_deleted_at"])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udata
3
- Version: 9.1.2.dev30472
3
+ Version: 9.1.2.dev30483
4
4
  Summary: Open data portal
5
5
  Home-page: https://github.com/opendatateam/udata
6
6
  Author: Opendata Team
@@ -143,6 +143,7 @@ It is collectively taken care of by members of the
143
143
  - Revamp CORS: remove flask-cors to always returns 204 on OPTIONS request [#3074](https://github.com/opendatateam/udata/pull/3074)
144
144
  - Update pinned dependencies according to project dependencies, without updating any project dependencies [#3089](https://github.com/opendatateam/udata/pull/3089)
145
145
  - Add "run" button to harvesters (configurable with `HARVEST_ENABLE_MANUAL_RUN`) [#3092](https://github.com/opendatateam/udata/pull/3092)
146
+ - Move from `object_type` / `object_id` to `subject_type` / `subject_id` in reports API [#3094](https://github.com/opendatateam/udata/pull/3094)
146
147
  - Allow to report without being authenticated [#3096](https://github.com/opendatateam/udata/pull/3096)
147
148
 
148
149
  ## 9.1.1 (2024-07-16)
@@ -1,7 +1,7 @@
1
1
  tasks/__init__.py,sha256=oZ9yTY3eyOXRbN18_83l1b4BpqjIBVqw-FqD1LFWGxo,8122
2
2
  tasks/helpers.py,sha256=0a9iXzVe2GC2f6ouFoXDfMGN2s1eu4urTUuM9CCTZP8,994
3
3
  udata/__init__.py,sha256=y6WWIlMmNt4ADXtwHIWm-Dzb-txi6Y8hxw0fqe5TJwU,101
4
- udata/api_fields.py,sha256=YRLHniJYoRRI6cNJNgB-0U9A049_g1yV-Tp5u8VcLQs,14148
4
+ udata/api_fields.py,sha256=y9ZWNbIbG4HYIsfSypzE_QNGiHuTgnIBzONR5N45tm4,15437
5
5
  udata/app.py,sha256=lMxCLveZyMmaimkryDjD-VpHBC5OMEgd95VszM84y-8,7284
6
6
  udata/assets.py,sha256=jGxFWVu6JvDLK1SXBZC-rY-DGkGxguF3LFikOklyzdE,645
7
7
  udata/cors.py,sha256=QyhlcnkLo9SmJhgK3FRjLcD5YC-03bU514UVMAHfbSc,2995
@@ -164,8 +164,8 @@ udata/core/post/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
164
164
  udata/core/post/tests/test_api.py,sha256=y4fXgjC0y9tqR4iOWHin9rADSPdo1BJjXJGilnqL23c,3946
165
165
  udata/core/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
166
  udata/core/reports/api.py,sha256=wrHJempfYupq0etLWM-RPq6-f3LqXWw3hulnoPHb5v4,1376
167
- udata/core/reports/constants.py,sha256=g0c_Uj0qEQknj20mz_PrCp0EIu3a6dX5hHvg4YoqLwU,983
168
- udata/core/reports/models.py,sha256=a0Xfwt3T7lsTnGGRIv_PQmDcAZzYZx11MRVXYLra8Ys,1858
167
+ udata/core/reports/constants.py,sha256=LRZSX3unyqZeB4yQjK3ws_hGbJcXYk4bu1Rhnhi5DEs,1235
168
+ udata/core/reports/models.py,sha256=qgWXjiwMJ5w5uDdjM9K-f-h4mb-dJ8Che83MmIJ7pNg,2474
169
169
  udata/core/reuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
170
  udata/core/reuse/activities.py,sha256=mAdHhqqpUF5zSh4e5AEo0J7alc3RflTbudDaKOzyTQw,1406
171
171
  udata/core/reuse/api.py,sha256=zVck4XV6jm0A2tkL1bHQuW7fKkVE0LI_szsnmqnAPxY,10195
@@ -607,7 +607,7 @@ udata/tests/api/test_fields.py,sha256=OW85Z5MES5HeWOpapeem8OvR1cIcrqW-xMWpdZO4LZ
607
607
  udata/tests/api/test_follow_api.py,sha256=fccgVNfcqET221PPS3p7qzb9hpvbBBUGhV-l4UeOpyk,3352
608
608
  udata/tests/api/test_me_api.py,sha256=ZJKGH9fFv-4cSGcYAGd6IJA_PwPjVGIqWNy_DhFA8ms,13827
609
609
  udata/tests/api/test_organizations_api.py,sha256=5hBs7-gsMfy4IFsrLYyUMGyxhtCs71K4QvPFAXyDsz4,35049
610
- udata/tests/api/test_reports_api.py,sha256=ee_Hrcs-rpbdUlV80ITmyrhtIC2cfg7rSwwVb3uXJgk,3604
610
+ udata/tests/api/test_reports_api.py,sha256=4BHpCxDOkmbxe2xd2qCd0xXUjdAqgom7PP9117V8Bds,4515
611
611
  udata/tests/api/test_reuses_api.py,sha256=v9RJcJ_fdR8dgnq6H7j58_7n83BhoFJJ9YOd7CJwsyw,16769
612
612
  udata/tests/api/test_swagger.py,sha256=eE6La9qdTYTIUFevRVPJgtj17Jq_8uOlsDwzCNR0LL8,760
613
613
  udata/tests/api/test_tags_api.py,sha256=MgSmKZeQ8L-fO-LwOGlDm_YN7lmEyvTpkDgaPiENHw8,2429
@@ -698,9 +698,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=yfwqLHV-_mGNaDklP7T0pQP4i1y__s
698
698
  udata/translations/pt/LC_MESSAGES/udata.po,sha256=E45ZRCW2bpedoWOztkgJ9gwBlg2L1pTcHmCXgqSdhT0,44979
699
699
  udata/translations/sr/LC_MESSAGES/udata.mo,sha256=w_x0mh_WagUuQ5QFweqHg5-HtDrsoyL66HVmUxrdR0U,28500
700
700
  udata/translations/sr/LC_MESSAGES/udata.po,sha256=LfaUQzhrfDClLdBo_U2erasp2XR1z1_V132cewvZ9C8,51548
701
- udata-9.1.2.dev30472.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
702
- udata-9.1.2.dev30472.dist-info/METADATA,sha256=JROSC0pBFhtzFFgA6_Ic_TpQyd8e7OKQbz9rrXj5zLM,126704
703
- udata-9.1.2.dev30472.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
704
- udata-9.1.2.dev30472.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
705
- udata-9.1.2.dev30472.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
706
- udata-9.1.2.dev30472.dist-info/RECORD,,
701
+ udata-9.1.2.dev30483.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
702
+ udata-9.1.2.dev30483.dist-info/METADATA,sha256=opfzZHL_6Xo7XmPscPW7Qwr1tmJsrz1yiJZf--Wwy7Y,126849
703
+ udata-9.1.2.dev30483.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
704
+ udata-9.1.2.dev30483.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
705
+ udata-9.1.2.dev30483.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
706
+ udata-9.1.2.dev30483.dist-info/RECORD,,