udata 9.0.1.dev29667__py2.py3-none-any.whl → 9.0.1.dev29696__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.

@@ -0,0 +1,19 @@
1
+ import factory
2
+
3
+ from udata.core.dataservices.models import Dataservice
4
+ from udata.core.organization.factories import OrganizationFactory
5
+ from udata.factories import ModelFactory
6
+
7
+
8
+ class DataserviceFactory(ModelFactory):
9
+ class Meta:
10
+ model = Dataservice
11
+
12
+ title = factory.Faker('sentence')
13
+ description = factory.Faker('text')
14
+ base_api_url = factory.Faker('url')
15
+
16
+ class Params:
17
+ org = factory.Trait(
18
+ organization=factory.SubFactory(OrganizationFactory),
19
+ )
@@ -1,13 +1,14 @@
1
1
 
2
2
  from datetime import datetime
3
3
  from typing import List, Optional
4
- from rdflib import RDF, Graph, URIRef
4
+ from rdflib import RDF, BNode, Graph, Literal, URIRef
5
5
 
6
6
  from udata.core.dataservices.models import Dataservice, HarvestMetadata as HarvestDataserviceMetadata
7
7
  from udata.core.dataset.models import Dataset, License
8
- from udata.core.dataset.rdf import sanitize_html
8
+ from udata.core.dataset.rdf import dataset_to_graph_id, sanitize_html
9
9
  from udata.harvest.models import HarvestSource
10
- from udata.rdf import DCAT, DCT, contact_point_from_rdf, rdf_value, remote_url_from_rdf, theme_labels_from_rdf, themes_from_rdf, url_from_rdf
10
+ from udata.rdf import DCATAP, TAG_TO_EU_HVD_CATEGORIES, namespace_manager, DCAT, DCT, contact_point_from_rdf, rdf_value, remote_url_from_rdf, themes_from_rdf, url_from_rdf
11
+ from udata.uris import endpoint_for
11
12
 
12
13
  def dataservice_from_rdf(graph: Graph, dataservice: Dataservice, node, all_datasets: List[Dataset]) -> Dataservice :
13
14
  '''
@@ -55,4 +56,54 @@ def dataservice_from_rdf(graph: Graph, dataservice: Dataservice, node, all_datas
55
56
 
56
57
  dataservice.tags = themes_from_rdf(d)
57
58
 
58
- return dataservice
59
+ return dataservice
60
+
61
+
62
+ def dataservice_to_rdf(dataservice, graph=None):
63
+ '''
64
+ Map a dataservice domain model to a DCAT/RDF graph
65
+ '''
66
+ # Use the unlocalized permalink to the dataset as URI when available
67
+ # unless there is already an upstream URI
68
+ if dataservice.harvest and dataservice.harvest.rdf_node_id_as_url:
69
+ id = URIRef(dataservice.harvest.rdf_node_id_as_url)
70
+ elif dataservice.id:
71
+ id = URIRef(endpoint_for('dataservices.show_redirect', 'api.dataservice',
72
+ dataservice=dataservice.id, _external=True))
73
+ else:
74
+ # Should not happen in production. Some test only
75
+ # `build()` a dataset without saving it to the DB.
76
+ id = BNode()
77
+
78
+ # Expose upstream identifier if present
79
+ if dataservice.harvest and dataservice.harvest.dct_identifier:
80
+ identifier = dataservice.harvest.dct_identifier
81
+ else:
82
+ identifier = dataservice.id
83
+ graph = graph or Graph(namespace_manager=namespace_manager)
84
+
85
+ d = graph.resource(id)
86
+ d.set(RDF.type, DCAT.DataService)
87
+ d.set(DCT.identifier, Literal(identifier))
88
+ d.set(DCT.title, Literal(dataservice.title))
89
+ d.set(DCT.description, Literal(dataservice.description))
90
+ d.set(DCT.issued, Literal(dataservice.created_at))
91
+
92
+ if dataservice.base_api_url:
93
+ d.set(DCAT.endpointURL, Literal(dataservice.base_api_url))
94
+
95
+ if dataservice.endpoint_description_url:
96
+ d.set(DCAT.endpointDescription, Literal(dataservice.endpoint_description_url))
97
+
98
+ for tag in dataservice.tags:
99
+ d.add(DCAT.keyword, Literal(tag))
100
+
101
+ # `dataset_to_graph_id(dataset)` URIRef may not exist in the current page
102
+ # but should exists in the catalog somewhere. Maybe we should create a Node
103
+ # with some basic information about this dataset (but this will return a page
104
+ # with more datasets than the page size… and could be problematic when processing the
105
+ # correct Node with all the information in a future page)
106
+ for dataset in dataservice.datasets:
107
+ d.add(DCAT.servesDataset, dataset_to_graph_id(dataset))
108
+
109
+ return d
udata/core/dataset/rdf.py CHANGED
@@ -6,7 +6,7 @@ import json
6
6
  import logging
7
7
 
8
8
  from datetime import date
9
- from typing import Optional
9
+ from typing import Optional, Union
10
10
  from dateutil.parser import parse as parse_dt
11
11
  from flask import current_app
12
12
  from geomet import wkt
@@ -149,19 +149,25 @@ def resource_to_rdf(resource, dataset=None, graph=None, is_hvd=False):
149
149
  return r
150
150
 
151
151
 
152
+ def dataset_to_graph_id(dataset: Dataset) -> Union[URIRef, BNode]:
153
+ if dataset.harvest and dataset.harvest.uri:
154
+ return URIRef(dataset.harvest.uri)
155
+ elif dataset.id:
156
+ return URIRef(endpoint_for('datasets.show_redirect', 'api.dataset',
157
+ dataset=dataset.id, _external=True))
158
+ else:
159
+ # Should not happen in production. Some test only
160
+ # `build()` a dataset without saving it to the DB.
161
+ return BNode()
162
+
152
163
  def dataset_to_rdf(dataset, graph=None):
153
164
  '''
154
165
  Map a dataset domain model to a DCAT/RDF graph
155
166
  '''
156
167
  # Use the unlocalized permalink to the dataset as URI when available
157
168
  # unless there is already an upstream URI
158
- if dataset.harvest and dataset.harvest.uri:
159
- id = URIRef(dataset.harvest.uri)
160
- elif dataset.id:
161
- id = URIRef(endpoint_for('datasets.show_redirect', 'api.dataset',
162
- dataset=dataset.id, _external=True))
163
- else:
164
- id = BNode()
169
+ id = dataset_to_graph_id(dataset)
170
+
165
171
  # Expose upstream identifier if present
166
172
  if dataset.harvest and dataset.harvest.dct_identifier:
167
173
  identifier = dataset.harvest.dct_identifier
@@ -1,6 +1,8 @@
1
1
  import logging
2
2
  from datetime import datetime
3
3
 
4
+ from flask_login import current_user
5
+
4
6
  from udata.mongo import db
5
7
  from udata.core.spam.models import SpamMixin, spam_protected
6
8
  from .signals import (on_new_discussion, on_discussion_closed, on_new_discussion_comment)
@@ -67,6 +69,24 @@ class Discussion(SpamMixin, db.Document):
67
69
  def embeds_to_check_for_spam(self):
68
70
  return self.discussion[1:]
69
71
 
72
+ def spam_is_whitelisted(self) -> bool:
73
+ from udata.core.dataset.permissions import OwnablePermission
74
+ from udata.core.owned import Owned
75
+
76
+ if not current_user.is_authenticated:
77
+ return False
78
+
79
+ if not isinstance(self.subject, Owned):
80
+ return False
81
+
82
+ # When creating a new Discussion the `subject` is an empty model
83
+ # with only `id`. We need to fetch it from the database to have
84
+ # all the required information
85
+ if not self.subject.owner or not self.subject.organization:
86
+ self.subject.reload()
87
+
88
+ return OwnablePermission(self.subject).can()
89
+
70
90
  @property
71
91
  def external_url(self):
72
92
  return self.subject.url_for(
udata/core/site/api.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from bson import ObjectId
2
2
 
3
3
  from flask import request, redirect, url_for, json, make_response
4
+ from mongoengine import Q
4
5
 
5
6
  from udata.api import api, API, fields
6
7
  from udata.auth import admin_permission
8
+ from udata.core.dataservices.models import Dataservice
7
9
  from udata.models import Dataset, Reuse
8
10
  from udata.utils import multi_to_dict
9
11
  from udata.rdf import (
@@ -109,7 +111,33 @@ class SiteRdfCatalogFormat(API):
109
111
  if 'tag' in params:
110
112
  datasets = datasets.filter(tags=params.get('tag', ''))
111
113
  datasets = datasets.paginate(page, page_size)
112
- catalog = build_catalog(current_site, datasets, format=format)
114
+
115
+ # We need to add Dataservice to the catalog.
116
+ # In the best world, we want:
117
+ # - Keep the correct number of datasets on the page (if the requested page size is 100, we should have 100 datasets)
118
+ # - Have simple MongoDB queries
119
+ # - Do not duplicate the datasets (each dataset is present once in the catalog)
120
+ # - Do not duplicate the dataservices (each dataservice is present once in the catalog)
121
+ # - Every referenced dataset for one dataservices present on the page (hard to do)
122
+ #
123
+ # Multiple solutions are possible but none check all the constraints.
124
+ # The selected one is to put all the dataservices referencing at least one of the dataset on
125
+ # the page at the end of it. It means dataservices could be duplicated (present on multiple pages)
126
+ # and these dataservices may referenced some datasets not present in the current page. It's working
127
+ # if somebody is doing the same thing as us (keeping the list of all the datasets IDs for the entire catalog then
128
+ # listing all dataservices in a second pass)
129
+ # Another option is to do some tricky Mongo requests to order/group datasets by their presence in some dataservices but
130
+ # it could be really hard to do with a n..n relation.
131
+ # Let's keep this solution simple right now and iterate on it in the future.
132
+ dataservices_filter = Q(datasets__in=[d.id for d in datasets])
133
+
134
+ # On the first page, add all dataservices without datasets
135
+ if page == 1:
136
+ dataservices_filter = dataservices_filter | Q(datasets__size=0)
137
+
138
+ dataservices = Dataservice.objects.visible().filter(dataservices_filter)
139
+
140
+ catalog = build_catalog(current_site, datasets, dataservices=dataservices, format=format)
113
141
  # bypass flask-restplus make_response, since graph_response
114
142
  # is handling the content negociation directly
115
143
  return make_response(*graph_response(catalog, format))
udata/core/site/rdf.py CHANGED
@@ -5,6 +5,7 @@ from flask import url_for, current_app
5
5
  from rdflib import Graph, URIRef, Literal, BNode
6
6
  from rdflib.namespace import RDF, FOAF
7
7
 
8
+ from udata.core.dataservices.rdf import dataservice_to_rdf
8
9
  from udata.core.dataset.rdf import dataset_to_rdf
9
10
  from udata.core.organization.rdf import organization_to_rdf
10
11
  from udata.core.user.rdf import user_to_rdf
@@ -13,7 +14,7 @@ from udata.utils import Paginable
13
14
  from udata.uris import endpoint_for
14
15
 
15
16
 
16
- def build_catalog(site, datasets, format=None):
17
+ def build_catalog(site, datasets, dataservices = [], format=None):
17
18
  '''Build the DCAT catalog for this site'''
18
19
  site_url = endpoint_for('site.home_redirect', 'api.site', _external=True)
19
20
  catalog_url = url_for('api.site_rdf_catalog', _external=True)
@@ -40,6 +41,10 @@ def build_catalog(site, datasets, format=None):
40
41
  rdf_dataset.add(DCT.publisher, organization_to_rdf(dataset.organization, graph))
41
42
  catalog.add(DCAT.dataset, rdf_dataset)
42
43
 
44
+ for dataservice in dataservices:
45
+ rdf_dataservice = dataservice_to_rdf(dataservice, graph)
46
+ catalog.add(DCAT.DataService, rdf_dataservice)
47
+
43
48
  if isinstance(datasets, Paginable):
44
49
  paginate_catalog(catalog, graph, datasets, format, 'api.site_rdf_catalog_format')
45
50
 
udata/core/spam/models.py CHANGED
@@ -67,6 +67,9 @@ class SpamMixin(object):
67
67
  if not self.spam:
68
68
  self.spam = SpamInfo(status=NOT_CHECKED, callbacks={})
69
69
 
70
+ if self.spam_is_whitelisted():
71
+ return
72
+
70
73
  # The breadcrumb is useful during reporting to know where we came from
71
74
  # in case of a potential spam inside an embed.
72
75
  if breadcrumb is None:
@@ -139,6 +142,9 @@ class SpamMixin(object):
139
142
  def embeds_to_check_for_spam(self):
140
143
  return []
141
144
 
145
+ def spam_is_whitelisted(self) -> bool :
146
+ return False
147
+
142
148
  def spam_report_message(self):
143
149
  return f"Spam potentiel sur {type(self).__name__}"
144
150
 
udata/tests/plugin.py CHANGED
@@ -8,6 +8,7 @@ from flask import json, template_rendered, url_for, current_app
8
8
  from flask.testing import FlaskClient
9
9
  from lxml import etree
10
10
  from werkzeug.urls import url_encode
11
+ from flask_principal import Identity, identity_changed
11
12
 
12
13
  from udata import settings
13
14
  from udata.app import create_app
@@ -49,6 +50,7 @@ class TestClient(FlaskClient):
49
50
  session['_fresh'] = True
50
51
  session['_id'] = current_app.login_manager._session_identifier_generator()
51
52
  current_app.login_manager._update_request_context_with_user(user)
53
+ identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
52
54
  return user
53
55
 
54
56
  def logout(self):
@@ -6,6 +6,7 @@ from rdflib import URIRef, Literal, Graph
6
6
  from rdflib.namespace import RDF, FOAF
7
7
  from rdflib.resource import Resource
8
8
 
9
+ from udata.core.dataservices.factories import DataserviceFactory
9
10
  from udata.core.dataset.factories import DatasetFactory
10
11
  from udata.core.dataset.models import Dataset
11
12
  from udata.core.organization.factories import OrganizationFactory
@@ -244,3 +245,52 @@ class SiteRdfViewsTest:
244
245
 
245
246
  for dat in datasets:
246
247
  assert graph.value(dat, DCAT.keyword) == Literal('my-tag')
248
+
249
+ def test_catalog_rdf_dataservices(self, client):
250
+ dataset_a = DatasetFactory.create()
251
+ dataset_b = DatasetFactory.create()
252
+ dataset_c = DatasetFactory.create()
253
+
254
+ dataservice_a = DataserviceFactory.create(datasets=[dataset_a.id])
255
+ dataservice_b = DataserviceFactory.create(datasets=[dataset_b.id])
256
+ dataservice_x = DataserviceFactory.create(datasets=[dataset_a.id, dataset_c.id])
257
+ dataservice_y = DataserviceFactory.create(datasets=[])
258
+
259
+ response = client.get(url_for('api.site_rdf_catalog_format', format='xml'), headers={'Accept': 'application/xml'})
260
+ assert200(response)
261
+
262
+ graph = Graph().parse(data=response.data, format='xml')
263
+
264
+ datasets = list(graph.subjects(RDF.type, DCAT.Dataset))
265
+ assert len(datasets) == 3
266
+
267
+ dataservices = list(graph.subjects(RDF.type, DCAT.DataService))
268
+ assert len(dataservices) == 4
269
+
270
+ # Test first page contains the dataservice without dataset
271
+ response = client.get(url_for('api.site_rdf_catalog_format', format='xml', page_size=1), headers={'Accept': 'application/xml'})
272
+ assert200(response)
273
+
274
+ graph = Graph().parse(data=response.data, format='xml')
275
+
276
+ datasets = list(graph.subjects(RDF.type, DCAT.Dataset))
277
+ assert len(datasets) == 1
278
+ assert str(graph.value(datasets[0], DCT.identifier)) == str(dataset_c.id)
279
+
280
+ dataservices = list(graph.subjects(RDF.type, DCAT.DataService))
281
+ assert len(dataservices) == 2
282
+ assert sorted([str(d.id) for d in [dataservice_x, dataservice_y]]) == sorted([str(graph.value(d, DCT.identifier)) for d in dataservices])
283
+
284
+ # Test second page doesn't contains the dataservice without dataset
285
+ response = client.get(url_for('api.site_rdf_catalog_format', format='xml', page_size=1, page=2), headers={'Accept': 'application/xml'})
286
+ assert200(response)
287
+
288
+ graph = Graph().parse(data=response.data, format='xml')
289
+
290
+ datasets = list(graph.subjects(RDF.type, DCAT.Dataset))
291
+ assert len(datasets) == 1
292
+ assert str(graph.value(datasets[0], DCT.identifier)) == str(dataset_b.id)
293
+
294
+ dataservices = list(graph.subjects(RDF.type, DCAT.DataService))
295
+ assert len(dataservices) == 1
296
+ assert str(graph.value(dataservices[0], DCT.identifier)) == str(dataservice_b.id)
@@ -76,7 +76,7 @@ class DiscussionsTest(APITestCase):
76
76
  discussion_id = None
77
77
  def check_signal(args):
78
78
  self.assertIsNotNone(discussion_id)
79
- self.assertIn(f'http://local.test/api/1/datasets/{dataset.id}/#discussion-{discussion_id}', args[1]['message'])
79
+ self.assertIn(f'http://local.test/api/1/datasets/{dataset.slug}/#discussion-{discussion_id}', args[1]['message'])
80
80
 
81
81
  with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
82
82
  response = self.post(url_for('api.discussions'), {
@@ -122,6 +122,29 @@ class DiscussionsTest(APITestCase):
122
122
  self.assertStatus(response, 200)
123
123
  self.assertFalse(discussion.reload().is_spam())
124
124
 
125
+
126
+ @pytest.mark.options(SPAM_WORDS=['spam'])
127
+ def test_spam_by_owner(self):
128
+ user = self.login()
129
+ dataset = Dataset.objects.create(title='Test dataset', owner=user)
130
+
131
+ with assert_not_emit(on_new_potential_spam):
132
+ response = self.post(url_for('api.discussions'), {
133
+ 'title': 'spam and blah',
134
+ 'comment': 'bla bla',
135
+ 'subject': {
136
+ 'class': 'Dataset',
137
+ 'id': dataset.id,
138
+ }
139
+ })
140
+ self.assertStatus(response, 201)
141
+
142
+ with assert_not_emit(on_new_potential_spam):
143
+ response = self.post(url_for('api.discussion', id=response.json['id']), {
144
+ 'comment': 'A comment with spam by owner'
145
+ })
146
+ self.assertStatus(response, 200)
147
+
125
148
  @pytest.mark.options(SPAM_WORDS=['spam'])
126
149
  def test_spam_in_new_discussion_comment(self):
127
150
  self.login()
@@ -495,39 +518,6 @@ class DiscussionsTest(APITestCase):
495
518
  {'comment': "can't comment"})
496
519
  self.assert403(response)
497
520
 
498
- @pytest.mark.options(SPAM_WORDS=['spam'], SPAM_ALLOWED_LANGS=['fr'])
499
- def test_close_discussion_with_spam(self):
500
- owner = self.login()
501
- dataset = Dataset.objects.create(title='Test dataset', owner=owner)
502
- user = UserFactory()
503
- message = Message(content='Premier message', posted_by=user)
504
- discussion = Discussion.objects.create(
505
- subject=dataset,
506
- user=user,
507
- title='test discussion',
508
- discussion=[message]
509
- )
510
- on_new_discussion.send(discussion) # Updating metrics.
511
-
512
- with assert_not_emit(on_discussion_closed):
513
- with assert_emit(on_new_potential_spam):
514
- response = self.post(url_for('api.discussion', id=discussion.id), {
515
- 'comment': 'This is a suspicious, real suspicious message in english.',
516
- 'close': True,
517
- })
518
- self.assert200(response)
519
-
520
- discussion.reload()
521
- self.assertFalse(discussion.is_spam())
522
- self.assertTrue(discussion.discussion[1].is_spam())
523
- self.assertTrue('signal_close' in discussion.discussion[1].spam.callbacks)
524
-
525
- with assert_emit(on_discussion_closed):
526
- admin = self.login(AdminFactory())
527
- response = self.delete(url_for('api.discussion_comment_spam', id=discussion.id, cidx=1))
528
- self.assertStatus(response, 200)
529
- self.assertFalse(discussion.reload().discussion[1].is_spam())
530
-
531
521
  def test_close_discussion_permissions(self):
532
522
  dataset = Dataset.objects.create(title='Test dataset')
533
523
  user = UserFactory()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udata
3
- Version: 9.0.1.dev29667
3
+ Version: 9.0.1.dev29696
4
4
  Summary: Open data portal
5
5
  Home-page: https://github.com/opendatateam/udata
6
6
  Author: Opendata Team
@@ -147,7 +147,9 @@ It is collectively taken care of by members of the
147
147
  - Improve URL validation errors [#3063](https://github.com/opendatateam/udata/pull/3063) [#2768](https://github.com/opendatateam/udata/pull/2768)
148
148
  - Do not return full dataset objects on dataservices endpoints [#3068](https://github.com/opendatateam/udata/pull/3068)
149
149
  - Update markdown base settings [#3067](https://github.com/opendatateam/udata/pull/3067)
150
+ - Prevent tagging as spam owners' messages [#3071](https://github.com/opendatateam/udata/pull/3071)
150
151
  - Add api endpoint /me/org_topics/ [#3070](https://github.com/opendatateam/udata/pull/3070)
152
+ - Expose dataservices in RDF catalog [#3058](https://github.com/opendatateam/udata/pull/3058)
151
153
 
152
154
  ## 9.0.0 (2024-06-07)
153
155
 
@@ -80,9 +80,10 @@ udata/core/contact_point/forms.py,sha256=ggLhSJ1IRn5MclrhydckjAxwr4fFZxgAD4huSSu
80
80
  udata/core/contact_point/models.py,sha256=NlNKureCpzgTLJuGviZPjNx-ABYRp4j2L-ur9Gmixao,324
81
81
  udata/core/dataservices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  udata/core/dataservices/api.py,sha256=rjCU55NNGgCDRlurfhJUT2byBGJWN5coM8b7AApzEew,3090
83
+ udata/core/dataservices/factories.py,sha256=_6Rp1qGB7z3BbPcIshj8LNsxUpublq0sf4KVTjBziPI,506
83
84
  udata/core/dataservices/models.py,sha256=zMhzjnXm1p5GHZU1lYgMqft5u7iFyX0BIvNB1hM4D6Q,5868
84
85
  udata/core/dataservices/permissions.py,sha256=X9Bh8e0pnx6OgeEf6NowXZUiwyreUa6UY479B16cCqs,175
85
- udata/core/dataservices/rdf.py,sha256=TV02R2ZV_aDboSyl-4LJN1qG-5ibgrUYQZYmpl6Jqr4,2469
86
+ udata/core/dataservices/rdf.py,sha256=YfHI60f0uJq0kSUKxSvFJzskbRH7c9yOj1ugcH6iLpA,4654
86
87
  udata/core/dataservices/tasks.py,sha256=NOWcTPoLasMrrvq9EkwQMGlUbQQmi_l3s815K-mtZTM,971
87
88
  udata/core/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
89
  udata/core/dataset/actions.py,sha256=3pzBg_qOR-w7fwPpTOKUHXWC9lkjALbOn1UQFmmT-s0,1199
@@ -100,7 +101,7 @@ udata/core/dataset/forms.py,sha256=VJCsGtgzhQgLW-M-J4ObpQ8o6c_saC3TTc1Js33m3sQ,6
100
101
  udata/core/dataset/models.py,sha256=q6AknSHYNjeUyVKoFgxEA0r9lSvMa_a4tJ3fLOzL03M,36053
101
102
  udata/core/dataset/permissions.py,sha256=3F2J7le3_rEYNhh88o3hSRWHAAt01_yHJM6RPmvCrRo,1090
102
103
  udata/core/dataset/preview.py,sha256=puPKT3fBD7ezAcT6owh0JK1_rGNDFZOqgT223qGn3LY,2597
103
- udata/core/dataset/rdf.py,sha256=d76N8NovOOKhzyxMe3J7906GP-HV4Q8PEfiK3_wQip8,22465
104
+ udata/core/dataset/rdf.py,sha256=BujVPk510wLQzWPKZhZ1rGruQZSCr0hTDz9b1pmupxI,22695
104
105
  udata/core/dataset/search.py,sha256=Ca23ljX7TZiKZKiAZQIH5xwS2tjXQcTDzieAJd-E22c,5314
105
106
  udata/core/dataset/signals.py,sha256=TK6dfrOUitZZkGGOh6XmhYqYvIjzZpI70JTLV4k-JRM,161
106
107
  udata/core/dataset/tasks.py,sha256=UYdm_O9hA0yo7cCNQFvZE9pGzUshkc3Wr4Gpel2aupU,8434
@@ -111,7 +112,7 @@ udata/core/discussions/constants.py,sha256=nbZgXESpg0TykIGPxW8xUtHtk7TwQoyOL0Ky4
111
112
  udata/core/discussions/factories.py,sha256=NQd_tD0Izrm67uO5HuuClmluteACrRd9PHrb2IkQ0P0,350
112
113
  udata/core/discussions/forms.py,sha256=daDc8vPDhaXjiEyniugiRC6pyv6OsflgIyO-KoAn6i8,828
113
114
  udata/core/discussions/metrics.py,sha256=qtgyDhM1aPgh8bGU-h-962EKR3J44imC155JVi6jvJI,362
114
- udata/core/discussions/models.py,sha256=9DQ9pYyYdc1VVilaY44TKL_OMJP9_FgYwuo0w9ZZw5s,3360
115
+ udata/core/discussions/models.py,sha256=B9tgaN6rqR4DLfHR6_jiZnaadVNZn0n08zmJRh7TPm8,4041
115
116
  udata/core/discussions/notifications.py,sha256=1lsu8WyyOL4bdt0lx6IW5wTxmQ5gS_7FoncN53g3ToQ,927
116
117
  udata/core/discussions/permissions.py,sha256=q3tXNuNmuXCvZhKyudROcwXF53l-IeDR3pfKSh_hIL0,636
117
118
  udata/core/discussions/signals.py,sha256=zjuF-TiFwu9U_RcgZtHB2Z3W4oBx5hVZy6CCAl5Ekug,543
@@ -176,16 +177,16 @@ udata/core/reuse/search.py,sha256=I3FX-9YP_cEqw5GFwFv2iqLZExeaQXsak13RMSCN7t8,28
176
177
  udata/core/reuse/signals.py,sha256=2vv668u8miJ9t6H-FwRgXcWwsYMQem3oLNXp36rKtno,155
177
178
  udata/core/reuse/tasks.py,sha256=FgkUN6s1uTDFQgiImiOEfIMYYW984bxxDtlGK0uZ0yY,1530
178
179
  udata/core/site/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
- udata/core/site/api.py,sha256=UXsMV8klzP5wQJazk_HfQGpckvkYddbuF8UJ5u0OBc0,4383
180
+ udata/core/site/api.py,sha256=nsHTfjFQMp2gdWQk2Y-CY8HDdpoDlClYcJxJcc-xa9E,6199
180
181
  udata/core/site/factories.py,sha256=8-RwP6QRCxXqWF4UHIkm2NfWRP2p8Ktc5RSY-4jgnfk,378
181
182
  udata/core/site/forms.py,sha256=lPnxm0MDhK7_ORzPgJLcCAPMj59hDrJ80gwRSW-jtIo,469
182
183
  udata/core/site/models.py,sha256=96IZHtPaMio7VMNCdP1Km_SL1GlMErwBuDqgUXfzKVY,5981
183
- udata/core/site/rdf.py,sha256=ONKksnuZTFH9Hdd6BE42a1769zJynna68rG-M30PAZQ,1858
184
+ udata/core/site/rdf.py,sha256=2yP3oUxTZBuP96TFMnh_68lO2Xuzpgn1Zwehg0POK5Y,2094
184
185
  udata/core/spam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
186
  udata/core/spam/api.py,sha256=8tVRPorw56kxgN64kme5nLkUfh8Gai9QyqT8aNQn9Xo,1674
186
187
  udata/core/spam/constants.py,sha256=M-wvYlcFnpUDortccIKFHWZ45vbNuMPWSvqKm2itn4w,143
187
188
  udata/core/spam/fields.py,sha256=ppazY9bGnz7mujmDndbxG3pPG_1HDUJCbIufxyD1UNQ,310
188
- udata/core/spam/models.py,sha256=4ylTXJ0EbjvwlPUTCwTMDERe26fCqLjAstuvt2EwGN0,7968
189
+ udata/core/spam/models.py,sha256=sGA4TMXpKbyEbwW8Gk6JpPIiV91JQ4SxWjdVtpPIyls,8093
189
190
  udata/core/spam/signals.py,sha256=4VVLyC2I39LFAot4P56nHzY0xumjMBDz_N0Ff_kgBd0,159
190
191
  udata/core/spam/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
191
192
  udata/core/spam/tests/test_spam.py,sha256=W1Ck_rsnURhFi0fy5xOO0CPpW9MuUFbr-NmPZdk5R4Q,676
@@ -571,10 +572,10 @@ udata/tests/__init__.py,sha256=BezijRRI6dPPiEWWjLsJFLrhhfsTZgcctcxhVfp4j70,2332
571
572
  udata/tests/es-fake-result.json,sha256=z0CX9Gs-NRj49dmtdFvw8ZKsAbMhDt97Na6JX3ucX34,3155
572
573
  udata/tests/helpers.py,sha256=aaifyevJ1Z8CZ8htRrl8OCG5hGcaHfj0lL8iMEKds9w,6022
573
574
  udata/tests/models.py,sha256=_V0smMb1Z6p3aZv6PorzNN-HiNt_B46Ox1fqXrTJEqk,238
574
- udata/tests/plugin.py,sha256=DXP0H1Sm2fc-okGSKKBOgH8D8x4fl4_1OVhakgQLz4w,11278
575
+ udata/tests/plugin.py,sha256=p8TZcFvlywaLeMXLQOBjZ0wgJM8d11pLYmMtLmXjtxg,11430
575
576
  udata/tests/schemas.json,sha256=szM1jDpkogfOG4xWbjIGjLgG8l9-ZyE3JKQtecJyD1E,4990
576
577
  udata/tests/test_activity.py,sha256=spWfhueuLze0kD-pAnegiL3_Kv5io155jQuFI4sjN7I,3258
577
- udata/tests/test_discussions.py,sha256=zPvKOdcTNGXrvHFp9zqjhKE2fqgUkhb_1F98egXYCL0,31036
578
+ udata/tests/test_discussions.py,sha256=mNRA9PkAkUNLQRmbLjvjF2878yY5jsIuA0_wwiLCGHk,30395
578
579
  udata/tests/test_i18n.py,sha256=BU9E7OoIkJw5tv5JYGLjDGBDsti2HuQ_3OWDKnBxnaM,3191
579
580
  udata/tests/test_linkchecker.py,sha256=KxV1-PuuuqogkHf3jP6JhRsc2QG2dFmFB-vSHOiHkuU,10374
580
581
  udata/tests/test_mail.py,sha256=LK_fOBbFoqbwdaIEXO9XyGPzxY9nrT9FjyD_dlglUdQ,1643
@@ -667,7 +668,7 @@ udata/tests/site/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
667
668
  udata/tests/site/test_site_api.py,sha256=XIjRH-iiwKdwmX145bQflxOatfIChi5RLikIMwJSbjQ,2276
668
669
  udata/tests/site/test_site_metrics.py,sha256=Sn9dQORwe-fvoyA7ZAnF63Cq5CwUGq1myH8Xe0EtfDA,2263
669
670
  udata/tests/site/test_site_model.py,sha256=nAx9JjEKdfjdw1Kj5Rs7P5FEePoATtCuOYPiSXEnVD0,1313
670
- udata/tests/site/test_site_rdf.py,sha256=NR09qJo8vmyAmtn0qQb-_nat7OyNV2fvwZTqW12BvQA,10283
671
+ udata/tests/site/test_site_rdf.py,sha256=UeGzBkV2JW75lm7g4zMqFNHMj7QBFVI2xg0cmK25Elg,12695
671
672
  udata/tests/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
672
673
  udata/tests/user/test_user_rdf.py,sha256=HrKirMURUXS9N3If_NMb8qnfJ4kE9IZymR1SPcNvlF0,1851
673
674
  udata/tests/workers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -690,9 +691,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=iAUNwbI8ESi8MHkE3ZCYCSIXfFC27z
690
691
  udata/translations/pt/LC_MESSAGES/udata.po,sha256=uTmbHfzyFWrVXUkKSuNFzbGpX7EkUuBdD8fE04d3v5g,44572
691
692
  udata/translations/sr/LC_MESSAGES/udata.mo,sha256=1MbQHvKKNUwzMBWLNsH1qqBehO3aILhQiMhi5u1bY8E,28553
692
693
  udata/translations/sr/LC_MESSAGES/udata.po,sha256=AAryt27Gbkhk7FntCCU8_e7HSXATfsAQhwFOFC8CAj0,51152
693
- udata-9.0.1.dev29667.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
694
- udata-9.0.1.dev29667.dist-info/METADATA,sha256=jRGiYzGgQ4kjdR658hbqRitIq0Fl3XwiPfcKbR_eXmI,125149
695
- udata-9.0.1.dev29667.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
696
- udata-9.0.1.dev29667.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
697
- udata-9.0.1.dev29667.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
698
- udata-9.0.1.dev29667.dist-info/RECORD,,
694
+ udata-9.0.1.dev29696.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
695
+ udata-9.0.1.dev29696.dist-info/METADATA,sha256=7FzsYbr-nRy6TOozV0-1HW9Hm3WF03byQS1SSR4tfow,125343
696
+ udata-9.0.1.dev29696.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
697
+ udata-9.0.1.dev29696.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
698
+ udata-9.0.1.dev29696.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
699
+ udata-9.0.1.dev29696.dist-info/RECORD,,