udata 9.2.5.dev32170__py2.py3-none-any.whl → 9.2.5.dev32222__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/__init__.py +1 -0
- udata/api_fields.py +143 -37
- udata/core/badges/factories.py +3 -5
- udata/core/badges/forms.py +0 -3
- udata/core/badges/models.py +19 -24
- udata/core/badges/tests/test_commands.py +3 -4
- udata/core/badges/tests/test_model.py +37 -28
- udata/core/dataservices/api.py +1 -1
- udata/core/dataservices/apiv2.py +29 -0
- udata/core/dataservices/models.py +4 -1
- udata/core/dataservices/search.py +118 -0
- udata/core/dataset/api.py +10 -0
- udata/core/dataset/models.py +16 -6
- udata/core/dataset/search.py +5 -4
- udata/core/organization/api.py +11 -0
- udata/core/organization/models.py +23 -10
- udata/core/organization/search.py +8 -5
- udata/core/reuse/api.py +10 -0
- udata/core/reuse/models.py +15 -5
- udata/core/reuse/search.py +5 -4
- udata/search/fields.py +8 -3
- udata/search/query.py +4 -2
- udata/static/chunks/{10.dac55d18d0b4ef3cdacf.js → 10.a99bb538cfbadb38dbcb.js} +3 -3
- udata/static/chunks/{10.dac55d18d0b4ef3cdacf.js.map → 10.a99bb538cfbadb38dbcb.js.map} +1 -1
- udata/static/chunks/{11.4a20a75f827c5a1125c3.js → 11.bb1c1fb39f740fbbeec0.js} +3 -3
- udata/static/chunks/{11.4a20a75f827c5a1125c3.js.map → 11.bb1c1fb39f740fbbeec0.js.map} +1 -1
- udata/static/chunks/{13.645dd0b7c0b9210f1b56.js → 13.bef5fdb3e147e94fea99.js} +2 -2
- udata/static/chunks/{13.645dd0b7c0b9210f1b56.js.map → 13.bef5fdb3e147e94fea99.js.map} +1 -1
- udata/static/chunks/{17.8e19985c4d12a3b7b0c0.js → 17.b91d28f550dc44bc4979.js} +2 -2
- udata/static/chunks/{17.8e19985c4d12a3b7b0c0.js.map → 17.b91d28f550dc44bc4979.js.map} +1 -1
- udata/static/chunks/{19.825a43c330157e351fca.js → 19.2c615ffee1e807000770.js} +3 -3
- udata/static/chunks/{19.825a43c330157e351fca.js.map → 19.2c615ffee1e807000770.js.map} +1 -1
- udata/static/chunks/{8.5ee0cf635c848abbfc05.js → 8.291bde987ed97294e4de.js} +2 -2
- udata/static/chunks/{8.5ee0cf635c848abbfc05.js.map → 8.291bde987ed97294e4de.js.map} +1 -1
- udata/static/chunks/{9.df3c36f8d0d210621fbb.js → 9.985935421e62c97a9f86.js} +3 -3
- udata/static/chunks/{9.df3c36f8d0d210621fbb.js.map → 9.985935421e62c97a9f86.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_dataservices_api.py +21 -1
- udata/tests/api/test_datasets_api.py +15 -0
- udata/tests/api/test_organizations_api.py +15 -4
- udata/tests/api/test_reuses_api.py +15 -0
- udata/tests/apiv2/test_datasets.py +17 -0
- udata/tests/organization/test_organization_model.py +21 -0
- udata/tests/site/test_site_metrics.py +1 -3
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/METADATA +4 -1
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/RECORD +51 -49
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/LICENSE +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/WHEEL +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/entry_points.txt +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from bson.objectid import ObjectId
|
|
4
|
+
from flask_restx.inputs import boolean
|
|
5
|
+
|
|
6
|
+
from udata.api import api
|
|
7
|
+
from udata.api.parsers import ModelApiParser
|
|
8
|
+
from udata.models import Dataservice, Organization, User
|
|
9
|
+
from udata.search import (
|
|
10
|
+
BoolFilter,
|
|
11
|
+
Filter,
|
|
12
|
+
ModelSearchAdapter,
|
|
13
|
+
ModelTermsFilter,
|
|
14
|
+
register,
|
|
15
|
+
)
|
|
16
|
+
from udata.utils import to_iso_datetime
|
|
17
|
+
|
|
18
|
+
__all__ = ("DataserviceSearch",)
|
|
19
|
+
|
|
20
|
+
DEFAULT_SORTING = "-created_at"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DataserviceApiParser(ModelApiParser):
|
|
24
|
+
sorts = {
|
|
25
|
+
"created": "created_at",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.parser.add_argument("tag", type=str, location="args")
|
|
31
|
+
self.parser.add_argument("organization", type=str, location="args")
|
|
32
|
+
self.parser.add_argument("is_restricted", type=bool, location="args")
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def parse_filters(dataservices, args):
|
|
36
|
+
if args.get("q"):
|
|
37
|
+
# Following code splits the 'q' argument by spaces to surround
|
|
38
|
+
# every word in it with quotes before rebuild it.
|
|
39
|
+
# This allows the search_text method to tokenise with an AND
|
|
40
|
+
# between tokens whereas an OR is used without it.
|
|
41
|
+
phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
42
|
+
dataservices = dataservices.search_text(phrase_query)
|
|
43
|
+
if args.get("tag"):
|
|
44
|
+
dataservices = dataservices.filter(tags=args["tag"])
|
|
45
|
+
if args.get("organization"):
|
|
46
|
+
if not ObjectId.is_valid(args["organization"]):
|
|
47
|
+
api.abort(400, "Organization arg must be an identifier")
|
|
48
|
+
dataservices = dataservices.filter(organization=args["organization"])
|
|
49
|
+
if "is_restricted" in args:
|
|
50
|
+
dataservices = dataservices.filter(is_restricted=boolean(args["is_restricted"]))
|
|
51
|
+
return dataservices
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@register
|
|
55
|
+
class DataserviceSearch(ModelSearchAdapter):
|
|
56
|
+
model = Dataservice
|
|
57
|
+
search_url = "dataservices/"
|
|
58
|
+
|
|
59
|
+
sorts = {
|
|
60
|
+
"created": "created_at",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
filters = {
|
|
64
|
+
"tag": Filter(),
|
|
65
|
+
"organization": ModelTermsFilter(model=Organization),
|
|
66
|
+
"archived": BoolFilter(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def is_indexable(cls, dataservice: Dataservice) -> bool:
|
|
71
|
+
return dataservice.deleted_at is None and not dataservice.private
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def mongo_search(cls, args):
|
|
75
|
+
dataservices = Dataservice.objects.visible()
|
|
76
|
+
dataservices = DataserviceApiParser.parse_filters(dataservices, args)
|
|
77
|
+
|
|
78
|
+
sort = (
|
|
79
|
+
cls.parse_sort(args["sort"])
|
|
80
|
+
or ("$text_score" if args["q"] else None)
|
|
81
|
+
or DEFAULT_SORTING
|
|
82
|
+
)
|
|
83
|
+
return dataservices.order_by(sort).paginate(args["page"], args["page_size"])
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def serialize(cls, dataservice: Dataservice) -> dict:
|
|
87
|
+
organization = None
|
|
88
|
+
owner = None
|
|
89
|
+
if dataservice.organization:
|
|
90
|
+
org = Organization.objects(id=dataservice.organization.id).first()
|
|
91
|
+
organization = {
|
|
92
|
+
"id": str(org.id),
|
|
93
|
+
"name": org.name,
|
|
94
|
+
"public_service": 1 if org.public_service else 0,
|
|
95
|
+
"followers": org.metrics.get("followers", 0),
|
|
96
|
+
}
|
|
97
|
+
elif dataservice.owner:
|
|
98
|
+
owner = User.objects(id=dataservice.owner.id).first()
|
|
99
|
+
extras = {}
|
|
100
|
+
for key, value in dataservice.extras.items():
|
|
101
|
+
extras[key] = to_iso_datetime(value) if isinstance(value, datetime.datetime) else value
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"id": str(dataservice.id),
|
|
105
|
+
"title": dataservice.title,
|
|
106
|
+
"description": dataservice.description,
|
|
107
|
+
"base_api_url": dataservice.base_api_url,
|
|
108
|
+
"created_at": to_iso_datetime(dataservice.created_at),
|
|
109
|
+
"archived": to_iso_datetime(dataservice.archived_at)
|
|
110
|
+
if dataservice.archived_at
|
|
111
|
+
else None,
|
|
112
|
+
"organization": organization,
|
|
113
|
+
"owner": str(owner.id) if owner else None,
|
|
114
|
+
"tags": dataservice.tags,
|
|
115
|
+
"extras": extras,
|
|
116
|
+
"followers": dataservice.metrics.get("followers", 0),
|
|
117
|
+
"is_restricted": dataservice.is_restricted or False,
|
|
118
|
+
}
|
udata/core/dataset/api.py
CHANGED
|
@@ -36,6 +36,7 @@ from udata.core.badges.fields import badge_fields
|
|
|
36
36
|
from udata.core.dataservices.models import Dataservice
|
|
37
37
|
from udata.core.dataset.models import CHECKSUM_TYPES
|
|
38
38
|
from udata.core.followers.api import FollowAPI
|
|
39
|
+
from udata.core.organization.models import Organization
|
|
39
40
|
from udata.core.storages.api import handle_upload, upload_parser
|
|
40
41
|
from udata.core.topic.models import Topic
|
|
41
42
|
from udata.linkchecker.checker import check_resource
|
|
@@ -96,6 +97,12 @@ class DatasetApiParser(ModelApiParser):
|
|
|
96
97
|
self.parser.add_argument("granularity", type=str, location="args")
|
|
97
98
|
self.parser.add_argument("temporal_coverage", type=str, location="args")
|
|
98
99
|
self.parser.add_argument("organization", type=str, location="args")
|
|
100
|
+
self.parser.add_argument(
|
|
101
|
+
"organization_badge",
|
|
102
|
+
type=str,
|
|
103
|
+
choices=list(Organization.__badges__),
|
|
104
|
+
location="args",
|
|
105
|
+
)
|
|
99
106
|
self.parser.add_argument("owner", type=str, location="args")
|
|
100
107
|
self.parser.add_argument("format", type=str, location="args")
|
|
101
108
|
self.parser.add_argument("schema", type=str, location="args")
|
|
@@ -131,6 +138,9 @@ class DatasetApiParser(ModelApiParser):
|
|
|
131
138
|
if not ObjectId.is_valid(args["organization"]):
|
|
132
139
|
api.abort(400, "Organization arg must be an identifier")
|
|
133
140
|
datasets = datasets.filter(organization=args["organization"])
|
|
141
|
+
if args.get("organization_badge"):
|
|
142
|
+
orgs = Organization.objects.with_badge(args["organization_badge"]).only("id")
|
|
143
|
+
datasets = datasets.filter(organization__in=orgs)
|
|
134
144
|
if args.get("owner"):
|
|
135
145
|
if not ObjectId.is_valid(args["owner"]):
|
|
136
146
|
api.abort(400, "Owner arg must be an identifier")
|
udata/core/dataset/models.py
CHANGED
|
@@ -14,12 +14,13 @@ from mongoengine.signals import post_save, pre_save
|
|
|
14
14
|
from stringdist import rdlevenshtein
|
|
15
15
|
from werkzeug.utils import cached_property
|
|
16
16
|
|
|
17
|
+
from udata.api_fields import field
|
|
17
18
|
from udata.app import cache
|
|
18
19
|
from udata.core import storages
|
|
19
20
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
20
21
|
from udata.frontend.markdown import mdstrip
|
|
21
22
|
from udata.i18n import lazy_gettext as _
|
|
22
|
-
from udata.models import BadgeMixin, SpatialCoverage, WithMetrics, db
|
|
23
|
+
from udata.models import Badge, BadgeMixin, BadgesList, SpatialCoverage, WithMetrics, db
|
|
23
24
|
from udata.mongo.errors import FieldValidationError
|
|
24
25
|
from udata.uris import ValidationError, endpoint_for
|
|
25
26
|
from udata.uris import validate as validate_url
|
|
@@ -53,6 +54,10 @@ __all__ = (
|
|
|
53
54
|
"ResourceSchema",
|
|
54
55
|
)
|
|
55
56
|
|
|
57
|
+
BADGES: dict[str, str] = {
|
|
58
|
+
PIVOTAL_DATA: _("Pivotal data"),
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
NON_ASSIGNABLE_SCHEMA_TYPES = ["datapackage"]
|
|
57
62
|
|
|
58
63
|
log = logging.getLogger(__name__)
|
|
@@ -498,7 +503,16 @@ class Resource(ResourceMixin, WithMetrics, db.EmbeddedDocument):
|
|
|
498
503
|
self.dataset.save(*args, **kwargs)
|
|
499
504
|
|
|
500
505
|
|
|
501
|
-
class
|
|
506
|
+
class DatasetBadge(Badge):
|
|
507
|
+
kind = db.StringField(required=True, choices=list(BADGES.keys()))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class DatasetBadgeMixin(BadgeMixin):
|
|
511
|
+
badges = field(BadgesList(DatasetBadge), **BadgeMixin.default_badges_list_params)
|
|
512
|
+
__badges__ = BADGES
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
502
516
|
title = db.StringField(required=True)
|
|
503
517
|
acronym = db.StringField(max_length=128)
|
|
504
518
|
# /!\ do not set directly the slug when creating or updating a dataset
|
|
@@ -539,10 +553,6 @@ class Dataset(WithMetrics, BadgeMixin, Owned, db.Document):
|
|
|
539
553
|
def __str__(self):
|
|
540
554
|
return self.title or ""
|
|
541
555
|
|
|
542
|
-
__badges__ = {
|
|
543
|
-
PIVOTAL_DATA: _("Pivotal data"),
|
|
544
|
-
}
|
|
545
|
-
|
|
546
556
|
__metrics_keys__ = [
|
|
547
557
|
"discussions",
|
|
548
558
|
"reuses",
|
udata/core/dataset/search.py
CHANGED
|
@@ -32,8 +32,9 @@ class DatasetSearch(ModelSearchAdapter):
|
|
|
32
32
|
|
|
33
33
|
filters = {
|
|
34
34
|
"tag": Filter(),
|
|
35
|
-
"badge": Filter(),
|
|
35
|
+
"badge": Filter(choices=list(Dataset.__badges__)),
|
|
36
36
|
"organization": ModelTermsFilter(model=Organization),
|
|
37
|
+
"organization_badge": Filter(choices=list(Organization.__badges__)),
|
|
37
38
|
"owner": ModelTermsFilter(model=User),
|
|
38
39
|
"license": ModelTermsFilter(model=License),
|
|
39
40
|
"geozone": ModelTermsFilter(model=GeoZone),
|
|
@@ -51,7 +52,7 @@ class DatasetSearch(ModelSearchAdapter):
|
|
|
51
52
|
|
|
52
53
|
@classmethod
|
|
53
54
|
def mongo_search(cls, args):
|
|
54
|
-
datasets = Dataset.objects(
|
|
55
|
+
datasets = Dataset.objects.visible()
|
|
55
56
|
datasets = DatasetApiParser.parse_filters(datasets, args)
|
|
56
57
|
|
|
57
58
|
sort = (
|
|
@@ -59,8 +60,7 @@ class DatasetSearch(ModelSearchAdapter):
|
|
|
59
60
|
or ("$text_score" if args["q"] else None)
|
|
60
61
|
or DEFAULT_SORTING
|
|
61
62
|
)
|
|
62
|
-
|
|
63
|
-
return datasets.order_by(sort).skip(offset).limit(args["page_size"]), datasets.count()
|
|
63
|
+
return datasets.order_by(sort).paginate(args["page"], args["page_size"])
|
|
64
64
|
|
|
65
65
|
@classmethod
|
|
66
66
|
def serialize(cls, dataset):
|
|
@@ -76,6 +76,7 @@ class DatasetSearch(ModelSearchAdapter):
|
|
|
76
76
|
"name": org.name,
|
|
77
77
|
"public_service": 1 if org.public_service else 0,
|
|
78
78
|
"followers": org.metrics.get("followers", 0),
|
|
79
|
+
"badges": [badge.kind for badge in org.badges],
|
|
79
80
|
}
|
|
80
81
|
elif dataset.owner:
|
|
81
82
|
owner = User.objects(id=dataset.owner.id).first()
|
udata/core/organization/api.py
CHANGED
|
@@ -63,6 +63,15 @@ class OrgApiParser(ModelApiParser):
|
|
|
63
63
|
"last_modified": "last_modified",
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
def __init__(self):
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.parser.add_argument(
|
|
69
|
+
"badge",
|
|
70
|
+
type=str,
|
|
71
|
+
choices=list(Organization.__badges__),
|
|
72
|
+
location="args",
|
|
73
|
+
)
|
|
74
|
+
|
|
66
75
|
@staticmethod
|
|
67
76
|
def parse_filters(organizations, args):
|
|
68
77
|
if args.get("q"):
|
|
@@ -72,6 +81,8 @@ class OrgApiParser(ModelApiParser):
|
|
|
72
81
|
# between tokens whereas an OR is used without it.
|
|
73
82
|
phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
74
83
|
organizations = organizations.search_text(phrase_query)
|
|
84
|
+
if args.get("badge"):
|
|
85
|
+
organizations = organizations.with_badge(args["badge"])
|
|
75
86
|
return organizations
|
|
76
87
|
|
|
77
88
|
|
|
@@ -5,7 +5,8 @@ from blinker import Signal
|
|
|
5
5
|
from mongoengine.signals import post_save, pre_save
|
|
6
6
|
from werkzeug.utils import cached_property
|
|
7
7
|
|
|
8
|
-
from udata.
|
|
8
|
+
from udata.api_fields import field
|
|
9
|
+
from udata.core.badges.models import Badge, BadgeMixin, BadgesList
|
|
9
10
|
from udata.core.metrics.models import WithMetrics
|
|
10
11
|
from udata.core.storages import avatars, default_image_basename
|
|
11
12
|
from udata.frontend.markdown import mdstrip
|
|
@@ -29,6 +30,14 @@ from .constants import (
|
|
|
29
30
|
|
|
30
31
|
__all__ = ("Organization", "Team", "Member", "MembershipRequest")
|
|
31
32
|
|
|
33
|
+
BADGES: dict[str, str] = {
|
|
34
|
+
PUBLIC_SERVICE: _("Public Service"),
|
|
35
|
+
CERTIFIED: _("Certified"),
|
|
36
|
+
ASSOCIATION: _("Association"),
|
|
37
|
+
COMPANY: _("Company"),
|
|
38
|
+
LOCAL_AUTHORITY: _("Local authority"),
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
|
|
33
42
|
class Team(db.EmbeddedDocument):
|
|
34
43
|
name = db.StringField(required=True)
|
|
@@ -82,8 +91,20 @@ class OrganizationQuerySet(db.BaseQuerySet):
|
|
|
82
91
|
def get_by_id_or_slug(self, id_or_slug):
|
|
83
92
|
return self(slug=id_or_slug).first() or self(id=id_or_slug).first()
|
|
84
93
|
|
|
94
|
+
def with_badge(self, kind):
|
|
95
|
+
return self(badges__kind=kind)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class OrganizationBadge(Badge):
|
|
99
|
+
kind = db.StringField(required=True, choices=list(BADGES.keys()))
|
|
100
|
+
|
|
85
101
|
|
|
86
|
-
class
|
|
102
|
+
class OrganizationBadgeMixin(BadgeMixin):
|
|
103
|
+
badges = field(BadgesList(OrganizationBadge), **BadgeMixin.default_badges_list_params)
|
|
104
|
+
__badges__ = BADGES
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Organization(WithMetrics, OrganizationBadgeMixin, db.Datetimed, db.Document):
|
|
87
108
|
name = db.StringField(required=True)
|
|
88
109
|
acronym = db.StringField(max_length=128)
|
|
89
110
|
slug = db.SlugField(
|
|
@@ -126,14 +147,6 @@ class Organization(WithMetrics, BadgeMixin, db.Datetimed, db.Document):
|
|
|
126
147
|
def __str__(self):
|
|
127
148
|
return self.name or ""
|
|
128
149
|
|
|
129
|
-
__badges__ = {
|
|
130
|
-
PUBLIC_SERVICE: _("Public Service"),
|
|
131
|
-
CERTIFIED: _("Certified"),
|
|
132
|
-
ASSOCIATION: _("Association"),
|
|
133
|
-
COMPANY: _("Company"),
|
|
134
|
-
LOCAL_AUTHORITY: _("Local authority"),
|
|
135
|
-
}
|
|
136
|
-
|
|
137
150
|
__metrics_keys__ = [
|
|
138
151
|
"datasets",
|
|
139
152
|
"members",
|
|
@@ -3,7 +3,7 @@ import datetime
|
|
|
3
3
|
from udata import search
|
|
4
4
|
from udata.core.organization.api import DEFAULT_SORTING, OrgApiParser
|
|
5
5
|
from udata.models import Organization
|
|
6
|
-
from udata.search.fields import
|
|
6
|
+
from udata.search.fields import ModelTermsFilter
|
|
7
7
|
from udata.utils import to_iso_datetime
|
|
8
8
|
|
|
9
9
|
__all__ = ("OrganizationSearch",)
|
|
@@ -22,7 +22,11 @@ class OrganizationSearch(search.ModelSearchAdapter):
|
|
|
22
22
|
"created": "created_at",
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
filters = {
|
|
25
|
+
filters = {
|
|
26
|
+
"badge": ModelTermsFilter(
|
|
27
|
+
model=Organization, field_name="badges", choices=list(Organization.__badges__)
|
|
28
|
+
),
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
@classmethod
|
|
28
32
|
def is_indexable(cls, org):
|
|
@@ -30,7 +34,7 @@ class OrganizationSearch(search.ModelSearchAdapter):
|
|
|
30
34
|
|
|
31
35
|
@classmethod
|
|
32
36
|
def mongo_search(cls, args):
|
|
33
|
-
orgs = Organization.objects(
|
|
37
|
+
orgs = Organization.objects.visible()
|
|
34
38
|
orgs = OrgApiParser.parse_filters(orgs, args)
|
|
35
39
|
|
|
36
40
|
sort = (
|
|
@@ -38,8 +42,7 @@ class OrganizationSearch(search.ModelSearchAdapter):
|
|
|
38
42
|
or ("$text_score" if args["q"] else None)
|
|
39
43
|
or DEFAULT_SORTING
|
|
40
44
|
)
|
|
41
|
-
|
|
42
|
-
return orgs.order_by(sort).skip(offset).limit(args["page_size"]), orgs.count()
|
|
45
|
+
return orgs.order_by(sort).paginate(args["page"], args["page_size"])
|
|
43
46
|
|
|
44
47
|
@classmethod
|
|
45
48
|
def serialize(cls, organization):
|
udata/core/reuse/api.py
CHANGED
|
@@ -13,6 +13,7 @@ from udata.core.badges import api as badges_api
|
|
|
13
13
|
from udata.core.badges.fields import badge_fields
|
|
14
14
|
from udata.core.dataset.api_fields import dataset_ref_fields
|
|
15
15
|
from udata.core.followers.api import FollowAPI
|
|
16
|
+
from udata.core.organization.models import Organization
|
|
16
17
|
from udata.core.reuse.constants import REUSE_TOPICS, REUSE_TYPES
|
|
17
18
|
from udata.core.storages.api import (
|
|
18
19
|
image_parser,
|
|
@@ -49,6 +50,12 @@ class ReuseApiParser(ModelApiParser):
|
|
|
49
50
|
self.parser.add_argument("dataset", type=str, location="args")
|
|
50
51
|
self.parser.add_argument("tag", type=str, location="args")
|
|
51
52
|
self.parser.add_argument("organization", type=str, location="args")
|
|
53
|
+
self.parser.add_argument(
|
|
54
|
+
"organization_badge",
|
|
55
|
+
type=str,
|
|
56
|
+
choices=list(Organization.__badges__),
|
|
57
|
+
location="args",
|
|
58
|
+
)
|
|
52
59
|
self.parser.add_argument("owner", type=str, location="args")
|
|
53
60
|
self.parser.add_argument("type", type=str, location="args")
|
|
54
61
|
self.parser.add_argument("topic", type=str, location="args")
|
|
@@ -79,6 +86,9 @@ class ReuseApiParser(ModelApiParser):
|
|
|
79
86
|
if not ObjectId.is_valid(args["organization"]):
|
|
80
87
|
api.abort(400, "Organization arg must be an identifier")
|
|
81
88
|
reuses = reuses.filter(organization=args["organization"])
|
|
89
|
+
if args.get("organization_badge"):
|
|
90
|
+
orgs = Organization.objects.with_badge(args["organization_badge"])
|
|
91
|
+
reuses = reuses.filter(organization__in=orgs)
|
|
82
92
|
if args.get("owner"):
|
|
83
93
|
if not ObjectId.is_valid(args["owner"]):
|
|
84
94
|
api.abort(400, "Owner arg must be an identifier")
|
udata/core/reuse/models.py
CHANGED
|
@@ -9,7 +9,7 @@ from udata.core.reuse.api_fields import BIGGEST_IMAGE_SIZE
|
|
|
9
9
|
from udata.core.storages import default_image_basename, images
|
|
10
10
|
from udata.frontend.markdown import mdstrip
|
|
11
11
|
from udata.i18n import lazy_gettext as _
|
|
12
|
-
from udata.models import BadgeMixin, WithMetrics, db
|
|
12
|
+
from udata.models import Badge, BadgeMixin, BadgesList, WithMetrics, db
|
|
13
13
|
from udata.mongo.errors import FieldValidationError
|
|
14
14
|
from udata.uris import endpoint_for
|
|
15
15
|
from udata.utils import hash_url
|
|
@@ -18,6 +18,8 @@ from .constants import IMAGE_MAX_SIZE, IMAGE_SIZES, REUSE_TOPICS, REUSE_TYPES
|
|
|
18
18
|
|
|
19
19
|
__all__ = ("Reuse",)
|
|
20
20
|
|
|
21
|
+
BADGES: dict[str, str] = {}
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class ReuseQuerySet(OwnedQuerySet):
|
|
23
25
|
def visible(self):
|
|
@@ -33,15 +35,25 @@ def check_url_does_not_exists(url):
|
|
|
33
35
|
raise FieldValidationError(_("This URL is already registered"), field="url")
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
class ReuseBadge(Badge):
|
|
39
|
+
kind = db.StringField(required=True, choices=list(BADGES.keys()))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReuseBadgeMixin(BadgeMixin):
|
|
43
|
+
badges = field(BadgesList(ReuseBadge), **BadgeMixin.default_badges_list_params)
|
|
44
|
+
__badges__ = BADGES
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
@generate_fields(
|
|
37
48
|
searchable=True,
|
|
38
|
-
|
|
49
|
+
additional_sorts=[
|
|
39
50
|
{"key": "datasets", "value": "metrics.datasets"},
|
|
40
51
|
{"key": "followers", "value": "metrics.followers"},
|
|
41
52
|
{"key": "views", "value": "metrics.views"},
|
|
42
53
|
],
|
|
54
|
+
additional_filters={"organization_badge": "organization.badges"},
|
|
43
55
|
)
|
|
44
|
-
class Reuse(db.Datetimed, WithMetrics,
|
|
56
|
+
class Reuse(db.Datetimed, WithMetrics, ReuseBadgeMixin, Owned, db.Document):
|
|
45
57
|
title = field(
|
|
46
58
|
db.StringField(required=True),
|
|
47
59
|
sortable=True,
|
|
@@ -124,8 +136,6 @@ class Reuse(db.Datetimed, WithMetrics, BadgeMixin, Owned, db.Document):
|
|
|
124
136
|
def __str__(self):
|
|
125
137
|
return self.title or ""
|
|
126
138
|
|
|
127
|
-
__badges__ = {}
|
|
128
|
-
|
|
129
139
|
__metrics_keys__ = [
|
|
130
140
|
"discussions",
|
|
131
141
|
"datasets",
|
udata/core/reuse/search.py
CHANGED
|
@@ -29,9 +29,10 @@ class ReuseSearch(ModelSearchAdapter):
|
|
|
29
29
|
filters = {
|
|
30
30
|
"tag": Filter(),
|
|
31
31
|
"organization": ModelTermsFilter(model=Organization),
|
|
32
|
+
"organization_badge": Filter(choices=list(Organization.__badges__)),
|
|
32
33
|
"owner": ModelTermsFilter(model=User),
|
|
33
34
|
"type": Filter(),
|
|
34
|
-
"badge": Filter(),
|
|
35
|
+
"badge": Filter(choices=list(Reuse.__badges__)),
|
|
35
36
|
"featured": BoolFilter(),
|
|
36
37
|
"topic": Filter(),
|
|
37
38
|
"archived": BoolFilter(),
|
|
@@ -43,7 +44,7 @@ class ReuseSearch(ModelSearchAdapter):
|
|
|
43
44
|
|
|
44
45
|
@classmethod
|
|
45
46
|
def mongo_search(cls, args):
|
|
46
|
-
reuses = Reuse.objects(
|
|
47
|
+
reuses = Reuse.objects.visible()
|
|
47
48
|
reuses = ReuseApiParser.parse_filters(reuses, args)
|
|
48
49
|
|
|
49
50
|
sort = (
|
|
@@ -51,8 +52,7 @@ class ReuseSearch(ModelSearchAdapter):
|
|
|
51
52
|
or ("$text_score" if args["q"] else None)
|
|
52
53
|
or DEFAULT_SORTING
|
|
53
54
|
)
|
|
54
|
-
|
|
55
|
-
return reuses.order_by(sort).skip(offset).limit(args["page_size"]), reuses.count()
|
|
55
|
+
return reuses.order_by(sort).paginate(args["page"], args["page_size"])
|
|
56
56
|
|
|
57
57
|
@classmethod
|
|
58
58
|
def serialize(cls, reuse: Reuse) -> dict:
|
|
@@ -65,6 +65,7 @@ class ReuseSearch(ModelSearchAdapter):
|
|
|
65
65
|
"name": org.name,
|
|
66
66
|
"public_service": 1 if org.public_service else 0,
|
|
67
67
|
"followers": org.metrics.get("followers", 0),
|
|
68
|
+
"badges": [badge.kind for badge in org.badges],
|
|
68
69
|
}
|
|
69
70
|
elif reuse.owner:
|
|
70
71
|
owner = User.objects(id=reuse.owner.id).first()
|
udata/search/fields.py
CHANGED
|
@@ -19,8 +19,12 @@ OR_SEPARATOR = "|"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Filter:
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
def __init__(self, choices=None):
|
|
23
|
+
self.choices = choices
|
|
24
|
+
|
|
25
|
+
def as_request_parser_kwargs(self):
|
|
26
|
+
if self.choices:
|
|
27
|
+
return {"type": clean_string, "choices": self.choices}
|
|
24
28
|
return {"type": clean_string}
|
|
25
29
|
|
|
26
30
|
|
|
@@ -31,9 +35,10 @@ class BoolFilter(Filter):
|
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class ModelTermsFilter(Filter):
|
|
34
|
-
def __init__(self, model, field_name="id"):
|
|
38
|
+
def __init__(self, model, field_name="id", choices=None):
|
|
35
39
|
self.model = model
|
|
36
40
|
self.field_name = field_name
|
|
41
|
+
super().__init__(choices=choices)
|
|
37
42
|
|
|
38
43
|
@property
|
|
39
44
|
def model_field(self):
|
udata/search/query.py
CHANGED
|
@@ -54,8 +54,10 @@ class SearchQuery:
|
|
|
54
54
|
"sort": self.sort,
|
|
55
55
|
}
|
|
56
56
|
query_args.update(self._filters)
|
|
57
|
-
result
|
|
58
|
-
return SearchResult(
|
|
57
|
+
result = self.adapter.mongo_search(query_args)
|
|
58
|
+
return SearchResult(
|
|
59
|
+
query=self, mongo_objects=list(result), total=result.total, **query_args
|
|
60
|
+
)
|
|
59
61
|
|
|
60
62
|
def to_url(self, url=None, replace=False, **kwargs):
|
|
61
63
|
"""Serialize the query into an URL"""
|