udata 9.1.5.dev31391__py2.py3-none-any.whl → 9.2.2.dev31715__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 (32) hide show
  1. tasks/__init__.py +1 -1
  2. udata/__init__.py +1 -1
  3. udata/api/commands.py +10 -2
  4. udata/api/oauth2.py +4 -3
  5. udata/api_fields.py +5 -1
  6. udata/core/dataservices/models.py +2 -2
  7. udata/core/dataset/csv.py +4 -16
  8. udata/core/discussions/api.py +24 -1
  9. udata/core/discussions/csv.py +22 -0
  10. udata/core/owned.py +16 -0
  11. udata/core/reuse/api.py +3 -2
  12. udata/core/reuse/models.py +1 -1
  13. udata/cors.py +9 -4
  14. udata/static/chunks/{5.3aa55302c802f48db900.js → 5.9049c2001a2f21930f78.js} +2 -2
  15. udata/static/chunks/5.9049c2001a2f21930f78.js.map +1 -0
  16. udata/static/chunks/{6.0db5d3ff22944de7edd5.js → 6.ad092769b0983a6eec2a.js} +19 -19
  17. udata/static/chunks/6.ad092769b0983a6eec2a.js.map +1 -0
  18. udata/static/common.js +1 -1
  19. udata/static/common.js.map +1 -1
  20. udata/tests/api/test_auth_api.py +88 -0
  21. udata/tests/api/test_reuses_api.py +105 -0
  22. udata/tests/test_cors.py +37 -1
  23. udata/tests/test_discussions.py +61 -0
  24. udata/tests/test_owned.py +100 -1
  25. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/METADATA +18 -3
  26. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/RECORD +30 -29
  27. udata/static/chunks/5.3aa55302c802f48db900.js.map +0 -1
  28. udata/static/chunks/6.0db5d3ff22944de7edd5.js.map +0 -1
  29. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/LICENSE +0 -0
  30. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/WHEEL +0 -0
  31. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/entry_points.txt +0 -0
  32. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/top_level.txt +0 -0
tasks/__init__.py CHANGED
@@ -197,7 +197,7 @@ def qa(ctx):
197
197
  def serve(ctx, host="localhost", port="7000"):
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(f"python manage.py serve -d -r -h {host} -p {port}", pty=True)
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.5.dev"
7
+ __version__ = "9.2.2.dev"
8
8
  __description__ = "Open data portal"
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
@@ -77,12 +78,15 @@ def validate():
77
78
  @click.option(
78
79
  "-r", "--response-types", multiple=True, default=["code"], help="Client's response types"
79
80
  )
80
- def create_oauth_client(client_name, user_email, uri, grant_types, scope, response_types):
81
+ @click.option("-p", "--public", is_flag=True, help="Public client (SPA)")
82
+ def create_oauth_client(client_name, user_email, uri, grant_types, scope, response_types, public):
81
83
  """Creates an OAuth2Client instance in DB"""
82
84
  user = User.objects(email=user_email).first()
83
85
  if user is None:
84
86
  exit_with_error("No matching user to email")
85
87
 
88
+ client_secret = gen_salt(50) if not public else None
89
+
86
90
  client = OAuth2Client.objects.create(
87
91
  name=client_name,
88
92
  owner=user,
@@ -90,11 +94,15 @@ def create_oauth_client(client_name, user_email, uri, grant_types, scope, respon
90
94
  scope=scope,
91
95
  response_types=response_types,
92
96
  redirect_uris=uri,
97
+ secret=client_secret,
93
98
  )
94
99
 
95
100
  click.echo(f"New OAuth client: {client.name}")
96
101
  click.echo(f"Client's ID {client.id}")
97
- click.echo(f"Client's secret {client.secret}")
102
+ if public:
103
+ click.echo("Client is public and has no secret.")
104
+ else:
105
+ click.echo(f"Client's secret {client.secret}")
98
106
  click.echo(f"Client's grant_types {client.grant_types}")
99
107
  click.echo(f"Client's response_types {client.response_types}")
100
108
  click.echo(f"Client's URI {client.redirect_uris}")
udata/api/oauth2.py CHANGED
@@ -32,7 +32,6 @@ from bson import ObjectId
32
32
  from flask import current_app, render_template, request
33
33
  from flask_security.utils import verify_password
34
34
  from werkzeug.exceptions import Unauthorized
35
- from werkzeug.security import gen_salt
36
35
 
37
36
  from udata.app import csrf
38
37
  from udata.auth import current_user, login_required, login_user
@@ -60,7 +59,7 @@ SCOPES = {"default": _("Default scope"), "admin": _("System administrator rights
60
59
 
61
60
 
62
61
  class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
63
- secret = db.StringField(default=lambda: gen_salt(50))
62
+ secret = db.StringField(default=None)
64
63
 
65
64
  name = db.StringField(required=True)
66
65
  description = db.StringField()
@@ -209,7 +208,7 @@ class OAuth2Code(db.Document):
209
208
 
210
209
 
211
210
  class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
212
- TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"]
211
+ TOKEN_ENDPOINT_AUTH_METHODS = ["none", "client_secret_basic", "client_secret_post"]
213
212
 
214
213
  def save_authorization_code(self, code, request):
215
214
  code_challenge = request.data.get("code_challenge")
@@ -265,6 +264,8 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
265
264
 
266
265
 
267
266
  class RevokeToken(RevocationEndpoint):
267
+ CLIENT_AUTH_METHODS = ["none", "client_secret_basic"]
268
+
268
269
  def query_token(self, token_string, token_type_hint):
269
270
  qs = OAuth2Token.objects()
270
271
  if token_type_hint == "access_token":
udata/api_fields.py CHANGED
@@ -302,7 +302,11 @@ def generate_fields(**kwargs):
302
302
  parser.add_argument("q", type=str, location="args")
303
303
 
304
304
  for filterable in filterables:
305
- parser.add_argument(filterable["key"], type=filterable["type"], location="args")
305
+ parser.add_argument(
306
+ filterable["key"],
307
+ type=filterable["type"],
308
+ location="args",
309
+ )
306
310
 
307
311
  cls.__index_parser__ = parser
308
312
 
@@ -94,7 +94,7 @@ class HarvestMetadata(db.EmbeddedDocument):
94
94
  archived_at = field(db.DateTimeField())
95
95
 
96
96
 
97
- @generate_fields()
97
+ @generate_fields(searchable=True)
98
98
  class Dataservice(WithMetrics, Owned, db.Document):
99
99
  meta = {
100
100
  "indexes": [
@@ -130,7 +130,7 @@ class Dataservice(WithMetrics, Owned, db.Document):
130
130
  authorization_request_url = field(db.URLField())
131
131
  availability = field(db.FloatField(min=0, max=100), example="99.99")
132
132
  rate_limiting = field(db.StringField())
133
- is_restricted = field(db.BooleanField())
133
+ is_restricted = field(db.BooleanField(), filterable={})
134
134
  has_token = field(db.BooleanField())
135
135
  format = field(db.StringField(choices=DATASERVICE_FORMATS))
136
136
 
udata/core/dataset/csv.py CHANGED
@@ -1,4 +1,5 @@
1
- from udata.core.discussions.models import Discussion
1
+ # for backwards compatibility (see https://github.com/opendatateam/udata/pull/3152)
2
+ from udata.core.discussions.csv import DiscussionCsvAdapter # noqa: F401
2
3
  from udata.frontend import csv
3
4
 
4
5
  from .models import Dataset, Resource
@@ -36,11 +37,13 @@ class DatasetCsvAdapter(csv.Adapter):
36
37
  ("archived", lambda o: o.archived or False),
37
38
  ("resources_count", lambda o: len(o.resources)),
38
39
  ("main_resources_count", lambda o: len([r for r in o.resources if r.type == "main"])),
40
+ ("resources_formats", lambda o: ",".join(set(r.format for r in o.resources))),
39
41
  "downloads",
40
42
  ("harvest.backend", lambda r: r.harvest and r.harvest.backend),
41
43
  ("harvest.domain", lambda r: r.harvest and r.harvest.domain),
42
44
  ("harvest.created_at", lambda r: r.harvest and r.harvest.created_at),
43
45
  ("harvest.modified_at", lambda r: r.harvest and r.harvest.modified_at),
46
+ ("harvest.remote_url", lambda r: r.harvest and r.harvest.remote_url),
44
47
  ("quality_score", lambda o: format(o.quality["score"], ".2f")),
45
48
  # schema? what is the schema of a dataset?
46
49
  )
@@ -90,18 +93,3 @@ class ResourcesCsvAdapter(csv.NestedAdapter):
90
93
  ("preview_url", lambda o: o.preview_url or False),
91
94
  )
92
95
  attribute = "resources"
93
-
94
-
95
- @csv.adapter(Discussion)
96
- class DiscussionCsvAdapter(csv.Adapter):
97
- fields = (
98
- "id",
99
- "user",
100
- "subject",
101
- "title",
102
- ("size", lambda o: len(o.discussion)),
103
- ("messages", lambda o: "\n".join(msg.content for msg in o.discussion)),
104
- "created",
105
- "closed",
106
- "closed_by",
107
- )
@@ -6,6 +6,10 @@ from flask_security import current_user
6
6
 
7
7
  from udata.api import API, api, fields
8
8
  from udata.auth import admin_permission
9
+ from udata.core.dataservices.models import Dataservice
10
+ from udata.core.dataset.models import Dataset
11
+ from udata.core.organization.models import Organization
12
+ from udata.core.reuse.models import Reuse
9
13
  from udata.core.spam.api import SpamAPIMixin
10
14
  from udata.core.spam.fields import spam_fields
11
15
  from udata.core.user.api_fields import user_ref_fields
@@ -73,8 +77,15 @@ comment_discussion_fields = api.model(
73
77
  discussion_page_fields = api.model("DiscussionPage", fields.pager(discussion_fields))
74
78
 
75
79
  parser = api.parser()
80
+ sorting_keys: list[str] = ["created", "title", "closed"]
81
+ sorting_choices: list[str] = sorting_keys + ["-" + k for k in sorting_keys]
76
82
  parser.add_argument(
77
- "sort", type=str, default="-created", location="args", help="The sorting attribute"
83
+ "sort",
84
+ type=str,
85
+ default="-created",
86
+ choices=sorting_choices,
87
+ location="args",
88
+ help="The field (and direction) on which sorting apply",
78
89
  )
79
90
  parser.add_argument(
80
91
  "closed",
@@ -85,6 +96,9 @@ parser.add_argument(
85
96
  parser.add_argument(
86
97
  "for", type=str, location="args", action="append", help="Filter discussions for a given subject"
87
98
  )
99
+ parser.add_argument(
100
+ "org", type=str, location="args", help="Filter discussions for a given organization"
101
+ )
88
102
  parser.add_argument("user", type=str, location="args", help="Filter discussions created by a user")
89
103
  parser.add_argument("page", type=int, default=1, location="args", help="The page to fetch")
90
104
  parser.add_argument(
@@ -198,6 +212,15 @@ class DiscussionsAPI(API):
198
212
  discussions = Discussion.objects
199
213
  if args["for"]:
200
214
  discussions = discussions.generic_in(subject=args["for"])
215
+ if args["org"]:
216
+ org = Organization.objects.get_or_404(id=id_or_404(args["org"]))
217
+ if not org:
218
+ api.abort(404, "Organization does not exist")
219
+ reuses = Reuse.objects(organization=org).only("id")
220
+ datasets = Dataset.objects(organization=org).only("id")
221
+ dataservices = Dataservice.objects(organization=org).only("id")
222
+ subjects = list(reuses) + list(datasets) + list(dataservices)
223
+ discussions = discussions(subject__in=subjects)
201
224
  if args["user"]:
202
225
  discussions = discussions(discussion__posted_by=ObjectId(args["user"]))
203
226
  if args["closed"] is False:
@@ -0,0 +1,22 @@
1
+ from udata.frontend import csv
2
+
3
+ from .models import Discussion
4
+
5
+
6
+ @csv.adapter(Discussion)
7
+ class DiscussionCsvAdapter(csv.Adapter):
8
+ fields = (
9
+ "id",
10
+ "user",
11
+ "subject",
12
+ ("subject_class", "subject._class_name"),
13
+ ("subject_id", "subject.id"),
14
+ "title",
15
+ ("size", lambda o: len(o.discussion)),
16
+ ("participants", lambda o: ",".join(set(str(msg.posted_by.id) for msg in o.discussion))),
17
+ ("messages", lambda o: "\n".join(msg.content.replace("\n", " ") for msg in o.discussion)),
18
+ "created",
19
+ "closed",
20
+ "closed_by",
21
+ ("closed_by_id", "closed_by.id"),
22
+ )
udata/core/owned.py CHANGED
@@ -24,6 +24,22 @@ class OwnedQuerySet(UDataQuerySet):
24
24
  qs |= Q(owner=owner) | Q(organization=owner)
25
25
  return self(qs)
26
26
 
27
+ def visible_by_user(self, user: User, visible_query: Q):
28
+ """Return EVERYTHING visible to the user."""
29
+ if user.is_anonymous:
30
+ return self(visible_query)
31
+
32
+ if user.sysadmin:
33
+ return self()
34
+
35
+ owners: list[User | Organization] = list(user.organizations) + [user.id]
36
+ # We create a new queryset because we want a pristine self._query_obj.
37
+ owned_qs: OwnedQuerySet = self.__class__(self._document, self._collection_obj).owned_by(
38
+ *owners
39
+ )
40
+
41
+ return self(visible_query | owned_qs._query_obj)
42
+
27
43
 
28
44
  def check_owner_is_current_user(owner):
29
45
  from udata.auth import admin_permission, current_user
udata/core/reuse/api.py CHANGED
@@ -99,8 +99,9 @@ class ReuseListAPI(API):
99
99
  @api.expect(Reuse.__index_parser__)
100
100
  @api.marshal_with(Reuse.__page_fields__)
101
101
  def get(self):
102
- query = Reuse.objects(deleted=None, private__ne=True)
103
-
102
+ query = Reuse.objects.visible_by_user(
103
+ current_user, mongoengine.Q(private__ne=True, deleted=None)
104
+ )
104
105
  return Reuse.apply_sort_filters_and_pagination(query)
105
106
 
106
107
  @api.secure
@@ -104,7 +104,7 @@ class Reuse(db.Datetimed, WithMetrics, BadgeMixin, Owned, db.Document):
104
104
  )
105
105
  # badges = db.ListField(db.EmbeddedDocumentField(ReuseBadge))
106
106
 
107
- private = field(db.BooleanField(default=False))
107
+ private = field(db.BooleanField(default=False), filterable={})
108
108
 
109
109
  ext = db.MapField(db.GenericEmbeddedDocumentField())
110
110
  extras = field(db.ExtrasField())
udata/cors.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
 
3
- from flask import request
3
+ from flask import g, request
4
4
  from werkzeug.datastructures import Headers
5
5
 
6
6
  log = logging.getLogger(__name__)
@@ -32,10 +32,15 @@ def is_preflight_request() -> bool:
32
32
 
33
33
 
34
34
  def is_allowed_cors_route():
35
+ if g and hasattr(g, "lang_code"):
36
+ path: str = request.path.removeprefix(f"/{g.lang_code}")
37
+ else:
38
+ path: str = request.path
35
39
  return (
36
- request.path.endswith((".js", ".css", ".woff", ".woff2", ".png", ".jpg", ".jpeg", ".svg"))
37
- or request.path.startswith("/api")
38
- or request.path.startswith("/oauth")
40
+ path.endswith((".js", ".css", ".woff", ".woff2", ".png", ".jpg", ".jpeg", ".svg"))
41
+ or path.startswith("/api")
42
+ or path.startswith("/oauth")
43
+ or path.startswith("/datasets/r/")
39
44
  )
40
45
 
41
46