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 47 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
## Current (in progress)

- Improve search serialization perfs for datasets in big topics [#2937](https://github.com/opendatateam/udata/pull/2937)
- Contact points feature [#2914](https://github.com/opendatateam/udata/pull/2914):
- Users and Organizations can now define a list of contact points
- Api endpoint for creating, updating and deleting contact points
- Datasets can define one contact point, among the list of the organization or the user owning the dataset.
- Defining a contact point for a dataset is done throught a form field

## 7.0.1 (2023-12-06)

Expand Down
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.
62 changes: 62 additions & 0 deletions udata/core/contact_point/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from udata.api import api, API
from udata.api.parsers import ModelApiParser
from udata.auth import admin_permission

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.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
20 changes: 20 additions & 0 deletions udata/core/contact_point/api_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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


DEFAULT_MASK = ','.join(('id', 'name', 'email'))

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')
}, mask=DEFAULT_MASK)

contact_point_page_fields = api.model('ContactPointPage', fields.pager(contact_point_fields),
mask='data{{{0}}},*'.format(DEFAULT_MASK))
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
}
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
15 changes: 15 additions & 0 deletions udata/core/dataset/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ def map_legacy_frequencies(form, field):
field.data = LEGACY_FREQUENCIES[field.data]


def validate_contact_point(form, field):
'''Validates contact point with dataset's org or owner'''
from udata.models import ContactPoint
if field.data:
if form.organization.data:
contact_point = ContactPoint.objects(
id=field.data.id, organization=form.organization.data).first()
elif form.owner.data:
contact_point = ContactPoint.objects(
id=field.data.id, owner=form.owner.data).first()
if not contact_point:
raise validators.ValidationError(_('Wrong contact point id or contact point ownership mismatch'))


class DatasetForm(ModelForm):
model_class = Dataset

Expand Down Expand Up @@ -168,6 +182,7 @@ class DatasetForm(ModelForm):
organization = fields.PublishAsField(_('Publish as'))
extras = fields.ExtrasField()
resources = fields.NestedModelList(ResourceForm)
contact_point = fields.ContactPointField(validators=[validate_contact_point])


class ResourcesListForm(ModelForm):
Expand Down
3 changes: 3 additions & 0 deletions udata/core/dataset/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import mongoengine

from datetime import datetime, timedelta
from collections import OrderedDict
Expand Down Expand Up @@ -477,6 +478,8 @@ class Dataset(WithMetrics, BadgeMixin, db.Owned, db.Document):

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

contact_point = db.ReferenceField('ContactPoint', reverse_delete_rule=mongoengine.NULLIFY)
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
24 changes: 22 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,25 @@ def temporal_from_rdf(period_of_time):
log.warning('Unable to parse temporal coverage', exc_info=True)


def contact_point_from_rdf(rdf, dataset):
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))
if dataset.organization:
contact_point = ContactPoint.objects(
name=name, email=email, organization=dataset.organization).first()
return (contact_point or
ContactPoint(name=name, email=email, organization=dataset.organization).save())
elif dataset.owner:
contact_point = ContactPoint.objects(
name=name, email=email, owner=dataset.owner).first()
return (contact_point or
ContactPoint(name=name, email=email, owner=dataset.owner).save())


def frequency_from_rdf(term):
if isinstance(term, str):
try:
Expand Down Expand Up @@ -478,6 +497,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, dataset) 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>/contacts/', 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>/contacts/', endpoint='user_contact_points')
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