Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add contact points for Orgs and Users #2914

Merged
merged 48 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8340022
Add contact points for Orgs and Users
quaxsze Oct 19, 2023
5831a91
fix model
quaxsze Oct 19, 2023
7eff183
fix model
quaxsze Oct 19, 2023
72aa78a
fix model
quaxsze Oct 19, 2023
0d996e4
add core feature
quaxsze Oct 24, 2023
12b7806
Merge branch 'master' into ContactPoint
quaxsze Oct 24, 2023
e80c424
add test task
quaxsze Oct 24, 2023
49d2b87
Merge branch 'master' into ContactPoint
quaxsze Oct 26, 2023
2530931
add missing init file
quaxsze Oct 26, 2023
de1e3a4
fix swagger tests
quaxsze Oct 26, 2023
f0da4fd
Merge branch 'master' into ContactPoint
quaxsze Oct 26, 2023
67a8c6c
Update udata/core/contact_points/api.py
quaxsze Nov 7, 2023
0543863
Merge branch 'master' into ContactPoint
quaxsze Nov 7, 2023
91d26d0
contact point are now db.owned
quaxsze Nov 8, 2023
1c33ff0
fixing part tests
quaxsze Nov 8, 2023
ddfa44b
fixing part 2 tests
quaxsze Nov 8, 2023
b17bef6
add detach endpoint for dataset
quaxsze Nov 8, 2023
ab0bf06
add dcat
quaxsze Nov 9, 2023
0375ae6
Merge branch 'master' into ContactPoint
quaxsze Nov 13, 2023
3b80a57
rename singular and not a list anymore
quaxsze Nov 20, 2023
9f57e03
Merge branch 'master' into ContactPoint
quaxsze Nov 20, 2023
3190c54
fix test
quaxsze Nov 20, 2023
ba57ecf
Merge branch 'ContactPoint' of github.com:opendatateam/udata into Con…
quaxsze Nov 20, 2023
957bbdb
fix test
quaxsze Nov 20, 2023
7759611
use fields
quaxsze Nov 21, 2023
70a6922
use plural
quaxsze Nov 21, 2023
a09dc47
Merge branch 'master' into ContactPoint
quaxsze Nov 21, 2023
04cdf11
fix test
quaxsze Nov 21, 2023
fab2c21
Merge branch 'ContactPoint' of github.com:opendatateam/udata into Con…
quaxsze Nov 21, 2023
e4b97ac
fix rdf contact_point
quaxsze Nov 21, 2023
c631604
fix test
quaxsze Nov 21, 2023
b5ccb23
fix test
quaxsze Nov 21, 2023
afeb4d4
Merge branch 'master' into ContactPoint
quaxsze Nov 22, 2023
3754411
Update udata/core/dataset/rdf.py
quaxsze Nov 27, 2023
13cd2c1
Merge branch 'master' into ContactPoint
quaxsze Nov 27, 2023
a6930f2
enhancement
quaxsze Nov 27, 2023
af9f8f9
enhancement
quaxsze Nov 28, 2023
b30804b
fix test
quaxsze Nov 29, 2023
721cd39
fix tests
quaxsze Dec 5, 2023
b1ea047
contact point in form
quaxsze Dec 13, 2023
3373bdf
Merge branch 'master' into ContactPoint
quaxsze Dec 13, 2023
da9f019
add test
quaxsze Dec 13, 2023
a909179
Merge branch 'ContactPoint' of github.com:opendatateam/udata into Con…
quaxsze Dec 13, 2023
8b5d6ef
add admin to get endpoint
quaxsze Dec 14, 2023
e003351
fix tests
quaxsze Dec 14, 2023
ff45f9f
remove useless import
quaxsze Dec 14, 2023
ea48ab9
changelog
quaxsze Dec 14, 2023
8cd5994
Update udata/core/dataset/models.py
quaxsze Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions udata/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ def init_app(app):
import udata.core.topic.api # noqa
import udata.core.topic.apiv2 # noqa
import udata.core.post.api # noqa
import udata.core.contact_point.api # noqa
import udata.features.transfer.api # noqa
import udata.features.notifications.api # noqa
import udata.features.identicon.api # noqa
Expand Down
Empty file.
68 changes: 68 additions & 0 deletions udata/core/contact_point/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from udata.api import api, API
from udata.api.parsers import ModelApiParser

from .api_fields import contact_point_fields, contact_point_page_fields
from .forms import ContactPointForm
from .models import ContactPoint


class ContactPointApiParser(ModelApiParser):
sorts = {}

def __init__(self):
super().__init__()


ns = api.namespace('contacts', 'Contact points related operations')

contact_point_parser = ContactPointApiParser()


@ns.route('/', endpoint='contact_points')
class ContactPointsListAPI(API):
'''Contact points collection endpoint'''
@api.doc('list_contact_points')
@api.marshal_with(contact_point_page_fields)
def get(self):
'''List all contact points'''
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
args = contact_point_parser.parse()
return ContactPoint.objects().paginate(args['page'], args['page_size'])

@api.secure
@api.doc('create_contact_point')
@api.expect(contact_point_fields)
@api.marshal_with(contact_point_fields)
@api.response(400, 'Validation error')
def post(self):
'''Creates a contact point'''
form = api.validate(ContactPointForm)
contact_point = form.save()
return contact_point, 201


@ns.route('/<contact_point:contact_point>/', endpoint='contact_point')
@api.response(404, 'Contact point not found')
class ContactPointAPI(API):
@api.doc('get_contact_point')
@api.marshal_with(contact_point_fields)
def get(self, contact_point):
'''Get a contact point given its identifier'''
return contact_point

@api.secure
@api.doc('update_contact_point')
@api.expect(contact_point_fields)
@api.marshal_with(contact_point_fields)
@api.response(400, 'Validation error')
def put(self, contact_point):
'''Updates a contact point given its identifier'''
form = api.validate(ContactPointForm, contact_point)
return form.save()

@api.secure
@api.doc('delete_contact_point')
@api.response(204, 'Contact point deleted')
def delete(self, contact_point):
'''Deletes a contact point given its identifier'''
contact_point.delete()
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
return '', 204
17 changes: 17 additions & 0 deletions udata/core/contact_point/api_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from udata.api import api, fields
from udata.core.organization.api_fields import org_ref_fields
from udata.core.user.api_fields import user_ref_fields


contact_point_fields = api.model('ContactPoint', {
'id': fields.String(description='The contact point\'s identifier', readonly=True),
'name': fields.String(description='The contact point\'s name', required=True),
'email': fields.String(description='The contact point\'s email', required=True),
'organization': fields.Nested(
org_ref_fields, allow_null=True,
description='The producer organization'),
'owner': fields.Nested(
user_ref_fields, allow_null=True, description='The user information')
})
quaxsze marked this conversation as resolved.
Show resolved Hide resolved

contact_point_page_fields = api.model('ContactPointPage', fields.pager(contact_point_fields))
13 changes: 13 additions & 0 deletions udata/core/contact_point/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import factory

from udata.factories import ModelFactory

from .models import ContactPoint


class ContactPointFactory(ModelFactory):
class Meta:
model = ContactPoint

name = factory.Faker('name')
email = factory.Faker('email')
16 changes: 16 additions & 0 deletions udata/core/contact_point/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from udata.forms import ModelForm, fields, validators
from udata.i18n import lazy_gettext as _
from udata.models import ContactPoint


__all__ = ('ContactPointForm',)


class ContactPointForm(ModelForm):
model_class = ContactPoint

name = fields.StringField(_('Name'), [validators.DataRequired(),
validators.NoURLs(_('URLs not allowed in this field'))])
email = fields.StringField(_('Email'), [validators.DataRequired(), validators.Email()])
owner = fields.CurrentUserField()
organization = fields.PublishAsField(_('Publish as'))
13 changes: 13 additions & 0 deletions udata/core/contact_point/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from udata.models import db


__all__ = ('ContactPoint', )


class ContactPoint(db.Document, db.Owned):
email = db.StringField(max_length=255, required=True)
name = db.StringField(max_length=255, required=True)

meta = {
'queryset_class': db.OwnedQuerySet
}
42 changes: 41 additions & 1 deletion udata/core/dataset/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
from udata.core.storages.api import handle_upload, upload_parser
from udata.core.badges import api as badges_api
from udata.core.followers.api import FollowAPI
from udata.utils import get_by
from udata.core.contact_point.models import ContactPoint
from udata.core.contact_point.api_fields import contact_point_fields
from udata.utils import get_by, id_or_404
from udata.rdf import (
RDF_EXTENSIONS,
negociate_content, graph_response
Expand Down Expand Up @@ -245,6 +247,44 @@ def delete(self, dataset):
return '', 204


@ns.route('/<dataset:dataset>/contact/', endpoint='dataset_contact_point')
class DatasetContactAPI(API):
@api.secure
@api.doc('create_dataset_contact_point')
@api.expect(contact_point_fields)
@api.marshal_list_with(contact_point_fields, code=201)
def post(self, dataset):
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
'''Assign a contact point to a dataset'''
data = request.json
contact_point_id = data.get('id')
if not contact_point_id:
api.abort(400, 'Wrong payload format, id expected')
DatasetEditPermission(dataset).test()
if dataset.organization:
contact_point = ContactPoint.objects.get_or_404(
id=id_or_404(contact_point_id), organization=dataset.organization)
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
else:
contact_point = ContactPoint.objects.get_or_404(
id=id_or_404(contact_point_id), owner=dataset.owner)
dataset.contact_point = contact_point
dataset.save()
return contact_point, 201


@ns.route('/<dataset:dataset>/contact/<contact_point:contact_point>/', endpoint='specific_dataset_contact_point')
class DatasetSpecificContactAPI(API):
@api.secure
@api.doc('detach_dataset_contact_point')
def delete(self, dataset, contact_point):
'''Detach a contact point from a dataset'''
DatasetEditPermission(dataset).test()
if dataset.contact_point == contact_point:
dataset.contact_point = None
dataset.save()
return '', 204
api.abort(404)


@ns.route('/<dataset:dataset>/featured/', endpoint='dataset_featured')
@api.doc(**common_doc)
class DatasetFeaturedAPI(API):
Expand Down
4 changes: 3 additions & 1 deletion udata/core/dataset/api_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from udata.core.organization.models import LOGO_SIZES
from udata.core.spatial.api_fields import spatial_coverage_fields
from udata.core.user.api_fields import user_ref_fields
from udata.core.contact_point.api_fields import contact_point_fields

from .models import (
UPDATE_FREQUENCIES, RESOURCE_FILETYPES, DEFAULT_FREQUENCY,
Expand Down Expand Up @@ -171,7 +172,7 @@
'id', 'title', 'acronym', 'slug', 'description', 'created_at', 'last_modified', 'deleted',
'private', 'tags', 'badges', 'resources', 'frequency', 'frequency_date', 'extras', 'harvest',
'metrics', 'organization', 'owner', 'temporal_coverage', 'spatial', 'license',
'uri', 'page', 'last_update', 'archived', 'quality', 'internal'
'uri', 'page', 'last_update', 'archived', 'quality', 'internal', 'contact_point',
))

dataset_internal_fields = api.model('DatasetInternals', {
Expand Down Expand Up @@ -246,6 +247,7 @@
description='The resources last modification date', required=True),
'internal': fields.Nested(
dataset_internal_fields, readonly=True, description='Site internal and specific object\'s data'),
'contact_point': fields.Nested(contact_point_fields, allow_null=True, description='The dataset\'s contact points'),
}, mask=DEFAULT_MASK)

dataset_page_fields = api.model('DatasetPage', fields.pager(dataset_fields),
Expand Down
7 changes: 5 additions & 2 deletions udata/core/dataset/apiv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
resource_internal_fields
)
from udata.core.spatial.api_fields import geojson
from udata.core.contact_point.api_fields import contact_point_fields
from .models import (
Dataset, UPDATE_FREQUENCIES, DEFAULT_FREQUENCY, DEFAULT_LICENSE, CommunityResource
)
Expand All @@ -35,7 +36,7 @@
'id', 'title', 'acronym', 'slug', 'description', 'created_at', 'last_modified', 'deleted',
'private', 'tags', 'badges', 'resources', 'community_resources', 'frequency', 'frequency_date',
'extras', 'metrics', 'organization', 'owner', 'temporal_coverage', 'spatial', 'license',
'uri', 'page', 'last_update', 'archived', 'quality', 'harvest', 'internal'
'uri', 'page', 'last_update', 'archived', 'quality', 'harvest', 'internal', 'contact_point',
))

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -132,7 +133,8 @@
'last_update': fields.ISODateTime(
description='The resources last modification date', required=True),
'internal': fields.Nested(
dataset_internal_fields, readonly=True, description='Site internal and specific object\'s data')
dataset_internal_fields, readonly=True, description='Site internal and specific object\'s data'),
'contact_point': fields.Nested(contact_point_fields, allow_null=True, description='The dataset\'s contact point'),
}, mask=DEFAULT_MASK_APIV2)


Expand Down Expand Up @@ -170,6 +172,7 @@
apiv2.inherit('HarvestResourceMetadata', resource_harvest_fields)
apiv2.inherit('DatasetInternals', dataset_internal_fields)
apiv2.inherit('ResourceInternals', resource_internal_fields)
apiv2.inherit('ContactPoint', contact_point_fields)


@ns.route('/search/', endpoint='dataset_search')
Expand Down
2 changes: 2 additions & 0 deletions udata/core/dataset/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ class Dataset(WithMetrics, BadgeMixin, db.Owned, db.Document):

featured = db.BooleanField(required=True, default=False)

contact_point = db.ReferenceField('ContactPoint', reverse_delete_rule=db.PULL)
quaxsze marked this conversation as resolved.
Show resolved Hide resolved

created_at_internal = DateTimeField(verbose_name=_('Creation date'),
default=datetime.utcnow, required=True)
last_modified_internal = DateTimeField(verbose_name=_('Last modification date'),
Expand Down
16 changes: 14 additions & 2 deletions udata/core/dataset/rdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from udata import i18n, uris
from udata.frontend.markdown import parse_html
from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
from udata.models import db
from udata.models import db, ContactPoint
from udata.rdf import (
DCAT, DCT, FREQ, SCV, SKOS, SPDX, SCHEMA, EUFREQ, EUFORMAT, IANAFORMAT,
DCAT, DCT, FREQ, SCV, SKOS, SPDX, SCHEMA, EUFREQ, EUFORMAT, IANAFORMAT, VCARD,
namespace_manager, url_from_rdf
)
from udata.utils import get_by, safe_unicode
Expand Down Expand Up @@ -312,6 +312,17 @@ def temporal_from_rdf(period_of_time):
log.warning('Unable to parse temporal coverage', exc_info=True)


def contact_point_from_rdf(rdf):
contact_point = rdf.value(DCAT.contactPoint)
if contact_point:
name = contact_point.value(VCARD.fn)
email = (contact_point.value(VCARD.hasEmail)
or contact_point.value(VCARD.email)
or contact_point.value(DCAT.email))
contact = ContactPoint(name=name, email=email).save()
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
return contact


def frequency_from_rdf(term):
if isinstance(term, str):
try:
Expand Down Expand Up @@ -470,6 +481,7 @@ def dataset_from_rdf(graph, dataset=None, node=None):
description = d.value(DCT.description) or d.value(DCT.abstract)
dataset.description = sanitize_html(description)
dataset.frequency = frequency_from_rdf(d.value(DCT.accrualPeriodicity))
dataset.contact_point = contact_point_from_rdf(d) or dataset.contact_point

acronym = rdf_value(d, SKOS.altLabel)
if acronym:
Expand Down
19 changes: 19 additions & 0 deletions udata/core/organization/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,25 @@ def delete(self, org, badge_kind):
return badges_api.remove(org, badge_kind)


from udata.models import ContactPoint
from udata.core.contact_point.api import ContactPointApiParser
from udata.core.contact_point.api_fields import contact_point_page_fields


contact_point_parser = ContactPointApiParser()


@ns.route('/<org:org>/contact/', endpoint='org_contact_points')
class OrgContactAPI(API):
@api.doc('get_organization_contact_point')
@api.marshal_with(contact_point_page_fields)
def get(self, org):
'''List all organization contact points'''
args = contact_point_parser.parse()
contact_points = ContactPoint.objects.owned_by(org)
return contact_points.paginate(args['page'], args['page_size'])


requests_parser = api.parser()
requests_parser.add_argument(
'status',
Expand Down
1 change: 0 additions & 1 deletion udata/core/organization/api_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
readonly=True),
})


from udata.core.user.api_fields import user_ref_fields # noqa: required

request_fields = api.model('MembershipRequest', {
Expand Down
2 changes: 2 additions & 0 deletions udata/core/organization/apiv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from udata.utils import multi_to_dict
from .search import OrganizationSearch
from .api_fields import org_page_fields, org_fields, member_fields
from udata.core.contact_point.api_fields import contact_point_fields

apiv2.inherit('OrganizationPage', org_page_fields)
apiv2.inherit('Organization', org_fields)
apiv2.inherit('Member', member_fields)
apiv2.inherit('ContactPoint', contact_point_fields)


ns = apiv2.namespace('organizations', 'Organization related operations')
Expand Down
4 changes: 3 additions & 1 deletion udata/core/organization/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from udata import mail
from udata.i18n import lazy_gettext as _
from udata.core import storages
from udata.models import Follow, Activity, Dataset, Transfer
from udata.models import Follow, Activity, Dataset, Transfer, ContactPoint
from udata.search import reindex
from udata.tasks import job, task, get_logger

Expand All @@ -24,6 +24,8 @@ def purge_organizations(self):
# Remove transfers
Transfer.objects(recipient=organization).delete()
Transfer.objects(owner=organization).delete()
# Remove related contact points
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
ContactPoint.objects(organization=organization).delete()
# Store datasets for later reindexation
d_ids = [d.id for d in Dataset.objects(organization=organization)]
# Remove organization's logo in all sizes
Expand Down
19 changes: 19 additions & 0 deletions udata/core/user/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,25 @@ def delete(self, user):
return '', 204


from udata.models import ContactPoint
from udata.core.contact_point.api import ContactPointApiParser
from udata.core.contact_point.api_fields import contact_point_page_fields


contact_point_parser = ContactPointApiParser()


@ns.route('/<user:user>/contact/', endpoint='user_contact_points')
quaxsze marked this conversation as resolved.
Show resolved Hide resolved
class OrgContactAPI(API):
@api.doc('get_user_contact_point')
@api.marshal_with(contact_point_page_fields)
def get(self, user):
'''List all user contact points'''
args = contact_point_parser.parse()
contact_points = ContactPoint.objects.owned_by(user)
return contact_points.paginate(args['page'], args['page_size'])


@ns.route('/<id>/followers/', endpoint='user_followers')
@ns.doc(get={'id': 'list_user_followers'},
post={'id': 'follow_user'},
Expand Down