udata 9.1.4.dev31123__py2.py3-none-any.whl → 9.1.4.dev31143__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 (36) hide show
  1. udata/api_fields.py +119 -26
  2. udata/core/badges/models.py +7 -1
  3. udata/core/metrics/models.py +4 -1
  4. udata/core/organization/api.py +1 -2
  5. udata/core/owned.py +4 -2
  6. udata/core/post/api.py +2 -2
  7. udata/core/reuse/api.py +32 -25
  8. udata/core/reuse/api_fields.py +2 -101
  9. udata/core/reuse/apiv2.py +4 -4
  10. udata/core/reuse/models.py +98 -16
  11. udata/core/site/api.py +2 -3
  12. udata/core/topic/api.py +2 -2
  13. udata/core/topic/apiv2.py +1 -2
  14. udata/core/user/api.py +2 -3
  15. udata/features/transfer/api.py +1 -2
  16. udata/mongo/datetime_fields.py +11 -4
  17. udata/mongo/document.py +2 -0
  18. udata/mongo/taglist_field.py +26 -0
  19. udata/static/admin.js +36 -36
  20. udata/static/admin.js.map +1 -1
  21. udata/static/chunks/{12.5b900cac4417e10ef3a0.js → 12.576e63b7a990f8eab784.js} +2 -2
  22. udata/static/chunks/12.576e63b7a990f8eab784.js.map +1 -0
  23. udata/static/chunks/{28.1759a7f57d526e6db574.js → 28.1ef31a46255dc2bf56d1.js} +2 -2
  24. udata/static/chunks/28.1ef31a46255dc2bf56d1.js.map +1 -0
  25. udata/static/common.js +1 -1
  26. udata/static/common.js.map +1 -1
  27. udata/tests/api/test_reuses_api.py +49 -0
  28. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/METADATA +2 -1
  29. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/RECORD +33 -34
  30. udata/core/reuse/forms.py +0 -45
  31. udata/static/chunks/12.5b900cac4417e10ef3a0.js.map +0 -1
  32. udata/static/chunks/28.1759a7f57d526e6db574.js.map +0 -1
  33. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/LICENSE +0 -0
  34. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/WHEEL +0 -0
  35. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/entry_points.txt +0 -0
  36. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,15 @@ from blinker import Signal
2
2
  from mongoengine.signals import post_save, pre_save
3
3
  from werkzeug.utils import cached_property
4
4
 
5
+ from udata.api_fields import field, function_field, generate_fields
6
+ from udata.core.dataset.api_fields import dataset_fields
5
7
  from udata.core.owned import Owned, OwnedQuerySet
8
+ from udata.core.reuse.api_fields import BIGGEST_IMAGE_SIZE
6
9
  from udata.core.storages import default_image_basename, images
7
10
  from udata.frontend.markdown import mdstrip
8
11
  from udata.i18n import lazy_gettext as _
9
12
  from udata.models import BadgeMixin, WithMetrics, db
13
+ from udata.mongo.errors import FieldValidationError
10
14
  from udata.uris import endpoint_for
11
15
  from udata.utils import hash_url
12
16
 
@@ -23,32 +27,100 @@ class ReuseQuerySet(OwnedQuerySet):
23
27
  return self(db.Q(private=True) | db.Q(datasets__0__exists=False) | db.Q(deleted__ne=None))
24
28
 
25
29
 
30
+ def check_url_does_not_exists(url):
31
+ """Ensure a reuse URL is not yet registered"""
32
+ if url and Reuse.url_exists(url):
33
+ raise FieldValidationError(_("This URL is already registered"), field="url")
34
+
35
+
36
+ @generate_fields(
37
+ searchable=True,
38
+ additionalSorts=[
39
+ {"key": "datasets", "value": "metrics.datasets"},
40
+ {"key": "followers", "value": "metrics.followers"},
41
+ {"key": "views", "value": "metrics.views"},
42
+ ],
43
+ )
26
44
  class Reuse(db.Datetimed, WithMetrics, BadgeMixin, Owned, db.Document):
27
- title = db.StringField(required=True)
28
- slug = db.SlugField(
29
- max_length=255, required=True, populate_from="title", update=True, follow=True
45
+ title = field(
46
+ db.StringField(required=True),
47
+ sortable=True,
48
+ show_as_ref=True,
49
+ )
50
+ slug = field(
51
+ db.SlugField(
52
+ max_length=255, required=True, populate_from="title", update=True, follow=True
53
+ ),
54
+ readonly=True,
55
+ )
56
+ description = field(
57
+ db.StringField(required=True),
58
+ markdown=True,
59
+ )
60
+ type = field(
61
+ db.StringField(required=True, choices=list(REUSE_TYPES)),
62
+ filterable={},
63
+ )
64
+ url = field(
65
+ db.StringField(required=True),
66
+ description="The remote URL (website)",
67
+ check=check_url_does_not_exists,
30
68
  )
31
- description = db.StringField(required=True)
32
- type = db.StringField(required=True, choices=list(REUSE_TYPES))
33
- url = db.StringField(required=True)
34
69
  urlhash = db.StringField(required=True, unique=True)
35
70
  image_url = db.StringField()
36
- image = db.ImageField(
37
- fs=images, basename=default_image_basename, max_size=IMAGE_MAX_SIZE, thumbnails=IMAGE_SIZES
71
+ image = field(
72
+ db.ImageField(
73
+ fs=images,
74
+ basename=default_image_basename,
75
+ max_size=IMAGE_MAX_SIZE,
76
+ thumbnails=IMAGE_SIZES,
77
+ ),
78
+ readonly=True,
79
+ show_as_ref=True,
80
+ thumbnail_info={
81
+ "size": BIGGEST_IMAGE_SIZE,
82
+ },
83
+ )
84
+ datasets = field(
85
+ db.ListField(
86
+ field(
87
+ db.ReferenceField("Dataset", reverse_delete_rule=db.PULL),
88
+ nested_fields=dataset_fields,
89
+ ),
90
+ ),
91
+ filterable={
92
+ "key": "dataset",
93
+ },
94
+ )
95
+ tags = field(
96
+ db.TagListField(),
97
+ filterable={
98
+ "key": "tag",
99
+ },
100
+ )
101
+ topic = field(
102
+ db.StringField(required=True, choices=list(REUSE_TOPICS)),
103
+ filterable={},
38
104
  )
39
- datasets = db.ListField(db.ReferenceField("Dataset", reverse_delete_rule=db.PULL))
40
- tags = db.TagListField()
41
- topic = db.StringField(required=True, choices=list(REUSE_TOPICS))
42
105
  # badges = db.ListField(db.EmbeddedDocumentField(ReuseBadge))
43
106
 
44
- private = db.BooleanField(default=False)
107
+ private = field(db.BooleanField(default=False))
45
108
 
46
109
  ext = db.MapField(db.GenericEmbeddedDocumentField())
47
- extras = db.ExtrasField()
110
+ extras = field(db.ExtrasField())
48
111
 
49
- featured = db.BooleanField()
50
- deleted = db.DateTimeField()
51
- archived = db.DateTimeField()
112
+ featured = field(
113
+ db.BooleanField(),
114
+ filterable={},
115
+ readonly=True,
116
+ )
117
+ deleted = field(
118
+ db.DateTimeField(),
119
+ readonly=True,
120
+ )
121
+ archived = field(
122
+ db.DateTimeField(),
123
+ )
52
124
 
53
125
  def __str__(self):
54
126
  return self.title or ""
@@ -110,6 +182,16 @@ class Reuse(db.Datetimed, WithMetrics, BadgeMixin, Owned, db.Document):
110
182
 
111
183
  display_url = property(url_for)
112
184
 
185
+ @function_field(description="Link to the API endpoint for this reuse", show_as_ref=True)
186
+ def uri(self):
187
+ return endpoint_for("api.reuse", reuse=self, _external=True)
188
+
189
+ @function_field(description="Link to the udata web page for this reuse", show_as_ref=True)
190
+ def page(self):
191
+ return endpoint_for(
192
+ "reuses.show", reuse=self, _external=True, fallback_endpoint="api.reuse"
193
+ )
194
+
113
195
  @property
114
196
  def is_visible(self):
115
197
  return not self.is_hidden
udata/core/site/api.py CHANGED
@@ -6,7 +6,6 @@ from udata.api import API, api, fields
6
6
  from udata.auth import admin_permission
7
7
  from udata.core.dataservices.models import Dataservice
8
8
  from udata.core.dataset.api_fields import dataset_fields
9
- from udata.core.reuse.api_fields import reuse_fields
10
9
  from udata.models import Dataset, Reuse
11
10
  from udata.rdf import CONTEXT, RDF_EXTENSIONS, graph_response, negociate_content
12
11
  from udata.utils import multi_to_dict
@@ -62,7 +61,7 @@ class SiteHomeDatasetsAPI(API):
62
61
  class SiteHomeReusesAPI(API):
63
62
  @api.doc("get_home_reuses")
64
63
  @api.secure(admin_permission)
65
- @api.marshal_list_with(reuse_fields)
64
+ @api.marshal_list_with(Reuse.__read_fields__)
66
65
  def get(self):
67
66
  """List homepage featured reuses"""
68
67
  return current_site.settings.home_reuses
@@ -70,7 +69,7 @@ class SiteHomeReusesAPI(API):
70
69
  @api.secure(admin_permission)
71
70
  @api.doc("set_home_reuses")
72
71
  @api.expect(([str], "Reuse IDs to put in homepage"))
73
- @api.marshal_list_with(reuse_fields)
72
+ @api.marshal_list_with(Reuse.__read_fields__)
74
73
  def put(self):
75
74
  """Set the homepage reuses editorial selection"""
76
75
  if not isinstance(request.json, list):
udata/core/topic/api.py CHANGED
@@ -2,7 +2,7 @@ from udata.api import API, api, fields
2
2
  from udata.core.dataset.api_fields import dataset_fields
3
3
  from udata.core.discussions.models import Discussion
4
4
  from udata.core.organization.api_fields import org_ref_fields
5
- from udata.core.reuse.api_fields import reuse_fields
5
+ from udata.core.reuse.models import Reuse
6
6
  from udata.core.spatial.api_fields import spatial_coverage_fields
7
7
  from udata.core.topic.parsers import TopicApiParser
8
8
  from udata.core.topic.permissions import TopicEditPermission
@@ -33,7 +33,7 @@ topic_fields = api.model(
33
33
  attribute=lambda o: [d.fetch() for d in o.datasets],
34
34
  ),
35
35
  "reuses": fields.List(
36
- fields.Nested(reuse_fields),
36
+ fields.Nested(Reuse.__read_fields__),
37
37
  description="The topic reuses",
38
38
  attribute=lambda o: [r.fetch() for r in o.reuses],
39
39
  ),
udata/core/topic/apiv2.py CHANGED
@@ -10,7 +10,6 @@ from udata.core.dataset.apiv2 import dataset_page_fields
10
10
  from udata.core.dataset.models import Dataset
11
11
  from udata.core.organization.api_fields import org_ref_fields
12
12
  from udata.core.reuse.api import ReuseApiParser
13
- from udata.core.reuse.apiv2 import reuse_page_fields
14
13
  from udata.core.reuse.models import Reuse
15
14
  from udata.core.spatial.api_fields import spatial_coverage_fields
16
15
  from udata.core.topic.models import Topic
@@ -216,7 +215,7 @@ class TopicDatasetAPI(API):
216
215
  class TopicReusesAPI(API):
217
216
  @apiv2.doc("topic_reuses")
218
217
  @apiv2.expect(reuse_parser.parser)
219
- @apiv2.marshal_with(reuse_page_fields)
218
+ @apiv2.marshal_with(Reuse.__page_fields__)
220
219
  def get(self, topic):
221
220
  """Get a given topic reuses, with filters"""
222
221
  args = reuse_parser.parse()
udata/core/user/api.py CHANGED
@@ -8,7 +8,6 @@ from udata.core.dataset.api_fields import community_resource_fields, dataset_fie
8
8
  from udata.core.discussions.actions import discussions_for
9
9
  from udata.core.discussions.api import discussion_fields
10
10
  from udata.core.followers.api import FollowAPI
11
- from udata.core.reuse.api_fields import reuse_fields
12
11
  from udata.core.storages.api import (
13
12
  image_parser,
14
13
  parse_uploaded_image,
@@ -102,7 +101,7 @@ class AvatarAPI(API):
102
101
  class MyReusesAPI(API):
103
102
  @api.secure
104
103
  @api.doc("my_reuses")
105
- @api.marshal_list_with(reuse_fields)
104
+ @api.marshal_list_with(Reuse.__read_fields__)
106
105
  def get(self):
107
106
  """List all my reuses (including private ones)"""
108
107
  return list(Reuse.objects.owned_by(current_user.id))
@@ -165,7 +164,7 @@ class MyOrgReusesAPI(API):
165
164
  @api.secure
166
165
  @api.doc("my_org_reuses")
167
166
  @api.expect(filter_parser)
168
- @api.marshal_list_with(reuse_fields)
167
+ @api.marshal_list_with(Reuse.__read_fields__)
169
168
  def get(self):
170
169
  """List all reuses related to me and my organizations."""
171
170
  q = filter_parser.parse_args().get("q")
@@ -3,7 +3,6 @@ from flask import request
3
3
  from udata.api import API, api, base_reference, fields
4
4
  from udata.core.dataset.api_fields import dataset_ref_fields
5
5
  from udata.core.organization.api_fields import org_ref_fields
6
- from udata.core.reuse.api_fields import reuse_ref_fields
7
6
  from udata.core.user.api_fields import user_ref_fields
8
7
  from udata.models import Dataset, Organization, Reuse, User, db
9
8
  from udata.utils import id_or_404
@@ -47,7 +46,7 @@ person_mapping = {
47
46
 
48
47
  subject_mapping = {
49
48
  Dataset: dataset_ref_fields,
50
- Reuse: reuse_ref_fields,
49
+ Reuse: Reuse.__ref_fields__,
51
50
  }
52
51
 
53
52
  transfer_fields = api.model(
@@ -6,6 +6,7 @@ from mongoengine import EmbeddedDocument
6
6
  from mongoengine.fields import BaseField, DateTimeField
7
7
  from mongoengine.signals import pre_save
8
8
 
9
+ from udata.api_fields import field
9
10
  from udata.i18n import lazy_gettext as _
10
11
 
11
12
  log = logging.getLogger(__name__)
@@ -55,11 +56,17 @@ class DateRange(EmbeddedDocument):
55
56
 
56
57
 
57
58
  class Datetimed(object):
58
- created_at = DateTimeField(
59
- verbose_name=_("Creation date"), default=datetime.utcnow, required=True
59
+ created_at = field(
60
+ DateTimeField(verbose_name=_("Creation date"), default=datetime.utcnow, required=True),
61
+ sortable="created",
62
+ readonly=True,
60
63
  )
61
- last_modified = DateTimeField(
62
- verbose_name=_("Last modification date"), default=datetime.utcnow, required=True
64
+ last_modified = field(
65
+ DateTimeField(
66
+ verbose_name=_("Last modification date"), default=datetime.utcnow, required=True
67
+ ),
68
+ sortable=True,
69
+ readonly=True,
63
70
  )
64
71
 
65
72
 
udata/mongo/document.py CHANGED
@@ -11,6 +11,8 @@ log = logging.getLogger(__name__)
11
11
  def serialize(value):
12
12
  if hasattr(value, "to_dict"):
13
13
  return value.to_dict()
14
+ elif isinstance(value, dict):
15
+ return {key: serialize(val) for key, val in value.items()}
14
16
  elif isinstance(value, Iterable) and not isinstance(value, str):
15
17
  return [serialize(val) for val in value]
16
18
  else:
@@ -1,12 +1,24 @@
1
1
  from mongoengine.fields import ListField, StringField
2
2
  from slugify import slugify
3
3
 
4
+ from udata import tags
5
+ from udata.i18n import lazy_gettext as _
6
+
4
7
 
5
8
  class TagListField(ListField):
6
9
  def __init__(self, **kwargs):
7
10
  self.tags = []
8
11
  super(TagListField, self).__init__(StringField(), **kwargs)
9
12
 
13
+ @staticmethod
14
+ def from_input(input):
15
+ if isinstance(input, list):
16
+ return [tags.slug(value) for value in input]
17
+ elif isinstance(input, str):
18
+ return tags.tags_list(input)
19
+ else:
20
+ return []
21
+
10
22
  def clean(self, value):
11
23
  return sorted(list(set([slugify(v, to_lower=True) for v in value])))
12
24
 
@@ -15,3 +27,17 @@ class TagListField(ListField):
15
27
 
16
28
  def to_mongo(self, value):
17
29
  return super(TagListField, self).to_mongo(self.clean(value))
30
+
31
+ def validate(self, values):
32
+ super(TagListField, self).validate(values)
33
+
34
+ for tag in values:
35
+ if not tags.MIN_TAG_LENGTH <= len(tag) <= tags.MAX_TAG_LENGTH:
36
+ self.error(
37
+ _(
38
+ 'Tag "%(tag)s" must be between %(min)d ' "and %(max)d characters long.",
39
+ min=tags.MIN_TAG_LENGTH,
40
+ max=tags.MAX_TAG_LENGTH,
41
+ tag=tag,
42
+ )
43
+ )