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

WIP: Add team logic to Udata #2971

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions udata/core/dataset/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ def put(self, dataset):
request_deleted = request.json.get('deleted', True)
if dataset.deleted and request_deleted is not None:
api.abort(410, 'Dataset has been deleted')
# This line will prevent a user to modify a dataset
# if he is not part of the dataset's team.
# Will be use for organization's team feature.
# if not dataset.is_user_in_dataset_team(current_user):
# api.abort(403, 'User has no right to modify this dataset')
DatasetEditPermission(dataset).test()
dataset.last_modified_internal = datetime.utcnow()
form = api.validate(DatasetForm, dataset)
Expand All @@ -244,6 +249,11 @@ def delete(self, dataset):
'''Delete a dataset given its identifier'''
if dataset.deleted:
api.abort(410, 'Dataset has been deleted')
# This line will prevent a user to modify a dataset
# if he is not part of the dataset's team.
# Will be use for organization's team feature.
# if not dataset.is_user_in_dataset_team(current_user):
# api.abort(403, 'User has no right to modify this dataset')
DatasetEditPermission(dataset).test()
dataset.deleted = datetime.utcnow()
dataset.last_modified_internal = datetime.utcnow()
Expand Down
9 changes: 9 additions & 0 deletions udata/core/dataset/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,15 @@ def is_hidden(self):
return (len(self.resources) == 0 or self.private or self.deleted
or self.archived)

@property
def is_user_in_dataset_team(self, user):
permission = False
if self.organization:
for team in self.teams:
if team.is_member(user):
permission = True
return permission

@property
def full_title(self):
if not self.acronym:
Expand Down
129 changes: 129 additions & 0 deletions udata/core/organization/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from udata.auth import admin_permission, current_user
from udata.core.badges import api as badges_api
from udata.core.followers.api import FollowAPI
from udata.core.user.api_fields import user_fields
from udata.core.team.api_fields import team_fields
from udata.core.team.forms import TeamForm
from udata.core.team.models import Team
from udata.utils import multi_to_dict
from udata.rdf import (
RDF_EXTENSIONS, negociate_content, graph_response
Expand Down Expand Up @@ -384,6 +388,131 @@ def delete(self, org, user):
api.abort(404)


@ns.route('/<org:org>/teams/', endpoint='teams')
class TeamsAPI(API):
@api.secure
@api.expect(team_fields)
@api.marshal_with(team_fields, code=201)
@api.doc('create_organization_team')
@api.response(403, 'Not Authorized')
@api.response(409, 'Team already exists', team_fields)
def post(self, org):
'''Create a team for a given organization.'''
EditOrganizationPermission(org).test()
form = api.validate(TeamForm)
team = Team()
form.populate_obj(team)
org.teams.append(team)
team.save()
org.save()
return team, 201


@ns.route('/<org:org>/teams/<team:team>/', endpoint='team')
class TeamAPI(API):
@api.doc('get_team')
@api.marshal_with(team_fields)
def get(self, org, team):
'''Get a team given its identifier'''
if org.deleted:
api.abort(410, 'Organization has been deleted')
return team

@api.secure
@api.doc('update_team')
@api.expect(team_fields)
@api.marshal_with(team_fields)
def put(self, org, team):
'''Update a given team on a given organization'''
EditOrganizationPermission(org).test()
form = api.validate(TeamForm, team)
form.populate_obj(team)
team.save()
return team

@api.secure
@api.doc('delete_team')
def delete(self, org, team):
'''Delete a given team on a given organization'''
EditOrganizationPermission(org).test()
org.teams.remove(team)
org.save()
team.delete()
return '', 204


@ns.route('/<org:org>/teams/<team:team>/member/<user:user>', endpoint='team_member')
class TeamMemberAPI(API):
@api.secure
@api.marshal_with(user_fields, code=201)
@api.doc('create_organization_team_member')
def post(self, org, team, user):
'''Add a member into a given organization's team'''
EditOrganizationPermission(org).test()
if not org.is_member(user):
api.abort(403, 'User is not a member of the organization')
team.members.append(user)
team.save()
return user, 201

@api.secure
@api.doc('delete_organization_team_member')
def delete(self, org, team, user):
'''Delete member from an organization's team'''
EditOrganizationPermission(org).test()
if not org.is_member(user):
api.abort(403, 'User is not a member of the organization')
member = team.member(user)
if member:
team.remove(user)
team.save()
return '', 204
else:
api.abort(404)


@ns.route('/<org:org>/teams/<team:team>/datasets', endpoint='team_datasets')
class TeamDatasetAPI(API):
@api.secure
@api.marshal_with(team_fields, code=201)
@api.doc('create_organization_team_datasets')
def post(self, org, team):
'''Add a list of datasets to a organization's team'''
EditOrganizationPermission(org).test()
data = request.json
datasets = data.get('datasets', [])
for dataset_id in datasets:
dataset = Dataset.objects(id=dataset_id).first()
if not dataset:
api.abort(404, f'Dataset {dataset_id} not found')
if not dataset.organization == org:
api.abort(403, f'Dataset {dataset_id} is not part of the organization')
dataset.teams.append(team)
dataset.save()
return team, 201

@api.secure
@api.marshal_with(team_fields, code=201)
@api.doc('delete_organization_team_datasets')
def delete(self, org, team):
'''remove a list of datasets to a organization's team'''
EditOrganizationPermission(org).test()
data = request.json
datasets = data.get('datasets', [])
for dataset_id in datasets:
dataset = Dataset.objects(id=dataset_id).first()
if not dataset:
api.abort(404, f'Dataset {dataset_id} not found')
if not dataset.organization == org:
api.abort(403, f'Dataset {dataset_id} is not part of the organization')
try:
dataset.teams.remove(team)
except ValueError:
api.abort(400, f'Dataset {dataset_id} is not part of the team')
dataset.save()
return team, 204


@ns.route('/<id>/followers/', endpoint='organization_followers')
@ns.doc(get={'id': 'list_organization_followers'},
post={'id': 'follow_organization'},
Expand Down
9 changes: 1 addition & 8 deletions udata/core/organization/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from udata.factories import ModelFactory

from .models import Organization, Team, Member
from .models import Organization, Member


class OrganizationFactory(ModelFactory):
Expand All @@ -22,10 +22,3 @@ class Meta:
class Params:
admins = []
editors = []


class TeamFactory(ModelFactory):
class Meta:
model = Team

name = factory.Faker('sentence')
17 changes: 6 additions & 11 deletions udata/core/organization/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


__all__ = (
'Organization', 'Team', 'Member', 'MembershipRequest',
'Organization', 'Member', 'MembershipRequest',
'ORG_ROLES', 'MEMBERSHIP_STATUS', 'PUBLIC_SERVICE', 'CERTIFIED'
)

Expand Down Expand Up @@ -43,15 +43,10 @@
ORG_BID_SIZE_LIMIT = 14


class Team(db.EmbeddedDocument):
name = db.StringField(required=True)
slug = db.SlugField(
max_length=255, required=True, populate_from='name', update=True,
unique=False)
description = db.StringField()

members = db.ListField(db.ReferenceField('User'))

# Si dataset a un owner -> verifie que c'est bien l'utilisateur
# Si dataset dans orga:
# Si admin c'est bon
# Sinon voir si dans la bonne équipe correspondante au dataset

class Member(db.EmbeddedDocument):
user = db.ReferenceField('User')
Expand Down Expand Up @@ -109,8 +104,8 @@ class Organization(WithMetrics, BadgeMixin, db.Datetimed, db.Document):
business_number_id = db.StringField(max_length=ORG_BID_SIZE_LIMIT)

members = db.ListField(db.EmbeddedDocumentField(Member))
teams = db.ListField(db.EmbeddedDocumentField(Team))
requests = db.ListField(db.EmbeddedDocumentField(MembershipRequest))
teams = db.ListField(db.ReferenceField('Team'))

ext = db.MapField(db.GenericEmbeddedDocumentField())
zone = db.StringField()
Expand Down
Empty file added udata/core/team/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions udata/core/team/api_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from udata.api import api, fields
from udata.core.user.api_fields import user_fields


team_fields = api.model('Team', {
'id': fields.String(
description='The team identifier', required=True),
'name': fields.String(description='The team name', required=True),
'slug': fields.String(
description='The team string used as permalink',
required=True),
'description': fields.Markdown(
description='The team description in Markdown', required=True),
'members': fields.List(
fields.Nested(user_fields, description='The team members'))
})
13 changes: 13 additions & 0 deletions udata/core/team/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 Team


class TeamFactory(ModelFactory):
class Meta:
model = Team

name = factory.Faker('sentence')
description = factory.Faker('text')
17 changes: 17 additions & 0 deletions udata/core/team/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from udata.forms import ModelForm, fields, validators
from udata.i18n import lazy_gettext as _

from .models import (
TITLE_SIZE_LIMIT, DESCRIPTION_SIZE_LIMIT
)

__all__ = (
'TeamForm'
)


class TeamForm(ModelForm):
name = fields.StringField(_('Name'), [validators.DataRequired(), validators.Length(max=TITLE_SIZE_LIMIT)])
description = fields.MarkdownField(
_('Description'), [validators.DataRequired(), validators.Length(max=DESCRIPTION_SIZE_LIMIT)],
description=_('The details about your team'))
29 changes: 29 additions & 0 deletions udata/core/team/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from udata.models import db


__all__ = (
'Team', 'TITLE_SIZE_LIMIT', 'DESCRIPTION_SIZE_LIMIT'
)


TITLE_SIZE_LIMIT = 350
DESCRIPTION_SIZE_LIMIT = 100000


class Team(db.Document):
name = db.StringField(required=True)
slug = db.SlugField(
max_length=255, required=True, populate_from='name', update=True,
unique=False)
description = db.StringField()

members = db.ListField(db.ReferenceField('User'))

def member(self, user):
for member in self.members:
if member == user:
return member
return None

def is_member(self, user):
return self.member(user) is not None
1 change: 1 addition & 0 deletions udata/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def resolve_model(self, model):
from udata.core.followers.models import * # noqa
from udata.core.user.models import * # noqa
from udata.core.organization.models import * # noqa
from udata.core.team.models import * # noqa
from udata.core.contact_point.models import * # noqa
from udata.core.site.models import * # noqa
from udata.core.dataset.models import * # noqa
Expand Down
6 changes: 4 additions & 2 deletions udata/models/owned.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from blinker import signal
from mongoengine import NULLIFY, Q, post_save
from mongoengine.fields import ReferenceField
from mongoengine.fields import ReferenceField, ListField

from .queryset import UDataQuerySet

Expand All @@ -23,17 +23,19 @@ class Owned(object):
'''
owner = ReferenceField('User', reverse_delete_rule=NULLIFY)
organization = ReferenceField('Organization', reverse_delete_rule=NULLIFY)
teams = ListField(ReferenceField('Team', reverse_delete_rule=NULLIFY))

on_owner_change = signal('Owned.on_owner_change')

meta = {
'indexes': [
'owner',
'organization',
'teams',
],
'queryset_class': OwnedQuerySet,
}

# TODO: Update clean with teams validation
def clean(self):
'''
Verify owner consistency and fetch original owner before the new one erase it.
Expand Down
5 changes: 5 additions & 0 deletions udata/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ class ContactPointConverter(ModelConverter):
model = models.ContactPoint


class TeamConverter(ModelConverter):
model = models.Team


class TerritoryConverter(PathConverter):
DEFAULT_PREFIX = 'fr' # TODO: make it a setting parameter

Expand Down Expand Up @@ -224,6 +228,7 @@ def init_app(app):
app.url_map.converters['dataset'] = DatasetConverter
app.url_map.converters['crid'] = CommunityResourceConverter
app.url_map.converters['org'] = OrganizationConverter
app.url_map.converters['team'] = TeamConverter
app.url_map.converters['reuse'] = ReuseConverter
app.url_map.converters['user'] = UserConverter
app.url_map.converters['topic'] = TopicConverter
Expand Down
Loading