Skip to content

Commit

Permalink
start work for metadata API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
fdambrine authored and artragis committed Sep 5, 2018
1 parent 3d4a7ac commit e3db836
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 55 deletions.
20 changes: 20 additions & 0 deletions zds/tutorialv2/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.permissions import BasePermission, DjangoModelPermissions


class IsOwner(BasePermission):
def has_permission(self, request, view):
request_param_user = request.kwargs.get('user', 0)
current_user = request.user
return current_user and current_user.pk == request_param_user


class CanModerate(DjangoModelPermissions):
perms_map = {
'GET': ['%(app_label)s.change_%(model_name)s'],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
97 changes: 88 additions & 9 deletions zds/tutorialv2/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
import copy
import logging
from collections import Counter

import uuslug
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, empty

from zds.api.serializers import ZdSModelSerializer
from zds.tutorialv2.api.view_models import ChildrenViewModel, ChildrenListViewModel, UpdateChildrenListViewModel
from gettext import gettext as _

from zds.tutorialv2.models.database import PublishableContent
from zds.tutorialv2.utils import init_new_repo
from zds.utils.forms import TagValidator

logger = logging.getLogger(__name__)


class CommaSeparatedCharField(CharField):
"""
Allows to transform a list of objects into comma separated list and vice versa
"""
def __init__(self, *, filter_function=None, **kwargs):
super().__init__(**kwargs)
self.filter_method = filter_function

def to_internal_value(self, data):
if isinstance(data, (list, tuple)):
return super().to_internal_value(','.join(str(value) for value in data))
return super().to_internal_value(data)

def run_validation(self, data=empty):
validated_string = super().run_validation(data)
if data == '':
return []
return list(filter(self.filter_method, validated_string.split(',')))


def transform(exception1, exception2, message):
"""
Decorates a method so that it can wrap some error into a more convenient type
Expand Down Expand Up @@ -60,7 +89,7 @@ class Meta:

def __init__(self, *args, **kwargs):
self.db_object = kwargs.pop('db_object', None)
super(ChildrenListSerializer, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self._validated_data = {}
self._errors = {}

Expand All @@ -80,7 +109,8 @@ def is_valid(self, raise_exception=False):
:return:
"""

error = not super(ChildrenListSerializer, self).is_valid(raise_exception)
has_error = not super().is_valid(raise_exception) # yes, this boolean is not mandatory but it allows us
# to write elegant condition such as if has_errors so it's more readable with that.
messages = {}

for field_name, value in self.initial_data.items():
Expand All @@ -91,31 +121,31 @@ def is_valid(self, raise_exception=False):
if self.initial_data.get('containers', None):
self._validated_data['containers'] = [ChildrenViewModel(**v) for v in self._validated_data['containers']]
if not all(c.child_type.lower() == 'extract' for c in self._validated_data.get('extracts', [])):
error = True
has_error = True
messages['extracts'] = _('un extrait est mal configuré')
if len(self._validated_data['extracts']) != len(set(e.title for e in self._validated_data['extracts'])):
error = True
has_error = True
titles = Counter(list(e.title for e in self._validated_data['extracts']))
doubly = [key for key, v in titles.items() if v > 1]
messages['extracts'] = _('Certains titres sont en double : {}').format(','.join(doubly))
if len(self._validated_data['containers']) != len(set(e.title for e in self._validated_data['containers'])):
error = True
has_error = True
titles = Counter(list(e.title for e in self._validated_data['containers']))
doubly = [key for key, v in titles.items() if v > 1]
messages['containers'] = _('Certaines parties ou chapitres sont en double : {}').format(','.join(doubly))
if not all(c.child_type.lower() == 'container' for c in self._validated_data.get('containers', [])):
error = True
has_error = True
messages['containers'] = _('Un conteneur est mal configuré')
self._validated_data['introduction'] = self.initial_data.get('introduction', '')
self._validated_data['conclusion'] = self.initial_data.get('conclusion', '')
if not self._validated_data['extracts'] and not self._validated_data['containers']:
error = True
has_error = True
messages['extracts'] = _('Le contenu semble vide.')
if raise_exception and error:
if raise_exception and has_error:
self._errors.update(messages)
raise ValidationError(self.errors)

return not error
return not has_error

def to_representation(self, instance):
dic_repr = {}
Expand Down Expand Up @@ -161,3 +191,52 @@ def is_valid(self, raise_exception=False):

def create(self, validated_data):
return UpdateChildrenListViewModel(**validated_data)


class PublishableMetaDataSerializer(ZdSModelSerializer):
tags = CommaSeparatedCharField(source='tags', required=False, filter_function=TagValidator().validate_one_element)

class Meta:
model = PublishableContent
exclude = ('is_obsolete', 'must_reindex', 'last_note', 'helps', 'beta_topic', 'image', 'content_type_attribute')
read_only_fields = ('authors', 'gallery', 'public_version', 'js_support', 'is_locked', 'relative_images_path',
'sha_picked', 'sha_draft', 'sha_validation', 'sha_beta', 'sha_public', 'picked_date',
'update_date', 'pubdate', 'creation_date', 'slug')
depth = 2

def create(self, validated_data):
# default db values
validated_data['is_js'] = False # Always false when we create

# links to other entities
tags = validated_data.pop('tags', '')
content = super().create(validated_data)
content.add_tags(tags)
content.add_author(self.context['author'])
init_new_repo(content, '', '', _('Création de {}').format(content.title), do_commit=True)
return content

def update(self, instance, validated_data):
working_dictionary = copy.deepcopy(validated_data)
versioned = instance.load_version()
must_save_version = False
if working_dictionary.get('tags', []):
instance.replace_tags(working_dictionary.pop('tags'))
if working_dictionary.get('title', instance.title) != instance.title:
instance.title = working_dictionary.pop('title')
instance.slug = uuslug(instance.title, instance=instance, max_length=80)
versioned.title = instance.title
versioned.slug = instance.slug
must_save_version = True
if working_dictionary.get('type', instance.type) != instance.type:
instance.type = working_dictionary.pop('type')
versioned.type = instance.type
must_save_version = True
if must_save_version:
instance.sha_draft = versioned.repo_update(
title=instance.title,
introduction=versioned.get_introduction(),
conclusion=versioned.get_conclusion(),
do_commit=True
)
return super.update(instance, working_dictionary)
11 changes: 9 additions & 2 deletions zds/tutorialv2/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.conf.urls import url

from zds.tutorialv2.api.views import ContentReactionKarmaView, ContainerPublicationReadinessView, RedactionChildrenListView
from zds.tutorialv2.api.views import ContentReactionKarmaView, ContainerPublicationReadinessView,\
RedactionChildrenListView, AuthorContentListCreateAPIView

urlpatterns = [
url(r'^reactions/(?P<pk>\d+)/karma/?$', ContentReactionKarmaView.as_view(), name='reaction-karma'),
url(r'^publication/preparation/(?P<pk>[0-9]+)/', ContainerPublicationReadinessView.as_view(), name='readiness')
url(r'^publication/preparation/(?P<pk>[0-9]+)/', ContainerPublicationReadinessView.as_view(), name='readiness'),
url(r'^children-content/(?P<pk>\d+)/(?P<slug>[a-zA-Z0-9_-]+)/'
r'(?P<parent_container_slug>.+)/(?P<container_slug>.+)/$',
RedactionChildrenListView.as_view(public_is_prioritary=False),
Expand All @@ -16,5 +17,11 @@
url(r'^children-content/(?P<pk>\d+)/(?P<slug>[a-zA-Z0-9_-]+)/$',
RedactionChildrenListView.as_view(public_is_prioritary=False),
name='children-content'),
url(r'^(?P<user>\d+)/$',
AuthorContentListCreateAPIView.as_view(),
name='api-author-contents'),
url(r'^(?P<pk>\d+)/(?P<slug>[a-zA-Z0-9_-]+)/$',
AuthorContentListCreateAPIView.as_view(),
name='api-author-contents'),

]
23 changes: 21 additions & 2 deletions zds/tutorialv2/api/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from rest_framework.exceptions import ValidationError
from rest_framework.generics import RetrieveUpdateAPIView
from django.http import Http404
from django.utils.translation import gettext as _
from rest_framework.fields import empty
from rest_framework.generics import UpdateAPIView
from rest_framework.serializers import Serializer, CharField, BooleanField
from rest_framework.generics import RetrieveUpdateAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView, \
get_object_or_404
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from zds.member.api.permissions import IsAuthorOrStaff
from rest_framework.response import Response
from zds.member.api.permissions import CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly
from zds.tutorialv2.api.serializers import ChildrenListSerializer, ChildrenListModifySerializer
from zds.tutorialv2.api.permissions import IsOwner, CanModerate
from zds.tutorialv2.api.serializers import ChildrenListSerializer, ChildrenListModifySerializer, \
PublishableMetaDataSerializer
from zds.tutorialv2.api.view_models import ChildrenListViewModel, ChildrenViewModel
from zds.tutorialv2.mixins import SingleContentDetailViewMixin
from zds.tutorialv2.models.versioned import Extract, Container
Expand Down Expand Up @@ -143,3 +146,19 @@ def get_serializer_class(self):

def get_permissions(self):
return [IsAuthenticatedOrReadOnly()]


class AuthorContentListCreateAPIView(ListCreateAPIView):
permission_classes = (IsOwner, CanModerate)
serializer_class = PublishableMetaDataSerializer

def get_queryset(self):
return PublishableContent.objects.filter(authors__pk__in=[self.kwargs.get('user')])


class InRedactionContentRetrieveUpdateDeleteAPIView(RetrieveUpdateDestroyAPIView):
permission_classes = (IsOwner, CanModerate)
serializer_class = PublishableMetaDataSerializer

def get_object(self):
return get_object_or_404(PublishableContent, pk=self.kwargs.get('pk'))
8 changes: 5 additions & 3 deletions zds/tutorialv2/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,16 @@ def get_versioned_object(self):
# check slug, if any:
if 'slug' in self.kwargs:
slug = self.kwargs['slug']
if versioned.slug != slug:
if slug != self.object.slug: # retro-compatibility, but should raise permanent redirect instead
raise Http404("Ce slug n'existe pas pour ce contenu.{} vs {}".format(slug, versioned.slug))
if versioned.slug != slug and slug != self.object.slug:
raise Http404("Ce slug n'existe pas pour ce contenu: obtenu={}, attendu={}".format(slug,
versioned.slug))

return versioned

def get_public_object(self):
"""Get the published version, if any
:rtype: zds.tutorialv2.models.database.PublishedContent
"""

object = PublishedContent.objects.filter(content_pk=self.object.pk, must_redirect=False).last()
Expand Down
10 changes: 7 additions & 3 deletions zds/tutorialv2/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,19 +539,23 @@ def repo_delete(self):
def add_tags(self, tag_collection):
"""
Add all tags contained in `tag_collection` to this content.
If a tag is unknown, it is added to the system.
If a tag_as_text is unknown, it is added to the system.
:param tag_collection: A collection of tags.
:type tag_collection: list
"""
for tag in tag_collection:
for tag_as_text in tag_collection:
try:
current_tag, created = Tag.objects.get_or_create(title=tag.lower().strip())
current_tag, created = Tag.objects.get_or_create(title=tag_as_text.lower().strip())
self.tags.add(current_tag)
except ValueError as e:
logger.warning(e)

self.save()

def replace_tags(self, tag_collection):
self.tags.clear()
self.add_tags(tag_collection)

def requires_validation(self):
"""
Check if content required a validation before publication.
Expand Down
75 changes: 39 additions & 36 deletions zds/tutorialv2/models/versioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

from zds import json_handler
from pathlib import Path
from git import Repo
import os
import shutil
Expand Down Expand Up @@ -496,6 +497,31 @@ def repo_update(self, title, introduction, conclusion, commit_message='', do_com

repo = self.top_container().repository

self.update_title_if_needed(repo, title, update_slug)

# update introduction and conclusion (if any)
path = self.top_container().get_path()
rel_path = self.get_path(relative=True)

self._update_documents(introduction, path, rel_path, repo, 'introduction')
self._update_documents(conclusion, path, rel_path, repo, 'conclusion')
# saving manifest is necesary to :
# - update title and slug inside the json file
# - add or remove introduction/conclusion
# - add or remove element
# - change content type
self._update_manifest(repo)

if not commit_message:
commit_message = _('Mise à jour de « {} »').format(self.title)

if do_commit:
return self.top_container().commit_changes(commit_message)

def update_title_if_needed(self, repo, title, update_slug):
"""
If title changes, we need to see if we need to change the slug. Then we probably need to move the repo
"""
# update title
if title != self.title:
self.title = title
Expand All @@ -516,46 +542,22 @@ def repo_update(self, title, introduction, conclusion, commit_message='', do_com
self.parent.children_dict.pop(old_slug)
self.parent.children_dict[self.slug] = self

# update introduction and conclusion (if any)
path = self.top_container().get_path()
rel_path = self.get_path(relative=True)

if introduction is not None:
if self.introduction is None:
self.introduction = os.path.join(rel_path, 'introduction.md')

f = codecs.open(os.path.join(path, self.introduction), 'w', encoding='utf-8')
f.write(introduction)
f.close()
repo.index.add([self.introduction])

elif self.introduction:
repo.index.remove([self.introduction])
os.remove(os.path.join(path, self.introduction))
self.introduction = None

if conclusion is not None:
if self.conclusion is None:
self.conclusion = os.path.join(rel_path, 'conclusion.md')

f = codecs.open(os.path.join(path, self.conclusion), 'w', encoding='utf-8')
f.write(conclusion)
f.close()
repo.index.add([self.conclusion])

elif self.conclusion:
repo.index.remove([self.conclusion])
os.remove(os.path.join(path, self.conclusion))
self.conclusion = None

def _update_manifest(self, repo):
self.top_container().dump_json()
repo.index.add(['manifest.json'])

if not commit_message:
commit_message = _('Mise à jour de « {} »').format(self.title)
def _update_documents(self, document_text, path, rel_path, repo, updated_doc_name):
if document_text is not None:
if getattr(self, updated_doc_name, None) is None:
setattr(self, updated_doc_name, os.path.join(rel_path, updated_doc_name + '.md'))

if do_commit:
return self.top_container().commit_changes(commit_message)
with Path(path, getattr(self, updated_doc_name)).open('w', encoding='utf-8') as f:
f.write(document_text)
repo.index.add([getattr(self, updated_doc_name)])
elif getattr(self, updated_doc_name):
repo.index.remove([getattr(self, updated_doc_name)])
os.remove(os.path.join(path, getattr(self, updated_doc_name)))
setattr(self, updated_doc_name, None)

def repo_add_container(self, title, introduction, conclusion, commit_message='', do_commit=True, slug=None):
"""
Expand All @@ -564,6 +566,7 @@ def repo_add_container(self, title, introduction, conclusion, commit_message='',
:param conclusion: text of its conclusion
:param commit_message: commit message that will be used instead of the default one
:param do_commit: perform the commit in repository if ``True``
:param slug: the new slug if needed to be customized.
:return: commit sha
:rtype: str
"""
Expand Down
Loading

0 comments on commit e3db836

Please sign in to comment.