From d7d9feba6b097f2575b56a8d1fbf2fb135070ac2 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 20 Dec 2014 12:38:54 +0100 Subject: [PATCH 001/887] =?UTF-8?q?Initialise=20le=20module=20parall=C3=A8?= =?UTF-8?q?le=20pour=20la=20ZEP=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/__init__.py | 1 + zds/tutorialv2/admin.py | 13 + zds/tutorialv2/factories.py | 358 +++ zds/tutorialv2/feeds.py | 44 + zds/tutorialv2/forms.py | 627 +++++ zds/tutorialv2/models.py | 753 ++++++ zds/tutorialv2/search_indexes.py | 68 + zds/tutorialv2/tests/tests_views.py | 0 zds/tutorialv2/urls.py | 123 + zds/tutorialv2/utils.py | 52 + zds/tutorialv2/views.py | 3643 +++++++++++++++++++++++++++ 11 files changed, 5682 insertions(+) create mode 100644 zds/tutorialv2/__init__.py create mode 100644 zds/tutorialv2/admin.py create mode 100644 zds/tutorialv2/factories.py create mode 100644 zds/tutorialv2/feeds.py create mode 100644 zds/tutorialv2/forms.py create mode 100644 zds/tutorialv2/models.py create mode 100644 zds/tutorialv2/search_indexes.py create mode 100644 zds/tutorialv2/tests/tests_views.py create mode 100644 zds/tutorialv2/urls.py create mode 100644 zds/tutorialv2/utils.py create mode 100644 zds/tutorialv2/views.py diff --git a/zds/tutorialv2/__init__.py b/zds/tutorialv2/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/zds/tutorialv2/__init__.py @@ -0,0 +1 @@ + diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py new file mode 100644 index 0000000000..0907547f20 --- /dev/null +++ b/zds/tutorialv2/admin.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +from django.contrib import admin + +from .models import Tutorial, Part, Chapter, Extract, Validation, Note + + +admin.site.register(Tutorial) +admin.site.register(Part) +admin.site.register(Chapter) +admin.site.register(Extract) +admin.site.register(Validation) +admin.site.register(Note) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py new file mode 100644 index 0000000000..dd9147ee72 --- /dev/null +++ b/zds/tutorialv2/factories.py @@ -0,0 +1,358 @@ +# coding: utf-8 + +from datetime import datetime +from git.repo import Repo +import json as json_writer +import os + +import factory + +from zds.tutorial.models import Tutorial, Part, Chapter, Extract, Note,\ + Validation +from zds.utils.models import SubCategory, Licence +from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.utils.tutorials import export_tutorial +from zds.tutorial.views import mep + +content = ( + u'Ceci est un contenu de tutoriel utile et à tester un peu partout\n\n ' + u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits \n\n ' + u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc \n\n ' + u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)\n\n ' + u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)\n\n ' + u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)\n\n ' + u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)\n\n ' + u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2' + u'F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)\n\n ' + u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)\n\n ' + u'\n Attention les tests ne doivent pas crasher \n\n \n\n \n\n ' + u'qu\'un sujet abandonné !\n\n ') + +content_light = u'Un contenu light pour quand ce n\'est pas vraiment ça qui est testé' + + +class BigTutorialFactory(factory.DjangoModelFactory): + FACTORY_FOR = Tutorial + + title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) + description = factory.Sequence( + lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'BIG' + create_at = datetime.now() + introduction = 'introduction.md' + conclusion = 'conclusion.md' + + @classmethod + def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) + tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) + path = tuto.get_path() + real_content = content + if light: + real_content = content_light + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + man = export_tutorial(tuto) + repo = Repo.init(path, bare=False) + repo = Repo(path) + + f = open(os.path.join(path, 'manifest.json'), "w") + f.write(json_writer.dumps(man, indent=4, ensure_ascii=False).encode('utf-8')) + f.close() + f = open(os.path.join(path, tuto.introduction), "w") + f.write(real_content.encode('utf-8')) + f.close() + f = open(os.path.join(path, tuto.conclusion), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + cm = repo.index.commit("Init Tuto") + + tuto.sha_draft = cm.hexsha + tuto.sha_beta = None + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) + return tuto + + +class MiniTutorialFactory(factory.DjangoModelFactory): + FACTORY_FOR = Tutorial + + title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) + description = factory.Sequence( + lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'MINI' + create_at = datetime.now() + introduction = 'introduction.md' + conclusion = 'conclusion.md' + + @classmethod + def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) + tuto = super(MiniTutorialFactory, cls)._prepare(create, **kwargs) + real_content = content + + if light: + real_content = content_light + path = tuto.get_path() + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + man = export_tutorial(tuto) + repo = Repo.init(path, bare=False) + repo = Repo(path) + + file = open(os.path.join(path, 'manifest.json'), "w") + file.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + file.close() + file = open(os.path.join(path, tuto.introduction), "w") + file.write(real_content.encode('utf-8')) + file.close() + file = open(os.path.join(path, tuto.conclusion), "w") + file.write(real_content.encode('utf-8')) + file.close() + + repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + cm = repo.index.commit("Init Tuto") + + tuto.sha_draft = cm.hexsha + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) + return tuto + + +class PartFactory(factory.DjangoModelFactory): + FACTORY_FOR = Part + + title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) + part = super(PartFactory, cls)._prepare(create, **kwargs) + tutorial = kwargs.pop('tutorial', None) + + real_content = content + if light: + real_content = content_light + + path = part.get_path() + repo = Repo(part.tutorial.get_path()) + + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + part.introduction = os.path.join(part.get_phy_slug(), 'introduction.md') + part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') + part.save() + + f = open(os.path.join(tutorial.get_path(), part.introduction), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add([part.introduction]) + f = open(os.path.join(tutorial.get_path(), part.conclusion), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add([part.conclusion]) + + if tutorial: + tutorial.save() + + man = export_tutorial(tutorial) + f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + + repo.index.add(['manifest.json']) + + cm = repo.index.commit("Init Part") + + if tutorial: + tutorial.sha_draft = cm.hexsha + tutorial.save() + + return part + + +class ChapterFactory(factory.DjangoModelFactory): + FACTORY_FOR = Chapter + + title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) + chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) + tutorial = kwargs.pop('tutorial', None) + part = kwargs.pop('part', None) + + real_content = content + if light: + real_content = content_light + path = chapter.get_path() + + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + if tutorial: + chapter.introduction = '' + chapter.conclusion = '' + tutorial.save() + repo = Repo(tutorial.get_path()) + + man = export_tutorial(tutorial) + f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + repo.index.add(['manifest.json']) + + elif part: + chapter.introduction = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + 'introduction.md') + chapter.conclusion = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + 'conclusion.md') + chapter.save() + f = open( + os.path.join( + part.tutorial.get_path(), + chapter.introduction), + "w") + f.write(real_content.encode('utf-8')) + f.close() + f = open( + os.path.join( + part.tutorial.get_path(), + chapter.conclusion), + "w") + f.write(real_content.encode('utf-8')) + f.close() + part.tutorial.save() + repo = Repo(part.tutorial.get_path()) + + man = export_tutorial(part.tutorial) + f = open( + os.path.join( + part.tutorial.get_path(), + 'manifest.json'), + "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + + repo.index.add([chapter.introduction, chapter.conclusion]) + repo.index.add(['manifest.json']) + + cm = repo.index.commit("Init Chapter") + + if tutorial: + tutorial.sha_draft = cm.hexsha + tutorial.save() + chapter.tutorial = tutorial + elif part: + part.tutorial.sha_draft = cm.hexsha + part.tutorial.save() + part.save() + chapter.part = part + + return chapter + + +class ExtractFactory(factory.DjangoModelFactory): + FACTORY_FOR = Extract + + title = factory.Sequence(lambda n: 'Mon Extrait No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + extract = super(ExtractFactory, cls)._prepare(create, **kwargs) + chapter = kwargs.pop('chapter', None) + if chapter: + if chapter.tutorial: + chapter.tutorial.sha_draft = 'EXTRACT-AAAA' + chapter.tutorial.save() + elif chapter.part: + chapter.part.tutorial.sha_draft = 'EXTRACT-AAAA' + chapter.part.tutorial.save() + + return extract + + +class NoteFactory(factory.DjangoModelFactory): + FACTORY_FOR = Note + + ip_address = '192.168.3.1' + text = 'Bonjour, je me présente, je m\'appelle l\'homme au texte bidonné' + + @classmethod + def _prepare(cls, create, **kwargs): + note = super(NoteFactory, cls)._prepare(create, **kwargs) + note.pubdate = datetime.now() + note.save() + tutorial = kwargs.pop('tutorial', None) + if tutorial: + tutorial.last_note = note + tutorial.save() + return note + + +class SubCategoryFactory(factory.DjangoModelFactory): + FACTORY_FOR = SubCategory + + title = factory.Sequence(lambda n: 'Sous-Categorie {0} pour Tuto'.format(n)) + subtitle = factory.Sequence(lambda n: 'Sous titre de Sous-Categorie {0} pour Tuto'.format(n)) + slug = factory.Sequence(lambda n: 'sous-categorie-{0}'.format(n)) + + +class ValidationFactory(factory.DjangoModelFactory): + FACTORY_FOR = Validation + + +class LicenceFactory(factory.DjangoModelFactory): + FACTORY_FOR = Licence + + code = u'Licence bidon' + title = u'Licence bidon' + + @classmethod + def _prepare(cls, create, **kwargs): + licence = super(LicenceFactory, cls)._prepare(create, **kwargs) + return licence + + +class PublishedMiniTutorial(MiniTutorialFactory): + FACTORY_FOR = Tutorial + + @classmethod + def _prepare(cls, create, **kwargs): + tutorial = super(PublishedMiniTutorial, cls)._prepare(create, **kwargs) + tutorial.pubdate = datetime.now() + tutorial.sha_public = tutorial.sha_draft + tutorial.source = '' + tutorial.sha_validation = None + mep(tutorial, tutorial.sha_draft) + tutorial.save() + return tutorial diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py new file mode 100644 index 0000000000..f2b6a6174e --- /dev/null +++ b/zds/tutorialv2/feeds.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +from django.contrib.syndication.views import Feed +from django.conf import settings + +from django.utils.feedgenerator import Atom1Feed + +from .models import Tutorial + + +class LastTutorialsFeedRSS(Feed): + title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) + link = "/tutoriels/" + description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + def items(self): + return Tutorial.objects\ + .filter(sha_public__isnull=False)\ + .order_by('-pubdate')[:5] + + def item_title(self, item): + return item.title + + def item_pubdate(self, item): + return item.pubdate + + def item_description(self, item): + return item.description + + def item_author_name(self, item): + authors_list = item.authors.all() + authors = [] + for authors_obj in authors_list: + authors.append(authors_obj.username) + authors = ", ".join(authors) + return authors + + def item_link(self, item): + return item.get_absolute_url_online() + + +class LastTutorialsFeedATOM(LastTutorialsFeedRSS): + feed_type = Atom1Feed + subtitle = LastTutorialsFeedRSS.description diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py new file mode 100644 index 0000000000..e2001098e8 --- /dev/null +++ b/zds/tutorialv2/forms.py @@ -0,0 +1,627 @@ +# coding: utf-8 +from django import forms +from django.conf import settings + +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Layout, Fieldset, Submit, Field, \ + ButtonHolder, Hidden +from django.core.urlresolvers import reverse + +from zds.utils.forms import CommonLayoutModalText, CommonLayoutEditor, CommonLayoutVersionEditor +from zds.utils.models import SubCategory, Licence +from zds.tutorial.models import Tutorial, TYPE_CHOICES, HelpWriting +from django.utils.translation import ugettext_lazy as _ + + +class FormWithTitle(forms.Form): + title = forms.CharField( + label=_(u'Titre'), + max_length=Tutorial._meta.get_field('title').max_length, + widget=forms.TextInput( + attrs={ + 'required': 'required', + } + ) + ) + + def clean(self): + cleaned_data = super(FormWithTitle, self).clean() + + title = cleaned_data.get('title') + + if title is not None and title.strip() == '': + self._errors['title'] = self.error_class( + [_(u'Le champ Titre ne peut être vide')]) + if 'title' in cleaned_data: + del cleaned_data['title'] + + return cleaned_data + + +class TutorialForm(FormWithTitle): + + description = forms.CharField( + label=_(u'Description'), + max_length=Tutorial._meta.get_field('description').max_length, + required=False, + ) + + image = forms.ImageField( + label=_(u'Sélectionnez le logo du tutoriel (max. {} Ko)').format( + str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), + required=False + ) + + introduction = forms.CharField( + label=_(u'Introduction'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_('Conclusion'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + type = forms.ChoiceField( + choices=TYPE_CHOICES, + required=False + ) + + subcategory = forms.ModelMultipleChoiceField( + label=_(u"Sous catégories de votre tutoriel. Si aucune catégorie ne convient " + u"n'hésitez pas à en demander une nouvelle lors de la validation !"), + queryset=SubCategory.objects.all(), + required=True, + widget=forms.SelectMultiple( + attrs={ + 'required': 'required', + } + ) + ) + + licence = forms.ModelChoiceField( + label=( + _(u'Licence de votre publication (En savoir plus sur les licences et {2})') + .format( + settings.ZDS_APP['site']['licenses']['licence_info_title'], + settings.ZDS_APP['site']['licenses']['licence_info_link'], + settings.ZDS_APP['site']['name'] + ) + ), + queryset=Licence.objects.all(), + required=True, + empty_label=None + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + helps = forms.ModelMultipleChoiceField( + label=_(u"Pour m'aider je cherche un..."), + queryset=HelpWriting.objects.all(), + required=False, + widget=forms.SelectMultiple() + ) + + def __init__(self, *args, **kwargs): + super(TutorialForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('description'), + Field('type'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), + Field('licence'), + Field('subcategory'), + HTML(_(u"

Demander de l'aide à la communauté !
" + u"Si vous avez besoin d'un coup de main," + u"sélectionnez une ou plusieurs catégories d'aide ci-dessous " + u"et votre tutoriel apparaitra alors sur la page d'aide.

")), + Field('helps'), + Field('msg_commit'), + ButtonHolder( + StrictButton('Valider', type='submit'), + ), + ) + + if 'type' in self.initial: + self.helper['type'].wrap( + Field, + disabled=True) + + +class PartForm(FormWithTitle): + + introduction = forms.CharField( + label=_(u"Introduction"), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_(u"Conclusion"), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(PartForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'), + StrictButton( + _(u'Ajouter et continuer'), + type='submit', + name='submit_continue'), + ) + ) + + +class ChapterForm(FormWithTitle): + + image = forms.ImageField( + label=_(u'Selectionnez le logo du tutoriel ' + u'(max. {0} Ko)').format(str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), + required=False + ) + + introduction = forms.CharField( + label=_(u'Introduction'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_(u'Conclusion'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ChapterForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'), + StrictButton( + _(u'Ajouter et continuer'), + type='submit', + name='submit_continue'), + )) + + +class EmbdedChapterForm(forms.Form): + introduction = forms.CharField( + required=False, + widget=forms.Textarea + ) + + image = forms.ImageField( + label=_(u'Sélectionnez une image'), + required=False) + + conclusion = forms.CharField( + required=False, + widget=forms.Textarea + ) + + msg_commit = forms.CharField( + label=_(u'Message de suivi'), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Fieldset( + _(u'Contenu'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ), + ButtonHolder( + Submit('submit', _(u'Valider')) + ) + ) + super(EmbdedChapterForm, self).__init__(*args, **kwargs) + + +class ExtractForm(FormWithTitle): + + text = forms.CharField( + label=_(u'Texte'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ExtractForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Hidden('last_hash', '{{ last_hash }}'), + CommonLayoutVersionEditor(), + ) + + +class ImportForm(forms.Form): + + file = forms.FileField( + label=_(u'Sélectionnez le tutoriel à importer'), + required=True + ) + images = forms.FileField( + label=_(u'Fichier zip contenant les images du tutoriel'), + required=False + ) + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('file'), + Field('images'), + Submit('import-tuto', _(u'Importer le .tuto')), + ) + super(ImportForm, self).__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super(ImportForm, self).clean() + + # Check that the files extensions are correct + tuto = cleaned_data.get('file') + images = cleaned_data.get('images') + + if tuto is not None: + ext = tuto.name.split(".")[-1] + if ext != "tuto": + del cleaned_data['file'] + msg = _(u'Le fichier doit être au format .tuto') + self._errors['file'] = self.error_class([msg]) + + if images is not None: + ext = images.name.split(".")[-1] + if ext != "zip": + del cleaned_data['images'] + msg = _(u'Le fichier doit être au format .zip') + self._errors['images'] = self.error_class([msg]) + + +class ImportArchiveForm(forms.Form): + + file = forms.FileField( + label=_(u"Sélectionnez l'archive de votre tutoriel"), + required=True + ) + + tutorial = forms.ModelChoiceField( + label=_(u"Tutoriel vers lequel vous souhaitez importer votre archive"), + queryset=Tutorial.objects.none(), + required=True + ) + + def __init__(self, user, *args, **kwargs): + super(ImportArchiveForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + self.fields['tutorial'].queryset = Tutorial.objects.filter(authors__in=[user]) + + self.helper.layout = Layout( + Field('file'), + Field('tutorial'), + Submit('import-archive', _(u"Importer l'archive")), + ) + + +# Notes + + +class NoteForm(forms.Form): + text = forms.CharField( + label='', + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.'), + 'required': 'required' + } + ) + ) + + def __init__(self, tutorial, user, *args, **kwargs): + super(NoteForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse( + 'zds.tutorial.views.answer') + '?tutorial=' + str(tutorial.pk) + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutEditor(), + Hidden('last_note', '{{ last_note_pk }}'), + ) + + if tutorial.antispam(user): + if 'text' not in self.initial: + self.helper['text'].wrap( + Field, + placeholder=_(u'Vous venez de poster. Merci de patienter ' + u'au moins 15 minutes entre deux messages consécutifs ' + u'afin de limiter le flood.'), + disabled=True) + elif tutorial.is_locked: + self.helper['text'].wrap( + Field, + placeholder=_(u'Ce tutoriel est verrouillé.'), + disabled=True + ) + + def clean(self): + cleaned_data = super(NoteForm, self).clean() + + text = cleaned_data.get('text') + + if text is None or text.strip() == '': + self._errors['text'] = self.error_class( + [_(u'Vous devez écrire une réponse !')]) + if 'text' in cleaned_data: + del cleaned_data['text'] + + elif len(text) > settings.ZDS_APP['forum']['max_post_length']: + self._errors['text'] = self.error_class( + [_(u'Ce message est trop long, il ne doit pas dépasser {0} ' + u'caractères').format(settings.ZDS_APP['forum']['max_post_length'])]) + + return cleaned_data + + +# Validations. + +class AskValidationForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire pour votre demande.'), + 'rows': '3' + } + ) + ) + source = forms.CharField( + label='', + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'URL de la version originale') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(AskValidationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.ask_validation') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + Field('source'), + StrictButton( + _(u'Confirmer'), + type='submit'), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), ) + + +class ValidForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire de publication.'), + 'rows': '2' + } + ) + ) + is_major = forms.BooleanField( + label=_(u'Version majeure ?'), + required=False, + initial=True + ) + source = forms.CharField( + label='', + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'URL de la version originale') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ValidForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.valid_tutorial') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + Field('source'), + Field('is_major'), + StrictButton(_(u'Publier'), type='submit'), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), + ) + + +class RejectForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire de rejet.'), + 'rows': '6' + } + ) + ) + + def __init__(self, *args, **kwargs): + super(RejectForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.reject_tutorial') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + ButtonHolder( + StrictButton( + _(u'Rejeter'), + type='submit'),), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), ) + + +class ActivJsForm(forms.Form): + + js_support = forms.BooleanField( + label='Cocher pour activer JSFiddle', + required=False, + initial=True + ) + + def __init__(self, *args, **kwargs): + super(ActivJsForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.activ_js') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('js_support'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'),), + Hidden('tutorial', '{{ tutorial.pk }}'), ) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py new file mode 100644 index 0000000000..cb0ea3b437 --- /dev/null +++ b/zds/tutorialv2/models.py @@ -0,0 +1,753 @@ +# coding: utf-8 + +from math import ceil +import shutil +try: + import ujson as json_reader +except: + try: + import simplejson as json_reader + except: + import json as json_reader + +import json as json_writer +import os + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.db import models +from datetime import datetime +from git.repo import Repo + +from zds.gallery.models import Image, Gallery +from zds.utils import slugify, get_current_user +from zds.utils.models import SubCategory, Licence, Comment +from zds.utils.tutorials import get_blob, export_tutorial + + +TYPE_CHOICES = ( + ('TUTO', 'Tutoriel'), + ('ARTICLE', 'Article'), +) + +STATUS_CHOICES = ( + ('PENDING', 'En attente d\'un validateur'), + ('PENDING_V', 'En cours de validation'), + ('ACCEPT', 'Publié'), + ('REJECT', 'Rejeté'), +) + + +class InvalidOperationError(RuntimeError): + pass + + +class Container(models.Model): + + """A container (tuto/article, part, chapter).""" + class Meta: + verbose_name = 'Container' + verbose_name_plural = 'Containers' + + title = models.CharField('Titre', max_length=80) + + slug = models.SlugField(max_length=80) + + introduction = models.CharField( + 'chemin relatif introduction', + blank=True, + null=True, + max_length=200) + + conclusion = models.CharField( + 'chemin relatif conclusion', + blank=True, + null=True, + max_length=200) + + parent = models.ForeignKey("self", + verbose_name='Conteneur parent', + blank=True, null=True, + on_delete=models.SET_NULL) + + position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', + blank=False, + null=False, + default=1) + + def get_children(self): + """get this container children""" + if self.has_extract(): + return Extract.objects.filter(container_pk=self.pk) + + return Container.objects.filter(parent_pk=self.pk) + + def has_extract(self): + """Check this container has content extracts""" + return Extract.objects.filter(chapter=self).count() > 0 + + def has_sub_part(self): + """Check this container has a sub container""" + return Container.objects.filter(parent=self).count() > 0 + + def get_last_child_position(self): + """Get the relative position of the last child""" + return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + + def add_container(self, container): + """add a child container. A container can only be added if + no extract had already been added in this container""" + if not self.has_extract(): + container.parent = self + container.position_in_parent = container.get_last_child_position() + 1 + container.save() + else: + raise InvalidOperationError("Can't add a container if this container contains extracts.") + + def get_phy_slug(self): + """Get the physical path as stored in git file system""" + base = "" + if self.parent is not None: + base = self.parent.get_phy_slug() + return os.path.join(base, self.slug) + + def update_children(self): + for child in self.get_children(): + if child is Container: + self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") + self.conclusion = os.path.join(self.get_phy_slug(), "conclusion.md") + self.save() + child.update_children() + else: + child.text = child.get_path(relative=True) + child.save() + + def add_extract(self, extract): + if not self.has_sub_part(): + extract.chapter = self + + + +class PubliableContent(Container): + + """A tutorial whatever its size or an aticle.""" + class Meta: + verbose_name = 'Tutoriel' + verbose_name_plural = 'Tutoriels' + + + description = models.CharField('Description', max_length=200) + source = models.CharField('Source', max_length=200) + authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) + + subcategory = models.ManyToManyField(SubCategory, + verbose_name='Sous-Catégorie', + blank=True, null=True, db_index=True) + + image = models.ForeignKey(Image, + verbose_name='Image du tutoriel', + blank=True, null=True, + on_delete=models.SET_NULL) + + gallery = models.ForeignKey(Gallery, + verbose_name='Galerie d\'images', + blank=True, null=True, db_index=True) + + create_at = models.DateTimeField('Date de création') + pubdate = models.DateTimeField('Date de publication', + blank=True, null=True, db_index=True) + update = models.DateTimeField('Date de mise à jour', + blank=True, null=True) + + sha_public = models.CharField('Sha1 de la version publique', + blank=True, null=True, max_length=80, db_index=True) + sha_beta = models.CharField('Sha1 de la version beta publique', + blank=True, null=True, max_length=80, db_index=True) + sha_validation = models.CharField('Sha1 de la version en validation', + blank=True, null=True, max_length=80, db_index=True) + sha_draft = models.CharField('Sha1 de la version de rédaction', + blank=True, null=True, max_length=80, db_index=True) + + licence = models.ForeignKey(Licence, + verbose_name='Licence', + blank=True, null=True, db_index=True) + # as of ZEP 12 this fiels is no longer the size but the type of content (article/tutorial) + type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) + + images = models.CharField( + 'chemin relatif images', + blank=True, + null=True, + max_length=200) + + last_note = models.ForeignKey('Note', blank=True, null=True, + related_name='last_note', + verbose_name='Derniere note') + is_locked = models.BooleanField('Est verrouillé', default=False) + js_support = models.BooleanField('Support du Javascript', default=False) + + def __unicode__(self): + return self.title + + def get_phy_slug(self): + return str(self.pk) + "_" + self.slug + + def get_absolute_url(self): + return reverse('zds.tutorial.views.view_tutorial', args=[ + self.pk, slugify(self.title) + ]) + + def get_absolute_url_online(self): + return reverse('zds.tutorial.views.view_tutorial_online', args=[ + self.pk, slugify(self.title) + ]) + + def get_absolute_url_beta(self): + if self.sha_beta is not None: + return reverse('zds.tutorial.views.view_tutorial', args=[ + self.pk, slugify(self.title) + ]) + '?version=' + self.sha_beta + else: + return self.get_absolute_url() + + def get_edit_url(self): + return reverse('zds.tutorial.views.modify_tutorial') + \ + '?tutorial={0}'.format(self.pk) + + def get_subcontainers(self): + return Container.objects.all()\ + .filter(tutorial__pk=self.pk)\ + .order_by('position_in_parent') + + def in_beta(self): + return (self.sha_beta is not None) and (self.sha_beta.strip() != '') + + def in_validation(self): + return (self.sha_validation is not None) and (self.sha_validation.strip() != '') + + def in_drafting(self): + return (self.sha_draft is not None) and (self.sha_draft.strip() != '') + + def on_line(self): + return (self.sha_public is not None) and (self.sha_public.strip() != '') + + def is_article(self): + return self.type == 'ARTICLE' + + def is_tuto(self): + return self.type == 'TUTO' + + def get_path(self, relative=False): + if relative: + return '' + else: + return os.path.join(settings.ZDS_APP['tutorial']['repo_path'], self.get_phy_slug()) + + def get_prod_path(self, sha=None): + data = self.load_json_for_public(sha) + return os.path.join( + settings.ZDS_APP['tutorial']['repo_public_path'], + str(self.pk) + '_' + slugify(data['title'])) + + def load_dic(self, mandata, sha=None): + '''fill mandata with informations from database model''' + + fns = [ + 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', + 'have_epub', 'get_path', 'in_beta', 'in_validation', 'on_line' + ] + + attrs = [ + 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', + 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' + ] + + # load functions and attributs in tree + for fn in fns: + mandata[fn] = getattr(self, fn) + for attr in attrs: + mandata[attr] = getattr(self, attr) + + # general information + mandata['slug'] = slugify(mandata['title']) + mandata['is_beta'] = self.in_beta() and self.sha_beta == sha + mandata['is_validation'] = self.in_validation() \ + and self.sha_validation == sha + mandata['is_on_line'] = self.on_line() and self.sha_public == sha + + # url: + mandata['get_absolute_url'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + + if self.in_beta(): + mandata['get_absolute_url_beta'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + '?version=' + self.sha_beta + + else: + mandata['get_absolute_url_beta'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + + mandata['get_absolute_url_online'] = reverse( + 'zds.tutorial.views.view_tutorial_online', + args=[self.pk, mandata['slug']] + ) + + def load_introduction_and_conclusion(self, mandata, sha=None, public=False): + '''Explicitly load introduction and conclusion to avoid useless disk + access in load_dic() + ''' + + if public: + mandata['get_introduction_online'] = self.get_introduction_online() + mandata['get_conclusion_online'] = self.get_conclusion_online() + else: + mandata['get_introduction'] = self.get_introduction(sha) + mandata['get_conclusion'] = self.get_conclusion(sha) + + def load_json_for_public(self, sha=None): + if sha is None: + sha = self.sha_public + repo = Repo(self.get_path()) + mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') + data = json_reader.loads(mantuto) + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + + def load_json(self, path=None, online=False): + + if path is None: + if online: + man_path = os.path.join(self.get_prod_path(), 'manifest.json') + else: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + if os.path.isfile(man_path): + json_data = open(man_path) + data = json_reader.load(json_data) + json_data.close() + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + + def dump_json(self, path=None): + if path is None: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + dct = export_tutorial(self) + data = json_writer.dumps(dct, indent=4, ensure_ascii=False) + json_data = open(man_path, "w") + json_data.write(data.encode('utf-8')) + json_data.close() + + def get_introduction(self, sha=None): + # find hash code + if sha is None: + sha = self.sha_draft + repo = Repo(self.get_path()) + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + content_version = json_reader.loads(manifest) + if "introduction" in content_version: + path_content_intro = content_version["introduction"] + + if path_content_intro: + return get_blob(repo.commit(sha).tree, path_content_intro) + + def get_introduction_online(self): + """get the introduction content for a particular version if sha is not None""" + if self.on_line(): + intro = open( + os.path.join( + self.get_prod_path(), + self.introduction + + '.html'), + "r") + intro_contenu = intro.read() + intro.close() + + return intro_contenu.decode('utf-8') + + def get_conclusion(self, sha=None): + """get the conclusion content for a particular version if sha is not None""" + # find hash code + if sha is None: + sha = self.sha_draft + repo = Repo(self.get_path()) + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + content_version = json_reader.loads(manifest) + if "introduction" in content_version: + path_content_ccl = content_version["conclusion"] + + if path_content_ccl: + return get_blob(repo.commit(sha).tree, path_content_ccl) + + def get_conclusion_online(self): + """get the conclusion content for the online version of the current publiable content""" + if self.on_line(): + conclusion = open( + os.path.join( + self.get_prod_path(), + self.conclusion + + '.html'), + "r") + conlusion_content = conclusion.read() + conclusion.close() + + return conlusion_content.decode('utf-8') + + def delete_entity_and_tree(self): + """deletes the entity and its filesystem counterpart""" + shutil.rmtree(self.get_path(), 0) + Validation.objects.filter(tutorial=self).delete() + + if self.gallery is not None: + self.gallery.delete() + if self.on_line(): + shutil.rmtree(self.get_prod_path()) + self.delete() + + def save(self, *args, **kwargs): + self.slug = slugify(self.title) + + super(PubliableContent, self).save(*args, **kwargs) + + def get_note_count(self): + """Return the number of notes in the tutorial.""" + return Note.objects.filter(tutorial__pk=self.pk).count() + + def get_last_note(self): + """Gets the last answer in the thread, if any.""" + return Note.objects.all()\ + .filter(tutorial__pk=self.pk)\ + .order_by('-pubdate')\ + .first() + + def first_note(self): + """Return the first post of a topic, written by topic's author.""" + return Note.objects\ + .filter(tutorial=self)\ + .order_by('pubdate')\ + .first() + + def last_read_note(self): + """Return the last post the user has read.""" + try: + return ContentRead.objects\ + .select_related()\ + .filter(tutorial=self, user=get_current_user())\ + .latest('note__pubdate').note + except Note.DoesNotExist: + return self.first_post() + + def first_unread_note(self): + """Return the first note the user has unread.""" + try: + last_note = ContentRead.objects\ + .filter(tutorial=self, user=get_current_user())\ + .latest('note__pubdate').note + + next_note = Note.objects.filter( + tutorial__pk=self.pk, + pubdate__gt=last_note.pubdate)\ + .select_related("author").first() + + return next_note + except: + return self.first_note() + + def antispam(self, user=None): + """Check if the user is allowed to post in an tutorial according to the + SPAM_LIMIT_SECONDS value. + + If user shouldn't be able to note, then antispam is activated + and this method returns True. Otherwise time elapsed between + user's last note and now is enough, and the method will return + False. + + """ + if user is None: + user = get_current_user() + + last_user_notes = Note.objects\ + .filter(tutorial=self)\ + .filter(author=user.pk)\ + .order_by('-position') + + if last_user_notes and last_user_notes[0] == self.last_note: + last_user_note = last_user_notes[0] + t = datetime.now() - last_user_note.pubdate + if t.total_seconds() < settings.ZDS_APP['forum']['spam_limit_seconds']: + return True + return False + + def change_type(self, new_type): + """Allow someone to change the content type, basicaly from tutorial to article""" + if new_type not in TYPE_CHOICES: + raise ValueError("This type of content does not exist") + + self.type = new_type + + + def have_markdown(self): + """Check the markdown zip archive is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".md")) + + def have_html(self): + """Check the html version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".html")) + + def have_pdf(self): + """Check the pdf version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".pdf")) + + def have_epub(self): + """Check the standard epub version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".epub")) + + +class Note(Comment): + + """A comment written by an user about a Publiable content he just read.""" + class Meta: + verbose_name = 'note sur un tutoriel' + verbose_name_plural = 'notes sur un tutoriel' + + related_content = models.ForeignKey(PubliableContent, verbose_name='Tutoriel', db_index=True) + + def __unicode__(self): + """Textual form of a post.""" + return u''.format(self.related_content, self.pk) + + def get_absolute_url(self): + page = int(ceil(float(self.position) / settings.ZDS_APP['forum']['posts_per_page'])) + + return '{0}?page={1}#p{2}'.format( + self.related_content.get_absolute_url_online(), + page, + self.pk) + + +class ContentRead(models.Model): + + """Small model which keeps track of the user viewing tutorials. + + It remembers the topic he looked and what was the last Note at this + time. + + """ + class Meta: + verbose_name = 'Tutoriel lu' + verbose_name_plural = 'Tutoriels lus' + + tutorial = models.ForeignKey(PubliableContent, db_index=True) + note = models.ForeignKey(Note, db_index=True) + user = models.ForeignKey(User, related_name='tuto_notes_read', db_index=True) + + def __unicode__(self): + return u''.format(self.tutorial, + self.user, + self.note.pk) + + +class Extract(models.Model): + + """A content extract from a chapter.""" + class Meta: + verbose_name = 'Extrait' + verbose_name_plural = 'Extraits' + + title = models.CharField('Titre', max_length=80) + container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) + position_in_container = models.IntegerField('Position dans le parent', db_index=True) + + text = models.CharField( + 'chemin relatif du texte', + blank=True, + null=True, + max_length=200) + + def __unicode__(self): + return u''.format(self.title) + + def get_absolute_url(self): + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url(), + self.position_in_chapter, + slugify(self.title) + ) + + def get_absolute_url_online(self): + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_online(), + self.position_in_chapter, + slugify(self.title) + ) + + def get_path(self, relative=False): + if relative: + if self.container.tutorial: + chapter_path = '' + else: + chapter_path = os.path.join( + self.container.part.get_phy_slug(), + self.container.get_phy_slug()) + else: + if self.container.tutorial: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + self.container.tutorial.get_phy_slug()) + else: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + self.container.part.tutorial.get_phy_slug(), + self.container.part.get_phy_slug(), + self.container.get_phy_slug()) + + return os.path.join(chapter_path, str(self.pk) + "_" + slugify(self.title)) + '.md' + + def get_prod_path(self): + + if self.container.tutorial: + data = self.container.tutorial.load_json_for_public() + mandata = self.container.tutorial.load_dic(data) + if "chapter" in mandata: + for ext in mandata["chapter"]["extracts"]: + if ext['pk'] == self.pk: + return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(self.container.tutorial.pk) + '_' + slugify(mandata['title']), + str(ext['pk']) + "_" + slugify(ext['title'])) \ + + '.md.html' + else: + data = self.container.part.tutorial.load_json_for_public() + mandata = self.container.part.tutorial.load_dic(data) + for part in mandata["parts"]: + for chapter in part["chapters"]: + for ext in chapter["extracts"]: + if ext['pk'] == self.pk: + return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(mandata['pk']) + '_' + slugify(mandata['title']), + str(part['pk']) + "_" + slugify(part['title']), + str(chapter['pk']) + "_" + slugify(chapter['title']), + str(ext['pk']) + "_" + slugify(ext['title'])) \ + + '.md.html' + + def get_text(self, sha=None): + + if self.container.tutorial: + tutorial = self.container.tutorial + else: + tutorial = self.container.part.tutorial + repo = Repo(tutorial.get_path()) + + # find hash code + if sha is None: + sha = tutorial.sha_draft + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + tutorial_version = json_reader.loads(manifest) + if "parts" in tutorial_version: + for part in tutorial_version["parts"]: + if "chapters" in part: + for chapter in part["chapters"]: + if "extracts" in chapter: + for extract in chapter["extracts"]: + if extract["pk"] == self.pk: + path_ext = extract["text"] + break + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if "extracts" in chapter: + for extract in chapter["extracts"]: + if extract["pk"] == self.pk: + path_ext = extract["text"] + break + + if path_ext: + return get_blob(repo.commit(sha).tree, path_ext) + else: + return None + + def get_text_online(self): + + if self.container.tutorial: + path = os.path.join( + self.container.tutorial.get_prod_path(), + self.text + + '.html') + else: + path = os.path.join( + self.container.part.tutorial.get_prod_path(), + self.text + + '.html') + + if os.path.isfile(path): + text = open(path, "r") + text_contenu = text.read() + text.close() + + return text_contenu.decode('utf-8') + else: + return None + + +class Validation(models.Model): + + """Tutorial validation.""" + class Meta: + verbose_name = 'Validation' + verbose_name_plural = 'Validations' + + tutorial = models.ForeignKey(PubliableContent, null=True, blank=True, + verbose_name='Tutoriel proposé', db_index=True) + version = models.CharField('Sha1 de la version', + blank=True, null=True, max_length=80, db_index=True) + date_proposition = models.DateTimeField('Date de proposition', db_index=True) + comment_authors = models.TextField('Commentaire de l\'auteur') + validator = models.ForeignKey(User, + verbose_name='Validateur', + related_name='author_validations', + blank=True, null=True, db_index=True) + date_reserve = models.DateTimeField('Date de réservation', + blank=True, null=True) + date_validation = models.DateTimeField('Date de validation', + blank=True, null=True) + comment_validator = models.TextField('Commentaire du validateur', + blank=True, null=True) + status = models.CharField( + max_length=10, + choices=STATUS_CHOICES, + default='PENDING') + + def __unicode__(self): + return self.tutorial.title + + def is_pending(self): + return self.status == 'PENDING' + + def is_pending_valid(self): + return self.status == 'PENDING_V' + + def is_accept(self): + return self.status == 'ACCEPT' + + def is_reject(self): + return self.status == 'REJECT' diff --git a/zds/tutorialv2/search_indexes.py b/zds/tutorialv2/search_indexes.py new file mode 100644 index 0000000000..15e8cf1afa --- /dev/null +++ b/zds/tutorialv2/search_indexes.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +from django.db.models import Q + +from haystack import indexes + +from zds.tutorial.models import Tutorial, Part, Chapter, Extract + + +class TutorialIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr='description') + category = indexes.CharField(model_attr='subcategory') + sha_public = indexes.CharField(model_attr='sha_public') + + def get_model(self): + return Tutorial + + def index_queryset(self, using=None): + """Only tutorials online.""" + return self.get_model().objects.filter(sha_public__isnull=False) + + +class PartIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + tutorial = indexes.CharField(model_attr='tutorial') + + def get_model(self): + return Part + + def index_queryset(self, using=None): + """Only parts online.""" + return self.get_model().objects.filter( + tutorial__sha_public__isnull=False) + + +class ChapterIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + # A Chapter belongs to a Part (big-tuto) **or** a Tutorial (mini-tuto) + part = indexes.CharField(model_attr='part', null=True) + tutorial = indexes.CharField(model_attr='tutorial', null=True) + + def get_model(self): + return Chapter + + def index_queryset(self, using=None): + """Only chapters online.""" + return self.get_model()\ + .objects.filter(Q(tutorial__sha_public__isnull=False) + | Q(part__tutorial__sha_public__isnull=False)) + + +class ExtractIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + chapter = indexes.CharField(model_attr='chapter') + txt = indexes.CharField(model_attr='text') + + def get_model(self): + return Extract + + def index_queryset(self, using=None): + """Only extracts online.""" + return self.get_model() .objects.filter(Q(chapter__tutorial__sha_public__isnull=False) + | Q(chapter__part__tutorial__sha_public__isnull=False)) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py new file mode 100644 index 0000000000..15e3f3e8d3 --- /dev/null +++ b/zds/tutorialv2/urls.py @@ -0,0 +1,123 @@ +# coding: utf-8 + +from django.conf.urls import patterns, url + +from . import views +from . import feeds + +urlpatterns = patterns('', + # Viewing + url(r'^flux/rss/$', feeds.LastTutorialsFeedRSS(), name='tutorial-feed-rss'), + url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), + + # Current URLs + url(r'^recherche/(?P\d+)/$', + 'zds.tutorial.views.find_tuto'), + + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_chapter', + name="view-chapter-url"), + + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_part', + name="view-part-url"), + + url(r'^off/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_tutorial'), + + # View online + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_chapter_online', + name="view-chapter-url-online"), + + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_part_online', + name="view-part-url-online"), + + url(r'^(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_tutorial_online'), + + # Editing + url(r'^editer/tutoriel/$', + 'zds.tutorial.views.edit_tutorial'), + url(r'^modifier/tutoriel/$', + 'zds.tutorial.views.modify_tutorial'), + url(r'^modifier/partie/$', + 'zds.tutorial.views.modify_part'), + url(r'^editer/partie/$', + 'zds.tutorial.views.edit_part'), + url(r'^modifier/chapitre/$', + 'zds.tutorial.views.modify_chapter'), + url(r'^editer/chapitre/$', + 'zds.tutorial.views.edit_chapter'), + url(r'^modifier/extrait/$', + 'zds.tutorial.views.modify_extract'), + url(r'^editer/extrait/$', + 'zds.tutorial.views.edit_extract'), + + # Adding + url(r'^nouveau/tutoriel/$', + 'zds.tutorial.views.add_tutorial'), + url(r'^nouveau/partie/$', + 'zds.tutorial.views.add_part'), + url(r'^nouveau/chapitre/$', + 'zds.tutorial.views.add_chapter'), + url(r'^nouveau/extrait/$', + 'zds.tutorial.views.add_extract'), + + url(r'^$', 'zds.tutorial.views.index'), + url(r'^importer/$', 'zds.tutorial.views.import_tuto'), + url(r'^import_local/$', + 'zds.tutorial.views.local_import'), + url(r'^telecharger/$', 'zds.tutorial.views.download'), + url(r'^telecharger/pdf/$', + 'zds.tutorial.views.download_pdf'), + url(r'^telecharger/html/$', + 'zds.tutorial.views.download_html'), + url(r'^telecharger/epub/$', + 'zds.tutorial.views.download_epub'), + url(r'^telecharger/md/$', + 'zds.tutorial.views.download_markdown'), + url(r'^historique/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.history'), + url(r'^comparaison/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.diff'), + + # user actions + url(r'^suppression/(?P\d+)/$', + 'zds.tutorial.views.delete_tutorial'), + url(r'^validation/tutoriel/$', + 'zds.tutorial.views.ask_validation'), + + # Validation + url(r'^validation/$', + 'zds.tutorial.views.list_validation'), + url(r'^validation/reserver/(?P\d+)/$', + 'zds.tutorial.views.reservation'), + url(r'^validation/reject/$', + 'zds.tutorial.views.reject_tutorial'), + url(r'^validation/valid/$', + 'zds.tutorial.views.valid_tutorial'), + url(r'^validation/invalid/(?P\d+)/$', + 'zds.tutorial.views.invalid_tutorial'), + url(r'^validation/historique/(?P\d+)/$', + 'zds.tutorial.views.history_validation'), + url(r'^activation_js/$', + 'zds.tutorial.views.activ_js'), + # Reactions + url(r'^message/editer/$', + 'zds.tutorial.views.edit_note'), + url(r'^message/nouveau/$', 'zds.tutorial.views.answer'), + url(r'^message/like/$', 'zds.tutorial.views.like_note'), + url(r'^message/dislike/$', + 'zds.tutorial.views.dislike_note'), + + # Moderation + url(r'^resolution_alerte/$', + 'zds.tutorial.views.solve_alert'), + + # Help + url(r'^aides/$', + 'zds.tutorial.views.help_tutorial'), + ) + diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py new file mode 100644 index 0000000000..365abcf760 --- /dev/null +++ b/zds/tutorialv2/utils.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +from zds.tutorialv2.models import PubliableContent, ContentRead +from zds import settings +from zds.utils import get_current_user + +def get_last_tutorials(): + """get the last issued tutorials""" + n = settings.ZDS_APP['tutorial']['home_number'] + tutorials = PubliableContent.objects.all()\ + .exclude(type="ARTICLE")\ + .exclude(sha_public__isnull=True)\ + .exclude(sha_public__exact='')\ + .order_by('-pubdate')[:n] + + return tutorials + + +def get_last_articles(): + """get the last issued articles""" + n = settings.ZDS_APP['tutorial']['home_number'] + articles = PubliableContent.objects.all()\ + .exclude(type="TUTO")\ + .exclude(sha_public__isnull=True)\ + .exclude(sha_public__exact='')\ + .order_by('-pubdate')[:n] + + return articles + + +def never_read(tutorial, user=None): + """Check if a topic has been read by an user since it last post was + added.""" + if user is None: + user = get_current_user() + + return ContentRead.objects\ + .filter(note=tutorial.last_note, tutorial=tutorial, user=user)\ + .count() == 0 + + +def mark_read(tutorial): + """Mark a tutorial as read for the user.""" + if tutorial.last_note is not None: + ContentRead.objects.filter( + tutorial=tutorial, + user=get_current_user()).delete() + a = ContentRead( + note=tutorial.last_note, + tutorial=tutorial, + user=get_current_user()) + a.save() \ No newline at end of file diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py new file mode 100644 index 0000000000..efd77f81f0 --- /dev/null +++ b/zds/tutorialv2/views.py @@ -0,0 +1,3643 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from collections import OrderedDict +from datetime import datetime +from operator import attrgetter +from urllib import urlretrieve +from django.contrib.humanize.templatetags.humanize import naturaltime +from urlparse import urlparse, parse_qs +try: + import ujson as json_reader +except ImportError: + try: + import simplejson as json_reader + except ImportError: + import json as json_reader +import json +import json as json_writer +import shutil +import re +import zipfile +import os +import glob +import tempfile + +from PIL import Image as ImagePIL +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.core.files import File +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.core.urlresolvers import reverse +from django.db import transaction +from django.db.models import Q, Count +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.encoding import smart_str +from django.views.decorators.http import require_POST +from git import Repo, Actor +from lxml import etree + +from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ + ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm +from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ + mark_read, Note, HelpWriting +from zds.gallery.models import Gallery, UserGallery, Image +from zds.member.decorator import can_write_and_read_now +from zds.member.models import get_info_old_tuto, Profile +from zds.member.views import get_client_ip +from zds.forum.models import Forum, Topic +from zds.utils import slugify +from zds.utils.models import Alert +from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ + SubCategory +from zds.utils.mps import send_mp +from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic +from zds.utils.paginator import paginator_range +from zds.utils.templatetags.emarkdown import emarkdown +from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive +from zds.utils.misc import compute_hash, content_has_changed +from django.utils.translation import ugettext as _ + + +def render_chapter_form(chapter): + if chapter.part: + return ChapterForm({"title": chapter.title, + "introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + else: + + return \ + EmbdedChapterForm({"introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + + +def index(request): + """Display all public tutorials of the website.""" + + # The tag indicate what the category tutorial the user would like to + # display. We can display all subcategories for tutorials. + + try: + tag = get_object_or_404(SubCategory, slug=request.GET["tag"]) + except (KeyError, Http404): + tag = None + if tag is None: + tutorials = \ + Tutorial.objects.filter(sha_public__isnull=False).exclude(sha_public="") \ + .order_by("-pubdate") \ + .all() + else: + # The tag isn't None and exist in the system. We can use it to retrieve + # all tutorials in the subcategory specified. + + tutorials = Tutorial.objects.filter( + sha_public__isnull=False, + subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata) + tuto_versions.append(mandata) + return render(request, "tutorial/index.html", {"tutorials": tuto_versions, "tag": tag}) + + +# Staff actions. + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +def list_validation(request): + """Display tutorials list in validation.""" + + # Retrieve type of the validation. Default value is all validations. + + try: + type = request.GET["type"] + except KeyError: + type = None + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + + # Orphan validation. There aren't validator attached to the validations. + + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": + + # Reserved validation. There are a validator attached to the + # validations. + + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + else: + + # Default, we display all validations. + + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" + + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.tutorial.get_absolute_url() + + "?version=" + validation.version + ) + + +@login_required +def diff(request, tutorial_pk, tutorial_slug): + try: + sha = request.GET["sha"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + repo = Repo(tutorial.get_path()) + hcommit = repo.commit(sha) + tdiff = hcommit.diff("HEAD~1") + return render(request, "tutorial/tutorial/diff.html", { + "tutorial": tutorial, + "path_add": tdiff.iter_change_type("A"), + "path_ren": tdiff.iter_change_type("R"), + "path_del": tdiff.iter_change_type("D"), + "path_maj": tdiff.iter_change_type("M"), + }) + + +@login_required +def history(request, tutorial_pk, tutorial_slug): + """History of the tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + repo = Repo(tutorial.get_path()) + logs = repo.head.reference.log() + logs = sorted(logs, key=attrgetter("time"), reverse=True) + return render(request, "tutorial/tutorial/history.html", + {"tutorial": tutorial, "logs": logs}) + + +@login_required +@permission_required("tutorial.change_tutorial", raise_exception=True) +def history_validation(request, tutorial_pk): + """History of the validation of a tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + if subcategory is None: + validations = \ + Validation.objects.filter(tutorial__pk=tutorial_pk) \ + .order_by("date_proposition" + ).all() + else: + validations = Validation.objects.filter(tutorial__pk=tutorial_pk, + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition" + ).all() + return render(request, "tutorial/validation/history.html", + {"validations": validations, "tutorial": tutorial}) + + +@can_write_and_read_now +@login_required +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def reject_tutorial(request): + """Staff reject tutorial of an author.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + validation.comment_validator = request.POST["text"] + validation.status = "REJECT" + validation.date_validation = datetime.now() + validation.save() + + # Remove sha_validation because we rejected this version of the tutorial. + + tutorial.sha_validation = None + tutorial.pubdate = None + tutorial.save() + messages.info(request, _(u"Le tutoriel a bien été refusé.")) + comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) + # send feedback + msg = ( + _(u'Désolé, le zeste **{0}** n\'a malheureusement ' + u'pas passé l’étape de validation. Mais ne désespère pas, ' + u'certaines corrections peuvent surement être faite pour ' + u'l’améliorer et repasser la validation plus tard. ' + u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' + u'N\'hésite pas a lui envoyer un petit message pour discuter ' + u'de la décision ou demander plus de détail si tout cela te ' + u'semble injuste ou manque de clarté.') + .format(tutorial.title, + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), + comment_reject)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Refus de Validation : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le refuser.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +@can_write_and_read_now +@login_required +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def valid_tutorial(request): + """Staff valid tutorial of an author.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + (output, err) = mep(tutorial, tutorial.sha_validation) + messages.info(request, output) + messages.error(request, err) + validation.comment_validator = request.POST["text"] + validation.status = "ACCEPT" + validation.date_validation = datetime.now() + validation.save() + + # Update sha_public with the sha of validation. We don't update sha_draft. + # So, the user can continue to edit his tutorial in offline. + + if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': + tutorial.pubdate = datetime.now() + tutorial.sha_public = validation.version + tutorial.source = request.POST["source"] + tutorial.sha_validation = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été validé.")) + + # send feedback + + msg = ( + _(u'Félicitations ! Le zeste [{0}]({1}) ' + u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' + u'peuvent venir l\'éplucher et réagir a son sujet. ' + u'Je te conseille de rester a leur écoute afin ' + u'd\'apporter des corrections/compléments.' + u'Un Tutoriel vivant et a jour est bien plus lu ' + u'qu\'un sujet abandonné !') + .format(tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Publication : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le valider.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +@can_write_and_read_now +@login_required +@permission_required("tutorial.change_tutorial", raise_exception=True) +@require_POST +def invalid_tutorial(request, tutorial_pk): + """Staff invalid tutorial of an author.""" + + # Retrieve current tutorial + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + un_mep(tutorial) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_public).latest("date_proposition") + validation.status = "PENDING" + validation.date_validation = None + validation.save() + + # Only update sha_validation because contributors can contribute on + # rereading version. + + tutorial.sha_public = None + tutorial.sha_validation = validation.version + tutorial.pubdate = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été dépublié.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +# User actions on tutorial. + +@can_write_and_read_now +@login_required +@require_POST +def ask_validation(request): + """User ask validation for his tutorial.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator + else: + old_validator = None + # delete old pending validation + Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING', 'PENDING_V'])\ + .delete() + # We create and save validation object of the tutorial. + + validation = Validation() + validation.tutorial = tutorial + validation.date_proposition = datetime.now() + validation.comment_authors = request.POST["text"] + validation.version = request.POST["version"] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' + u'consulter l\'historique des versions' + u'\n\n> Merci').format(old_validator.username, tutorial.title)) + send_mp( + bot, + [old_validator], + _(u"Mise à jour de tuto : {0}").format(tutorial.title), + _(u"En validation"), + msg, + False, + ) + validation.save() + validation.tutorial.source = request.POST["source"] + validation.tutorial.sha_validation = request.POST["version"] + validation.tutorial.save() + messages.success(request, + _(u"Votre demande de validation a été envoyée à l'équipe.")) + return redirect(tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +@require_POST +def delete_tutorial(request, tutorial_pk): + """User would like delete his tutorial.""" + + # Retrieve current tutorial + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # when author is alone we can delete definitively tutorial + + if tutorial.authors.count() == 1: + + # user can access to gallery + + try: + ug = UserGallery.objects.filter(user=request.user, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + + # Delete the tutorial on the repo and on the database. + + old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + maj_repo_tuto(request, old_slug_path=old_slug, tuto=tutorial, + action="del") + messages.success(request, + _(u'Le tutoriel {0} a bien ' + u'été supprimé.').format(tutorial.title)) + tutorial.delete() + else: + tutorial.authors.remove(request.user) + + # user can access to gallery + + try: + ug = UserGallery.objects.filter( + user=request.user, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + tutorial.save() + messages.success(request, + _(u'Vous ne faites plus partie des rédacteurs de ce ' + u'tutoriel.')) + return redirect(reverse("zds.tutorial.views.index")) + + +@can_write_and_read_now +@require_POST +def modify_tutorial(request): + tutorial_pk = request.POST["tutorial"] + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + # User actions + + if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): + if "add_author" in request.POST: + redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ + tutorial.pk, + tutorial.slug, + ]) + author_username = request.POST["author"] + author = None + try: + author = User.objects.get(username=author_username) + except User.DoesNotExist: + return redirect(redirect_url) + tutorial.authors.add(author) + tutorial.save() + + # share gallery + + ug = UserGallery() + ug.user = author + ug.gallery = tutorial.gallery + ug.mode = "W" + ug.save() + messages.success(request, + _(u'L\'auteur {0} a bien été ajouté à la rédaction ' + u'du tutoriel.').format(author.username)) + + # send msg to new author + + msg = ( + _(u'Bonjour **{0}**,\n\n' + u'Tu as été ajouté comme auteur du tutoriel [{1}]({2}).\n' + u'Tu peux retrouver ce tutoriel en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' + u'"Tutoriels" sur la page de ton profil.\n\n' + u'Tu peux maintenant commencer à rédiger !').format( + author.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url(), + settings.ZDS_APP['site']['url'] + reverse("zds.member.views.tutorials")) + ) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + [author], + _(u"Ajout en tant qu'auteur : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + + return redirect(redirect_url) + elif "remove_author" in request.POST: + redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ + tutorial.pk, + tutorial.slug, + ]) + + # Avoid orphan tutorials + + if tutorial.authors.all().count() <= 1: + raise Http404 + author_pk = request.POST["author"] + author = get_object_or_404(User, pk=author_pk) + tutorial.authors.remove(author) + + # user can access to gallery + + try: + ug = UserGallery.objects.filter(user=author, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + tutorial.save() + messages.success(request, + _(u"L'auteur {0} a bien été retiré du tutoriel.") + .format(author.username)) + + # send msg to removed author + + msg = ( + _(u'Bonjour **{0}**,\n\n' + u'Tu as été supprimé des auteurs du tutoriel [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' + u'pourra plus y accéder.\n').format( + author.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url()) + ) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + [author], + _(u"Suppression des auteurs : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + + return redirect(redirect_url) + elif "activ_beta" in request.POST: + if "version" in request.POST: + tutorial.sha_beta = request.POST['version'] + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id'])\ + .first() + msg = \ + (_(u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerais obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide').format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + + create_topic(author=request.user, + forum=forum, + title=_(u"[beta][tutoriel]{0}").format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + tp = Topic.objects.get(key=tutorial.pk) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + private_mp = \ + (_(u'Bonjour {},\n\n' + u'Vous venez de mettre votre tutoriel **{}** en beta. La communauté ' + u'pourra le consulter afin de vous faire des retours ' + u'constructifs avant sa soumission en validation.\n\n' + u'Un sujet dédié pour la beta de votre tutoriel a été ' + u'crée dans le forum et est accessible [ici]({})').format( + request.user.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tp.get_absolute_url())) + send_mp( + bot, + [request.user], + _(u"Tutoriel en beta : {0}").format(tutorial.title), + "", + private_mp, + False, + ) + else: + msg_up = \ + (_(u'Bonjour,\n\n' + u'La beta du tutoriel est de nouveau active.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures').format(tutorial.title, + settings.ZDS_APP['site']['url'] + + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) + + messages.success(request, _(u"La BETA sur ce tutoriel est bien activée.")) + else: + messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être activée.")) + return redirect(tutorial.get_absolute_url_beta()) + elif "update_beta" in request.POST: + if "version" in request.POST: + tutorial.sha_beta = request.POST['version'] + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, + forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() + msg = \ + (_(u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide').format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + + create_topic(author=request.user, + forum=forum, + title=u"[beta][tutoriel]{0}".format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + else: + msg_up = \ + (_(u'Bonjour, !\n\n' + u'La beta du tutoriel a été mise à jour.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures').format(tutorial.title, + settings.ZDS_APP['site']['url'] + + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) + messages.success(request, _(u"La BETA sur ce tutoriel a bien été mise à jour.")) + else: + messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être mise à jour.")) + return redirect(tutorial.get_absolute_url_beta()) + elif "desactiv_beta" in request.POST: + tutorial.sha_beta = None + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() + if topic is not None: + msg = \ + (_(u'Désactivation de la beta du tutoriel **{}**' + u'\n\nPour plus d\'informations envoyez moi un message privé.').format(tutorial.title)) + lock_topic(topic) + send_post(topic, msg) + messages.info(request, _(u"La BETA sur ce tutoriel a bien été désactivée.")) + + return redirect(tutorial.get_absolute_url()) + + # No action performed, raise 403 + + raise PermissionDenied + + +# Tutorials. + + +@login_required +def view_tutorial(request, tutorial_pk, tutorial_slug): + """Show the given offline tutorial if exists.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the article. + + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # Two variables to handle two distinct cases (large/small tutorial) + + chapter = None + parts = None + + # Find the good manifest file + + repo = Repo(tutorial.get_path()) + + # Load the tutorial. + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha) + tutorial.load_introduction_and_conclusion(mandata, sha) + + # If it's a small tutorial, fetch its chapter + + if tutorial.type == "MINI": + if 'chapter' in mandata: + chapter = mandata["chapter"] + chapter["path"] = tutorial.get_path() + chapter["type"] = "MINI" + chapter["pk"] = Chapter.objects.get(tutorial=tutorial).pk + chapter["intro"] = get_blob(repo.commit(sha).tree, + "introduction.md") + chapter["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" + ) + cpt = 1 + for ext in chapter["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt += 1 + else: + chapter = None + else: + + # If it's a big tutorial, fetch parts. + + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + part["tutorial"] = tutorial + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt_e += 1 + cpt_c += 1 + cpt_p += 1 + validation = Validation.objects.filter(tutorial__pk=tutorial.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": tutorial.js_support}) + + if tutorial.source: + form_ask_validation = AskValidationForm(initial={"source": tutorial.source}) + form_valid = ValidForm(initial={"source": tutorial.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + return render(request, "tutorial/tutorial/view.html", { + "tutorial": mandata, + "chapter": chapter, + "parts": parts, + "version": sha, + "validation": validation, + "formAskValidation": form_ask_validation, + "formJs": form_js, + "formValid": form_valid, + "formReject": form_reject, + "is_js": is_js + }) + + +def view_tutorial_online(request, tutorial_pk, tutorial_slug): + """Display a tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the tutorial isn't online, we raise 404 error. + if not tutorial.on_line(): + raise Http404 + + # Two variables to handle two distinct cases (large/small tutorial) + + chapter = None + parts = None + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + tutorial.load_introduction_and_conclusion(mandata, public=True) + mandata["update"] = tutorial.update + mandata["get_note_count"] = tutorial.get_note_count() + + # If it's a small tutorial, fetch its chapter + + if tutorial.type == "MINI": + if "chapter" in mandata: + chapter = mandata["chapter"] + chapter["path"] = tutorial.get_prod_path() + chapter["type"] = "MINI" + intro = open(os.path.join(tutorial.get_prod_path(), + mandata["introduction"] + ".html"), "r") + chapter["intro"] = intro.read() + intro.close() + conclu = open(os.path.join(tutorial.get_prod_path(), + mandata["conclusion"] + ".html"), "r") + chapter["conclu"] = conclu.read() + conclu.close() + cpt = 1 + for ext in chapter["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = tutorial.get_prod_path() + text = open(os.path.join(tutorial.get_prod_path(), ext["text"] + + ".html"), "r") + ext["txt"] = text.read() + text.close() + cpt += 1 + else: + chapter = None + else: + + # chapter = Chapter.objects.get(tutorial=tutorial) + + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + part["tutorial"] = mandata + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + cpt_e += 1 + cpt_c += 1 + part["get_chapters"] = part["chapters"] + cpt_p += 1 + + mandata['get_parts'] = parts + + # If the user is authenticated + if request.user.is_authenticated(): + # We check if he can post a tutorial or not with + # antispam filter. + mandata['antispam'] = tutorial.antispam() + + # If the user is never read, we mark this tutorial read. + if never_read(tutorial): + mark_read(tutorial) + + # Find all notes of the tutorial. + + notes = Note.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() + + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. + + last_note_pk = 0 + if tutorial.last_note: + last_note_pk = tutorial.last_note.pk + + # Handle pagination + + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) + try: + page_nbr = int(request.GET["page"]) + except KeyError: + page_nbr = 1 + except ValueError: + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + + res = [] + if page_nbr != 1: + + # Show the last note of the previous page + + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) + + # Build form to send a note for the current tutorial. + + form = NoteForm(tutorial, request.user) + return render(request, "tutorial/tutorial/view_online.html", { + "tutorial": mandata, + "chapter": chapter, + "parts": parts, + "notes": res, + "pages": paginator_range(page_nbr, paginator.num_pages), + "nb": page_nbr, + "last_note_pk": last_note_pk, + "form": form, + }) + + +@can_write_and_read_now +@login_required +def add_tutorial(request): + """'Adds a tutorial.""" + + if request.method == "POST": + form = TutorialForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + + # Creating a tutorial + + tutorial = Tutorial() + tutorial.title = data["title"] + tutorial.description = data["description"] + tutorial.type = data["type"] + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + tutorial.images = "images" + if "licence" in data and data["licence"] != "": + lc = Licence.objects.filter(pk=data["licence"]).all()[0] + tutorial.licence = lc + else: + tutorial.licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + + # add create date + + tutorial.create_at = datetime.now() + tutorial.pubdate = datetime.now() + + # Creating the gallery + + gal = Gallery() + gal.title = data["title"] + gal.slug = slugify(data["title"]) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = gal + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + tutorial.image = img + tutorial.save() + + # Add subcategories on tutorial + + for subcat in form.cleaned_data["subcategory"]: + tutorial.subcategory.add(subcat) + + # Add helps if needed + for helpwriting in form.cleaned_data["helps"]: + tutorial.helps.add(helpwriting) + + # We need to save the tutorial before changing its author list + # since it's a many-to-many relationship + + tutorial.authors.add(request.user) + + # If it's a small tutorial, create its corresponding chapter + + if tutorial.type == "MINI": + chapter = Chapter() + chapter.tutorial = tutorial + chapter.save() + tutorial.save() + maj_repo_tuto( + request, + new_slug_path=tutorial.get_path(), + tuto=tutorial, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + return redirect(tutorial.get_absolute_url()) + else: + form = TutorialForm( + initial={ + 'licence': Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + } + ) + return render(request, "tutorial/tutorial/new.html", {"form": form}) + + +@can_write_and_read_now +@login_required +def edit_tutorial(request): + """Edit a tutorial.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.GET["tutoriel"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + introduction = os.path.join(tutorial.get_path(), "introduction.md") + conclusion = os.path.join(tutorial.get_path(), "conclusion.md") + if request.method == "POST": + form = TutorialForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = TutorialForm(initial={ + "title": tutorial.title, + "type": tutorial.type, + "licence": tutorial.licence, + "description": tutorial.description, + "subcategory": tutorial.subcategory.all(), + "introduction": tutorial.get_introduction(), + "conclusion": tutorial.get_conclusion(), + "helps": tutorial.helps.all(), + }) + return render(request, "tutorial/tutorial/edit.html", + { + "tutorial": tutorial, "form": form, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True + }) + old_slug = tutorial.get_path() + tutorial.title = data["title"] + tutorial.description = data["description"] + if "licence" in data and data["licence"] != "": + lc = Licence.objects.filter(pk=data["licence"]).all()[0] + tutorial.licence = lc + else: + tutorial.licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + + # add MAJ date + + tutorial.update = datetime.now() + + # MAJ gallery + + gal = Gallery.objects.filter(pk=tutorial.gallery.pk) + gal.update(title=data["title"]) + gal.update(slug=slugify(data["title"])) + gal.update(update=datetime.now()) + + # MAJ image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = tutorial.gallery + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + tutorial.image = img + tutorial.save() + tutorial.update_children() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + + maj_repo_tuto( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + tuto=tutorial, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + + tutorial.subcategory.clear() + for subcat in form.cleaned_data["subcategory"]: + tutorial.subcategory.add(subcat) + + tutorial.helps.clear() + for help in form.cleaned_data["helps"]: + tutorial.helps.add(help) + + tutorial.save() + return redirect(tutorial.get_absolute_url()) + else: + json = tutorial.load_json() + if "licence" in json: + licence = json['licence'] + else: + licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + form = TutorialForm(initial={ + "title": json["title"], + "type": json["type"], + "licence": licence, + "description": json["description"], + "subcategory": tutorial.subcategory.all(), + "introduction": tutorial.get_introduction(), + "conclusion": tutorial.get_conclusion(), + "helps": tutorial.helps.all(), + }) + return render(request, "tutorial/tutorial/edit.html", + {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])}) + +# Parts. + + +@login_required +def view_part( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, +): + """Display a part.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + final_part = None + + # find the good manifest file + + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha=sha) + + parts = mandata["parts"] + find = False + cpt_p = 1 + for part in parts: + if part_pk == str(part["pk"]): + find = True + part["tutorial"] = tutorial + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) + part["conclu"] = get_blob(repo.commit(sha).tree, part["conclusion"]) + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + cpt_e += 1 + cpt_c += 1 + final_part = part + break + cpt_p += 1 + + # if part can't find + if not find: + raise Http404 + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + + return render(request, "tutorial/part/view.html", + {"tutorial": mandata, + "part": final_part, + "version": sha, + "is_js": is_js}) + + +def view_part_online( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, +): + """Display a part.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if not tutorial.on_line(): + raise Http404 + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + mandata["update"] = tutorial.update + + mandata["get_parts"] = mandata["parts"] + parts = mandata["parts"] + cpt_p = 1 + final_part = None + find = False + for part in parts: + part["tutorial"] = mandata + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + if part_pk == str(part["pk"]): + find = True + intro = open(os.path.join(tutorial.get_prod_path(), + part["introduction"] + ".html"), "r") + part["intro"] = intro.read() + intro.close() + conclu = open(os.path.join(tutorial.get_prod_path(), + part["conclusion"] + ".html"), "r") + part["conclu"] = conclu.read() + conclu.close() + final_part = part + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + if part_slug == slugify(part["title"]): + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_prod_path() + cpt_e += 1 + cpt_c += 1 + part["get_chapters"] = part["chapters"] + cpt_p += 1 + + # if part can't find + if not find: + raise Http404 + + return render(request, "tutorial/part/view_online.html", {"part": final_part}) + + +@can_write_and_read_now +@login_required +def add_part(request): + """Add a new part.""" + + try: + tutorial_pk = request.GET["tutoriel"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Make sure it's a big tutorial, just in case + + if not tutorial.type == "BIG": + raise Http404 + + # Make sure the user belongs to the author list + + if request.user not in tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = PartForm(request.POST) + if form.is_valid(): + data = form.data + part = Part() + part.tutorial = tutorial + part.title = data["title"] + part.position_in_tutorial = tutorial.get_parts().count() + 1 + part.save() + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part.save() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + + maj_repo_part( + request, + new_slug_path=new_slug, + part=part, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + if "submit_continue" in request.POST: + form = PartForm() + messages.success(request, + _(u'La partie « {0} » a été ajoutée ' + u'avec succès.').format(part.title)) + else: + return redirect(part.get_absolute_url()) + else: + form = PartForm() + return render(request, "tutorial/part/new.html", {"tutorial": tutorial, + "form": form}) + + +@can_write_and_read_now +@login_required +def modify_part(request): + """Modifiy the given part.""" + + if not request.method == "POST": + raise Http404 + part_pk = request.POST["part"] + part = get_object_or_404(Part, pk=part_pk) + + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if "move" in request.POST: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + # Invalid conversion, maybe the user played with the move button + return redirect(part.tutorial.get_absolute_url()) + + move(part, new_pos, "position_in_tutorial", "tutorial", "get_parts") + part.save() + + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) + + maj_repo_tuto(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + tuto=part.tutorial, + action="maj", + msg=_(u"Déplacement de la partie {} ").format(part.title)) + elif "delete" in request.POST: + # Delete all chapters belonging to the part + + Chapter.objects.all().filter(part=part).delete() + + # Move other parts + + old_pos = part.position_in_tutorial + for tut_p in part.tutorial.get_parts(): + if old_pos <= tut_p.position_in_tutorial: + tut_p.position_in_tutorial = tut_p.position_in_tutorial - 1 + tut_p.save() + old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + maj_repo_part(request, old_slug_path=old_slug, part=part, action="del") + + new_slug_tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) + # Actually delete the part + part.delete() + + maj_repo_tuto(request, + old_slug_path=new_slug_tuto_path, + new_slug_path=new_slug_tuto_path, + tuto=part.tutorial, + action="maj", + msg=_(u"Suppression de la partie {} ").format(part.title)) + return redirect(part.tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_part(request): + """Edit the given part.""" + + try: + part_pk = int(request.GET["partie"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + part = get_object_or_404(Part, pk=part_pk) + introduction = os.path.join(part.get_path(), "introduction.md") + conclusion = os.path.join(part.get_path(), "conclusion.md") + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = PartForm(request.POST) + if form.is_valid(): + data = form.data + # avoid collision + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = PartForm({"title": part.title, + "introduction": part.get_introduction(), + "conclusion": part.get_conclusion()}) + return render(request, "tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) + # Update title and his slug. + + part.title = data["title"] + old_slug = part.get_path() + part.save() + + # Update path for introduction and conclusion. + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part.save() + part.update_children() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + + maj_repo_part( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + part=part, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(part.get_absolute_url()) + else: + form = PartForm({"title": part.title, + "introduction": part.get_introduction(), + "conclusion": part.get_conclusion()}) + return render(request, "tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "form": form + }) + + +# Chapters. + + +@login_required +def view_chapter( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, + chapter_pk, + chapter_slug, +): + """View chapter.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # find the good manifest file + + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha=sha) + + parts = mandata["parts"] + cpt_p = 1 + final_chapter = None + chapter_tab = [] + final_position = 0 + find = False + for part in parts: + cpt_c = 1 + part["slug"] = slugify(part["title"]) + part["get_absolute_url"] = reverse( + "zds.tutorial.views.view_part", + args=[ + tutorial.pk, + tutorial.slug, + part["pk"], + part["slug"]]) + part["tutorial"] = tutorial + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + chapter["get_absolute_url"] = part["get_absolute_url"] \ + + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) + if chapter_pk == str(chapter["pk"]): + find = True + chapter["intro"] = get_blob(repo.commit(sha).tree, + chapter["introduction"]) + chapter["conclu"] = get_blob(repo.commit(sha).tree, + chapter["conclusion"]) + + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt_e += 1 + chapter_tab.append(chapter) + if chapter_pk == str(chapter["pk"]): + final_chapter = chapter + final_position = len(chapter_tab) - 1 + cpt_c += 1 + cpt_p += 1 + + # if chapter can't find + if not find: + raise Http404 + + prev_chapter = (chapter_tab[final_position - 1] if final_position + > 0 else None) + next_chapter = (chapter_tab[final_position + 1] if final_position + 1 + < len(chapter_tab) else None) + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + + return render(request, "tutorial/chapter/view.html", { + "tutorial": mandata, + "chapter": final_chapter, + "prev": prev_chapter, + "next": next_chapter, + "version": sha, + "is_js": is_js + }) + + +def view_chapter_online( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, + chapter_pk, + chapter_slug, +): + """View chapter.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if not tutorial.on_line(): + raise Http404 + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + mandata["update"] = tutorial.update + + mandata['get_parts'] = mandata["parts"] + parts = mandata["parts"] + cpt_p = 1 + final_chapter = None + chapter_tab = [] + final_position = 0 + + find = False + for part in parts: + cpt_c = 1 + part["slug"] = slugify(part["title"]) + part["get_absolute_url_online"] = reverse( + "zds.tutorial.views.view_part_online", + args=[ + tutorial.pk, + tutorial.slug, + part["pk"], + part["slug"]]) + part["tutorial"] = mandata + part["position_in_tutorial"] = cpt_p + part["get_chapters"] = part["chapters"] + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_prod_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + chapter["get_absolute_url_online"] = part[ + "get_absolute_url_online"] + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) + if chapter_pk == str(chapter["pk"]): + find = True + intro = open( + os.path.join( + tutorial.get_prod_path(), + chapter["introduction"] + + ".html"), + "r") + chapter["intro"] = intro.read() + intro.close() + conclu = open( + os.path.join( + tutorial.get_prod_path(), + chapter["conclusion"] + + ".html"), + "r") + chapter["conclu"] = conclu.read() + conclu.close() + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + text = open(os.path.join(tutorial.get_prod_path(), + ext["text"] + ".html"), "r") + ext["txt"] = text.read() + text.close() + cpt_e += 1 + else: + intro = None + conclu = None + chapter_tab.append(chapter) + if chapter_pk == str(chapter["pk"]): + final_chapter = chapter + final_position = len(chapter_tab) - 1 + cpt_c += 1 + cpt_p += 1 + + # if chapter can't find + if not find: + raise Http404 + + prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None) + next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) + + return render(request, "tutorial/chapter/view_online.html", { + "chapter": final_chapter, + "parts": parts, + "prev": prev_chapter, + "next": next_chapter, + }) + + +@can_write_and_read_now +@login_required +def add_chapter(request): + """Add a new chapter to given part.""" + + try: + part_pk = request.GET["partie"] + except KeyError: + raise Http404 + part = get_object_or_404(Part, pk=part_pk) + + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = ChapterForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + chapter = Chapter() + chapter.title = data["title"] + chapter.part = part + chapter.position_in_part = part.get_chapters().count() + 1 + chapter.update_position_in_tutorial() + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = part.tutorial.gallery + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + chapter.image = img + + chapter.save() + if chapter.tutorial: + chapter_path = os.path.join( + os.path.join( + settings.ZDS_APP['tutorial']['repo_path'], + chapter.tutorial.get_phy_slug()), + chapter.get_phy_slug()) + chapter.introduction = os.path.join(chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join(chapter.get_phy_slug(), + "conclusion.md") + else: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + chapter.introduction = os.path.join( + chapter.part.get_phy_slug(), + chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug(), "conclusion.md") + chapter.save() + maj_repo_chapter( + request, + new_slug_path=chapter_path, + chapter=chapter, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + if "submit_continue" in request.POST: + form = ChapterForm() + messages.success(request, + _(u'Le chapitre « {0} » a été ajouté ' + u'avec succès.').format(chapter.title)) + else: + return redirect(chapter.get_absolute_url()) + else: + form = ChapterForm() + + return render(request, "tutorial/chapter/new.html", {"part": part, + "form": form}) + + +@can_write_and_read_now +@login_required +def modify_chapter(request): + """Modify the given chapter.""" + + if not request.method == "POST": + raise Http404 + data = request.POST + try: + chapter_pk = request.POST["chapter"] + except KeyError: + raise Http404 + chapter = get_object_or_404(Chapter, pk=chapter_pk) + + # Make sure the user is allowed to do that + + if request.user not in chapter.get_tutorial().authors.all() and \ + not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if "move" in data: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + + # User misplayed with the move button + + return redirect(chapter.get_absolute_url()) + move(chapter, new_pos, "position_in_part", "part", "get_chapters") + chapter.update_position_in_tutorial() + chapter.save() + + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug()) + + maj_repo_part(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + part=chapter.part, + action="maj", + msg=_(u"Déplacement du chapitre {}").format(chapter.title)) + messages.info(request, _(u"Le chapitre a bien été déplacé.")) + elif "delete" in data: + old_pos = chapter.position_in_part + old_tut_pos = chapter.position_in_tutorial + + if chapter.part: + parent = chapter.part + else: + parent = chapter.tutorial + + # Move other chapters first + + for tut_c in chapter.part.get_chapters(): + if old_pos <= tut_c.position_in_part: + tut_c.position_in_part = tut_c.position_in_part - 1 + tut_c.save() + maj_repo_chapter(request, chapter=chapter, + old_slug_path=chapter.get_path(), action="del") + + # Then delete the chapter + new_slug_path_part = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug()) + chapter.delete() + + # Update all the position_in_tutorial fields for the next chapters + + for tut_c in \ + Chapter.objects.filter(position_in_tutorial__gt=old_tut_pos): + tut_c.update_position_in_tutorial() + tut_c.save() + + maj_repo_part(request, + old_slug_path=new_slug_path_part, + new_slug_path=new_slug_path_part, + part=chapter.part, + action="maj", + msg=_(u"Suppression du chapitre {}").format(chapter.title)) + messages.info(request, _(u"Le chapitre a bien été supprimé.")) + + return redirect(parent.get_absolute_url()) + + return redirect(chapter.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_chapter(request): + """Edit the given chapter.""" + + try: + chapter_pk = int(request.GET["chapitre"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + chapter = get_object_or_404(Chapter, pk=chapter_pk) + big = chapter.part + small = chapter.tutorial + + # Make sure the user is allowed to do that + + if (big and request.user not in chapter.part.tutorial.authors.all() + or small and request.user not in chapter.tutorial.authors.all())\ + and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + introduction = os.path.join(chapter.get_path(), "introduction.md") + conclusion = os.path.join(chapter.get_path(), "conclusion.md") + if request.method == "POST": + + if chapter.part: + form = ChapterForm(request.POST, request.FILES) + gal = chapter.part.tutorial.gallery + else: + form = EmbdedChapterForm(request.POST, request.FILES) + gal = chapter.tutorial.gallery + if form.is_valid(): + data = form.data + # avoid collision + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = render_chapter_form(chapter) + return render(request, "tutorial/part/edit.html", + { + "chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) + chapter.title = data["title"] + + old_slug = chapter.get_path() + chapter.save() + chapter.update_children() + + if chapter.part: + if chapter.tutorial: + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.tutorial.get_phy_slug(), + chapter.get_phy_slug()) + else: + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = gal + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + chapter.image = img + maj_repo_chapter( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + chapter=chapter, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(chapter.get_absolute_url()) + else: + form = render_chapter_form(chapter) + return render(request, "tutorial/chapter/edit.html", {"chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), + "form": form}) + + +@login_required +def add_extract(request): + """Add extract.""" + + try: + chapter_pk = int(request.GET["chapitre"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + chapter = get_object_or_404(Chapter, pk=chapter_pk) + part = chapter.part + + # If part exist, we check if the user is in authors of the tutorial of the + # part or If part doesn't exist, we check if the user is in authors of the + # tutorial of the chapter. + + if part and request.user not in chapter.part.tutorial.authors.all() \ + or not part and request.user not in chapter.tutorial.authors.all(): + + # If the user isn't an author or a staff, we raise an exception. + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + data = request.POST + + # Using the « preview button » + + if "preview" in data: + form = ExtractForm(initial={"title": data["title"], + "text": data["text"], + 'msg_commit': data['msg_commit']}) + return render(request, "tutorial/extract/new.html", + {"chapter": chapter, "form": form}) + else: + + # Save extract. + + form = ExtractForm(request.POST) + if form.is_valid(): + data = form.data + extract = Extract() + extract.chapter = chapter + extract.position_in_chapter = chapter.get_extract_count() + 1 + extract.title = data["title"] + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract(request, new_slug_path=extract.get_path(), + extract=extract, text=data["text"], + action="add", + msg=request.POST.get('msg_commit', None)) + return redirect(extract.get_absolute_url()) + else: + form = ExtractForm() + + return render(request, "tutorial/extract/new.html", {"chapter": chapter, + "form": form}) + + +@can_write_and_read_now +@login_required +def edit_extract(request): + """Edit extract.""" + try: + extract_pk = request.GET["extrait"] + except KeyError: + raise Http404 + extract = get_object_or_404(Extract, pk=extract_pk) + part = extract.chapter.part + + # If part exist, we check if the user is in authors of the tutorial of the + # part or If part doesn't exist, we check if the user is in authors of the + # tutorial of the chapter. + + if part and request.user \ + not in extract.chapter.part.tutorial.authors.all() or not part \ + and request.user not in extract.chapter.tutorial.authors.all(): + + # If the user isn't an author or a staff, we raise an exception. + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + if request.method == "POST": + data = request.POST + # Using the « preview button » + + if "preview" in data: + form = ExtractForm(initial={ + "title": data["title"], + "text": data["text"], + 'msg_commit': data['msg_commit'] + }) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, "form": form, + "last_hash": compute_hash([extract.get_path()]) + }) + else: + if content_has_changed([extract.get_path()], data["last_hash"]): + form = ExtractForm(initial={ + "title": extract.title, + "text": extract.get_text(), + 'msg_commit': data['msg_commit']}) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "new_version": True, + "form": form + }) + # Edit extract. + + form = ExtractForm(request.POST) + if form.is_valid(): + data = form.data + old_slug = extract.get_path() + extract.title = data["title"] + extract.text = extract.get_path(relative=True) + + # Use path retrieve before and use it to create the new slug. + extract.save() + new_slug = extract.get_path() + + maj_repo_extract( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + extract=extract, + text=data["text"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(extract.get_absolute_url()) + else: + form = ExtractForm({"title": extract.title, + "text": extract.get_text()}) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "form": form + }) + + +@can_write_and_read_now +def modify_extract(request): + if not request.method == "POST": + raise Http404 + data = request.POST + try: + extract_pk = request.POST["extract"] + except KeyError: + raise Http404 + extract = get_object_or_404(Extract, pk=extract_pk) + chapter = extract.chapter + if "delete" in data: + pos_current_extract = extract.position_in_chapter + for extract_c in extract.chapter.get_extracts(): + if pos_current_extract <= extract_c.position_in_chapter: + extract_c.position_in_chapter = extract_c.position_in_chapter \ + - 1 + extract_c.save() + + # Use path retrieve before and use it to create the new slug. + + old_slug = extract.get_path() + + if extract.chapter.tutorial: + new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug()) + else: + new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + maj_repo_extract(request, old_slug_path=old_slug, extract=extract, + action="del") + + maj_repo_chapter(request, + old_slug_path=new_slug_path_chapter, + new_slug_path=new_slug_path_chapter, + chapter=chapter, + action="maj", + msg=_(u"Suppression de l'extrait {}").format(extract.title)) + return redirect(chapter.get_absolute_url()) + elif "move" in data: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + # Error, the user misplayed with the move button + return redirect(extract.get_absolute_url()) + + move(extract, new_pos, "position_in_chapter", "chapter", "get_extracts") + extract.save() + + if extract.chapter.tutorial: + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug()) + else: + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + maj_repo_chapter(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + chapter=chapter, + action="maj", + msg=_(u"Déplacement de l'extrait {}").format(extract.title)) + return redirect(extract.get_absolute_url()) + raise Http404 + + +def find_tuto(request, pk_user): + try: + type = request.GET["type"] + except KeyError: + type = None + display_user = get_object_or_404(User, pk=pk_user) + if type == "beta": + tutorials = Tutorial.objects.all().filter( + authors__in=[display_user], + sha_beta__isnull=False).exclude(sha_beta="").order_by("-pubdate") + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public(sha=tutorial.sha_beta) + tutorial.load_dic(mandata, sha=tutorial.sha_beta) + tuto_versions.append(mandata) + + return render(request, "tutorial/member/beta.html", + {"tutorials": tuto_versions, "usr": display_user}) + else: + tutorials = Tutorial.objects.all().filter( + authors__in=[display_user], + sha_public__isnull=False).exclude(sha_public="").order_by("-pubdate") + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata) + tuto_versions.append(mandata) + + return render(request, "tutorial/member/online.html", {"tutorials": tuto_versions, + "usr": display_user}) + + +def upload_images(images, tutorial): + mapping = OrderedDict() + + # download images + + zfile = zipfile.ZipFile(images, "a") + os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + for i in zfile.namelist(): + ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + try: + data = zfile.read(i) + fp = open(ph_temp, "wb") + fp.write(data) + fp.close() + f = File(open(ph_temp, "rb")) + f.name = os.path.basename(i) + pic = Image() + pic.gallery = tutorial.gallery + pic.title = os.path.basename(i) + pic.pubdate = datetime.now() + pic.physical = f + pic.save() + mapping[i] = pic.physical.url + f.close() + except IOError: + try: + os.makedirs(ph_temp) + except OSError: + pass + zfile.close() + return mapping + + +def replace_real_url(md_text, dict): + for (dt_old, dt_new) in dict.iteritems(): + md_text = md_text.replace(dt_old, dt_new) + return md_text + + +def import_content( + request, + tuto, + images, + logo, +): + tutorial = Tutorial() + + # add create date + + tutorial.create_at = datetime.now() + tree = etree.parse(tuto) + racine_big = tree.xpath("/bigtuto") + racine_mini = tree.xpath("/minituto") + if len(racine_big) > 0: + + # it's a big tuto + + tutorial.type = "BIG" + tutorial_title = tree.xpath("/bigtuto/titre")[0] + tutorial_intro = tree.xpath("/bigtuto/introduction")[0] + tutorial_conclu = tree.xpath("/bigtuto/conclusion")[0] + tutorial.title = tutorial_title.text.strip() + tutorial.description = tutorial_title.text.strip() + tutorial.images = "images" + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + + # Creating the gallery + + gal = Gallery() + gal.title = tutorial_title.text + gal.slug = slugify(tutorial_title.text) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + tutorial.save() + tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + mapping = upload_images(images, tutorial) + maj_repo_tuto( + request, + new_slug_path=tuto_path, + tuto=tutorial, + introduction=replace_real_url(tutorial_intro.text, mapping), + conclusion=replace_real_url(tutorial_conclu.text, mapping), + action="add", + ) + tutorial.authors.add(request.user) + part_count = 1 + for partie in tree.xpath("/bigtuto/parties/partie"): + part_title = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/titre")[0] + part_intro = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/introduction")[0] + part_conclu = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/conclusion")[0] + part = Part() + part.title = part_title.text.strip() + part.position_in_tutorial = part_count + part.tutorial = tutorial + part.save() + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + part.save() + maj_repo_part( + request, + None, + part_path, + part, + replace_real_url(part_intro.text, mapping), + replace_real_url(part_conclu.text, mapping), + action="add", + ) + chapter_count = 1 + for chapitre in tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre"): + chapter_title = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/titre")[0] + chapter_intro = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/introduction")[0] + chapter_conclu = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/conclusion")[0] + chapter = Chapter() + chapter.title = chapter_title.text.strip() + chapter.position_in_part = chapter_count + chapter.position_in_tutorial = part_count * chapter_count + chapter.part = part + chapter.save() + chapter.introduction = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + "conclusion.md") + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + chapter.save() + maj_repo_chapter( + request, + new_slug_path=chapter_path, + chapter=chapter, + introduction=replace_real_url(chapter_intro.text, + mapping), + conclusion=replace_real_url(chapter_conclu.text, mapping), + action="add", + ) + extract_count = 1 + for souspartie in tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/chapitres/chapitre[" + + str(chapter_count) + "]/sousparties/souspartie"): + extract_title = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/sousparties/souspartie[" + + str(extract_count) + + "]/titre")[0] + extract_text = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/sousparties/souspartie[" + + str(extract_count) + + "]/texte")[0] + extract = Extract() + extract.title = extract_title.text.strip() + extract.position_in_chapter = extract_count + extract.chapter = chapter + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract( + request, + new_slug_path=extract.get_path(), + extract=extract, + text=replace_real_url( + extract_text.text, + mapping), + action="add") + extract_count += 1 + chapter_count += 1 + part_count += 1 + elif len(racine_mini) > 0: + + # it's a mini tuto + + tutorial.type = "MINI" + tutorial_title = tree.xpath("/minituto/titre")[0] + tutorial_intro = tree.xpath("/minituto/introduction")[0] + tutorial_conclu = tree.xpath("/minituto/conclusion")[0] + tutorial.title = tutorial_title.text.strip() + tutorial.description = tutorial_title.text.strip() + tutorial.images = "images" + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + + # Creating the gallery + + gal = Gallery() + gal.title = tutorial_title.text + gal.slug = slugify(tutorial_title.text) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + tutorial.save() + tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + mapping = upload_images(images, tutorial) + maj_repo_tuto( + request, + new_slug_path=tuto_path, + tuto=tutorial, + introduction=replace_real_url(tutorial_intro.text, mapping), + conclusion=replace_real_url(tutorial_conclu.text, mapping), + action="add", + ) + tutorial.authors.add(request.user) + chapter = Chapter() + chapter.tutorial = tutorial + chapter.save() + extract_count = 1 + for souspartie in tree.xpath("/minituto/sousparties/souspartie"): + extract_title = tree.xpath("/minituto/sousparties/souspartie[" + + str(extract_count) + "]/titre")[0] + extract_text = tree.xpath("/minituto/sousparties/souspartie[" + + str(extract_count) + "]/texte")[0] + extract = Extract() + extract.title = extract_title.text.strip() + extract.position_in_chapter = extract_count + extract.chapter = chapter + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract(request, new_slug_path=extract.get_path(), + extract=extract, + text=replace_real_url(extract_text.text, + mapping), action="add") + extract_count += 1 + + +@can_write_and_read_now +@login_required +@require_POST +def local_import(request): + import_content(request, request.POST["tuto"], request.POST["images"], + request.POST["logo"]) + return redirect(reverse("zds.member.views.tutorials")) + + +@can_write_and_read_now +@login_required +def import_tuto(request): + if request.method == "POST": + # for import tuto + if "import-tuto" in request.POST: + form = ImportForm(request.POST, request.FILES) + if form.is_valid(): + import_content(request, request.FILES["file"], request.FILES["images"], "") + return redirect(reverse("zds.member.views.tutorials")) + else: + form_archive = ImportArchiveForm(user=request.user) + + elif "import-archive" in request.POST: + form_archive = ImportArchiveForm(request.user, request.POST, request.FILES) + if form_archive.is_valid(): + (check, reason) = import_archive(request) + if not check: + messages.error(request, reason) + else: + messages.success(request, reason) + return redirect(reverse("zds.member.views.tutorials")) + else: + form = ImportForm() + + else: + form = ImportForm() + form_archive = ImportArchiveForm(user=request.user) + + profile = get_object_or_404(Profile, user=request.user) + oldtutos = [] + if profile.sdz_tutorial: + olds = profile.sdz_tutorial.strip().split(":") + else: + olds = [] + for old in olds: + oldtutos.append(get_info_old_tuto(old)) + return render( + request, + "tutorial/tutorial/import.html", + {"form": form, "form_archive": form_archive, "old_tutos": oldtutos} + ) + + +# Handling repo +def maj_repo_tuto( + request, + old_slug_path=None, + new_slug_path=None, + tuto=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + if action == "del": + shutil.rmtree(old_slug_path) + else: + if action == "maj": + if old_slug_path != new_slug_path: + shutil.move(old_slug_path, new_slug_path) + repo = Repo(new_slug_path) + msg = _(u"Modification du tutoriel : «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + repo = Repo.init(new_slug_path, bare=False) + msg = _(u"Création du tutoriel «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg)).strip() + repo = Repo(new_slug_path) + index = repo.index + man_path = os.path.join(new_slug_path, "manifest.json") + tuto.dump_json(path=man_path) + index.add(["manifest.json"]) + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + index.add(["introduction.md"]) + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + index.add(["conclusion.md"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + tuto.sha_draft = com.hexsha + tuto.save() + + +def maj_repo_part( + request, + old_slug_path=None, + new_slug_path=None, + part=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + repo = Repo(part.tutorial.get_path()) + index = repo.index + if action == "del": + shutil.rmtree(old_slug_path) + msg = _(u"Suppresion de la partie : «{}»").format(part.title) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + + msg = _(u"Modification de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + index.add([part.get_phy_slug()]) + man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + part.tutorial.dump_json(path=man_path) + index.add(["manifest.json"]) + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + )]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['litteral_name']) + com_part = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + part.tutorial.sha_draft = com_part.hexsha + part.tutorial.save() + part.save() + + +def maj_repo_chapter( + request, + old_slug_path=None, + new_slug_path=None, + chapter=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + if chapter.tutorial: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.tutorial.get_phy_slug())) + ph = None + else: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug())) + ph = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug()) + index = repo.index + if action == "del": + shutil.rmtree(old_slug_path) + msg = _(u"Suppresion du chapitre : «{}»").format(chapter.title) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + if chapter.tutorial: + msg = _(u"Modification du tutoriel «{}» " + u"{} {}").format(chapter.tutorial.title, get_sep(msg), get_text_is_empty(msg)).strip() + else: + msg = _(u"Modification du chapitre «{}» " + u"{} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg)).strip() + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + msg = _(u"Création du chapitre «{}» {} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + if ph is not None: + index.add([ph]) + + # update manifest + + if chapter.tutorial: + man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + chapter.tutorial.dump_json(path=man_path) + else: + man_path = os.path.join(chapter.part.tutorial.get_path(), + "manifest.json") + chapter.part.tutorial.dump_json(path=man_path) + index.add(["manifest.json"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com_ch = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + if chapter.tutorial: + chapter.tutorial.sha_draft = com_ch.hexsha + chapter.tutorial.save() + else: + chapter.part.tutorial.sha_draft = com_ch.hexsha + chapter.part.tutorial.save() + chapter.save() + + +def maj_repo_extract( + request, + old_slug_path=None, + new_slug_path=None, + extract=None, + text=None, + action=None, + msg=None, +): + + if extract.chapter.tutorial: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug())) + else: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.part.tutorial.get_phy_slug())) + index = repo.index + + chap = extract.chapter + + if action == "del": + msg = _(u"Suppression de l'extrait : «{}»").format(extract.title) + extract.delete() + if old_slug_path: + os.remove(old_slug_path) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + msg = _(u"Mise à jour de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + elif action == "add": + msg = _(u"Création de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + ext = open(new_slug_path, "w") + ext.write(smart_str(text).strip()) + ext.close() + index.add([extract.get_path(relative=True)]) + + # update manifest + if chap.tutorial: + man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + chap.tutorial.dump_json(path=man_path) + else: + man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + chap.part.tutorial.dump_json(path=man_path) + + index.add(["manifest.json"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com_ex = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + if chap.tutorial: + chap.tutorial.sha_draft = com_ex.hexsha + chap.tutorial.save() + else: + chap.part.tutorial.sha_draft = com_ex.hexsha + chap.part.tutorial.save() + + +def insert_into_zip(zip_file, git_tree): + """recursively add files from a git_tree to a zip archive""" + for blob in git_tree.blobs: # first, add files : + zip_file.writestr(blob.path, blob.data_stream.read()) + if len(git_tree.trees) is not 0: # then, recursively add dirs : + for subtree in git_tree.trees: + insert_into_zip(zip_file, subtree) + + +def download(request): + """Download a tutorial.""" + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + + repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + repo = Repo(repo_path) + sha = tutorial.sha_draft + if 'online' in request.GET and tutorial.on_line(): + sha = tutorial.sha_public + elif request.user not in tutorial.authors.all(): + if not request.user.has_perm('tutorial.change_tutorial'): + raise PermissionDenied # Only authors can download draft version + zip_path = os.path.join(tempfile.gettempdir(), tutorial.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(sha).tree) + zip_file.close() + response = HttpResponse(open(zip_path, "rb").read(), content_type="application/zip") + response["Content-Disposition"] = "attachment; filename={0}.zip".format(tutorial.slug) + os.remove(zip_path) + return response + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +def download_markdown(request): + """Download a markdown tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".md") + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/txt") + response["Content-Disposition"] = \ + "attachment; filename={0}.md".format(tutorial.slug) + return response + + +def download_html(request): + """Download a pdf tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".html") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="text/html") + response["Content-Disposition"] = \ + "attachment; filename={0}.html".format(tutorial.slug) + return response + + +def download_pdf(request): + """Download a pdf tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".pdf") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/pdf") + response["Content-Disposition"] = \ + "attachment; filename={0}.pdf".format(tutorial.slug) + return response + + +def download_epub(request): + """Download an epub tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".epub") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/epub") + response["Content-Disposition"] = \ + "attachment; filename={0}.epub".format(tutorial.slug) + return response + + +def get_url_images(md_text, pt): + """find images urls in markdown text and download this.""" + + regex = ur"(!\[.*?\]\()(.+?)(\))" + unknow_path = os.path.join(settings.SITE_ROOT, "fixtures", "noir_black.png") + + # if text is empty don't download + + if md_text is not None: + imgs = re.findall(regex, md_text) + for img in imgs: + real_url = img[1] + # decompose images + parse_object = urlparse(real_url) + if parse_object.query != '': + resp = parse_qs(urlparse(img[1]).query, keep_blank_values=True) + real_url = resp["u"][0] + parse_object = urlparse(real_url) + + # if link is http type + if parse_object.scheme in ["http", "https", "ftp"] or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + (filepath, filename) = os.path.split(parse_object.path) + if not os.path.isdir(os.path.join(pt, "images")): + os.makedirs(os.path.join(pt, "images")) + + # download image + down_path = os.path.abspath(os.path.join(pt, "images", filename)) + try: + urlretrieve(real_url, down_path) + try: + ext = filename.split(".")[-1] + im = ImagePIL.open(down_path) + # if image is gif, convert to png + if ext == "gif": + im.save(os.path.join(pt, "images", filename.split(".")[0] + ".png")) + except IOError: + ext = filename.split(".")[-1] + im = ImagePIL.open(unknow_path) + if ext == "gif": + im.save(os.path.join(pt, "images", filename.split(".")[0] + ".png")) + else: + im.save(os.path.join(pt, "images", filename)) + except IOError: + pass + else: + # relative link + srcfile = settings.SITE_ROOT + real_url + if os.path.isfile(srcfile): + dstroot = pt + real_url + dstdir = os.path.dirname(dstroot) + if not os.path.exists(dstdir): + os.makedirs(dstdir) + shutil.copy(srcfile, dstroot) + + try: + ext = dstroot.split(".")[-1] + im = ImagePIL.open(dstroot) + # if image is gif, convert to png + if ext == "gif": + im.save(os.path.join(dstroot.split(".")[0] + ".png")) + except IOError: + ext = dstroot.split(".")[-1] + im = ImagePIL.open(unknow_path) + if ext == "gif": + im.save(os.path.join(dstroot.split(".")[0] + ".png")) + else: + im.save(os.path.join(dstroot)) + + +def sub_urlimg(g): + start = g.group("start") + url = g.group("url") + parse_object = urlparse(url) + if parse_object.query != '': + resp = parse_qs(urlparse(url).query, keep_blank_values=True) + parse_object = urlparse(resp["u"][0]) + (filepath, filename) = os.path.split(parse_object.path) + if filename != '': + mark = g.group("mark") + ext = filename.split(".")[-1] + if ext == "gif": + if parse_object.scheme in ("http", "https") or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + url = os.path.join("images", filename.split(".")[0] + ".png") + else: + url = (url.split(".")[0])[1:] + ".png" + else: + if parse_object.scheme in ("http", "https") or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + url = os.path.join("images", filename) + else: + url = url[1:] + end = g.group("end") + return start + mark + url + end + else: + return start + + +def markdown_to_out(md_text): + return re.sub(ur"(?P)(?P!\[.*?\]\()(?P.+?)(?P\))", sub_urlimg, + md_text) + + +def mep(tutorial, sha): + (output, err) = (None, None) + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + tutorial_version = json_reader.loads(manifest) + + prod_path = tutorial.get_prod_path(sha) + + pattern = os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], str(tutorial.pk) + '_*') + del_paths = glob.glob(pattern) + for del_path in del_paths: + if os.path.isdir(del_path): + try: + shutil.rmtree(del_path) + except OSError: + shutil.rmtree(u"\\\\?\{0}".format(del_path)) + # WARNING: this can throw another OSError + shutil.copytree(tutorial.get_path(), prod_path) + repo.head.reset(commit=sha, index=True, working_tree=True) + + # collect md files + + fichiers = [] + fichiers.append(tutorial_version["introduction"]) + fichiers.append(tutorial_version["conclusion"]) + if "parts" in tutorial_version: + for part in tutorial_version["parts"]: + fichiers.append(part["introduction"]) + fichiers.append(part["conclusion"]) + if "chapters" in part: + for chapter in part["chapters"]: + fichiers.append(chapter["introduction"]) + fichiers.append(chapter["conclusion"]) + if "extracts" in chapter: + for extract in chapter["extracts"]: + fichiers.append(extract["text"]) + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if "extracts" in tutorial_version["chapter"]: + for extract in chapter["extracts"]: + fichiers.append(extract["text"]) + + # convert markdown file to html file + + for fichier in fichiers: + md_file_contenu = get_blob(repo.commit(sha).tree, fichier) + + # download images + + get_url_images(md_file_contenu, prod_path) + + # convert to out format + out_file = open(os.path.join(prod_path, fichier), "w") + if md_file_contenu is not None: + out_file.write(markdown_to_out(md_file_contenu.encode("utf-8"))) + out_file.close() + target = os.path.join(prod_path, fichier + ".html") + os.chdir(os.path.dirname(target)) + try: + html_file = open(target, "w") + except IOError: + + # handle limit of 255 on windows + + target = u"\\\\?\{0}".format(target) + html_file = open(target, "w") + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + if md_file_contenu is not None: + html_file.write(emarkdown(md_file_contenu, is_js)) + html_file.close() + + # load markdown out + + contenu = export_tutorial_to_md(tutorial, sha).lstrip() + out_file = open(os.path.join(prod_path, tutorial.slug + ".md"), "w") + out_file.write(smart_str(contenu)) + out_file.close() + + # define whether to log pandoc's errors + + pandoc_debug_str = "" + if settings.PANDOC_LOG_STATE: + pandoc_debug_str = " 2>&1 | tee -a " + settings.PANDOC_LOG + + # load pandoc + + os.chdir(prod_path) + os.system(settings.PANDOC_LOC + + "pandoc --latex-engine=xelatex -s -S --toc " + + os.path.join(prod_path, tutorial.slug) + + ".md -o " + os.path.join(prod_path, + tutorial.slug) + ".html" + pandoc_debug_str) + os.system(settings.PANDOC_LOC + "pandoc " + settings.PANDOC_PDF_PARAM + " " + + os.path.join(prod_path, tutorial.slug) + ".md " + + "-o " + os.path.join(prod_path, tutorial.slug) + + ".pdf" + pandoc_debug_str) + os.system(settings.PANDOC_LOC + "pandoc -s -S --toc " + + os.path.join(prod_path, tutorial.slug) + + ".md -o " + os.path.join(prod_path, + tutorial.slug) + ".epub" + pandoc_debug_str) + os.chdir(settings.SITE_ROOT) + return (output, err) + + +def un_mep(tutorial): + del_paths = glob.glob(os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(tutorial.pk) + '_*')) + for del_path in del_paths: + if os.path.isdir(del_path): + try: + shutil.rmtree(del_path) + except OSError: + shutil.rmtree(u"\\\\?\{0}".format(del_path)) + # WARNING: this can throw another OSError + + +@can_write_and_read_now +@login_required +def answer(request): + """Adds an answer from a user to an tutorial.""" + + try: + tutorial_pk = request.GET["tutorial"] + except KeyError: + raise Http404 + + # Retrieve current tutorial. + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Making sure reactioning is allowed + + if tutorial.is_locked: + raise PermissionDenied + + # Check that the user isn't spamming + + if tutorial.antispam(request.user): + raise PermissionDenied + + # Retrieve 3 last notes of the current tutorial. + + notes = Note.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] + + # If there is a last notes for the tutorial, we save his pk. Otherwise, we + # save 0. + + last_note_pk = 0 + if tutorial.last_note: + last_note_pk = tutorial.last_note.pk + + # Retrieve lasts notes of the current tutorial. + notes = Note.objects.filter(tutorial=tutorial) \ + .prefetch_related() \ + .order_by("-pubdate")[:settings.ZDS_APP['forum']['posts_per_page']] + + # User would like preview his post or post a new note on the tutorial. + + if request.method == "POST": + data = request.POST + newnote = last_note_pk != int(data["last_note"]) + + # Using the « preview button », the « more » button or new note + + if "preview" in data or newnote: + form = NoteForm(tutorial, request.user, + initial={"text": data["text"]}) + if request.is_ajax(): + return HttpResponse(json.dumps({"text": emarkdown(data["text"])}), + content_type='application/json') + else: + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "last_note_pk": last_note_pk, + "newnote": newnote, + "notes": notes, + "form": form, + }) + else: + + # Saving the message + + form = NoteForm(tutorial, request.user, request.POST) + if form.is_valid(): + data = form.data + note = Note() + note.related_content = tutorial + note.author = request.user + note.text = data["text"] + note.text_html = emarkdown(data["text"]) + note.pubdate = datetime.now() + note.position = tutorial.get_note_count() + 1 + note.ip_address = get_client_ip(request) + note.save() + tutorial.last_note = note + tutorial.save() + return redirect(note.get_absolute_url()) + else: + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "last_note_pk": last_note_pk, + "newnote": newnote, + "notes": notes, + "form": form, + }) + else: + + # Actions from the editor render to answer.html. + text = "" + + # Using the quote button + + if "cite" in request.GET: + note_cite_pk = request.GET["cite"] + note_cite = Note.objects.get(pk=note_cite_pk) + if not note_cite.is_visible: + raise PermissionDenied + + for line in note_cite.text.splitlines(): + text = text + "> " + line + "\n" + + text = u'{0}Source:[{1}]({2}{3})'.format( + text, + note_cite.author.username, + settings.ZDS_APP['site']['url'], + note_cite.get_absolute_url()) + + form = NoteForm(tutorial, request.user, initial={"text": text}) + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "notes": notes, + "last_note_pk": last_note_pk, + "form": form, + }) + + +@can_write_and_read_now +@login_required +@require_POST +@transaction.atomic +def solve_alert(request): + + # only staff can move topic + + if not request.user.has_perm("tutorial.change_note"): + raise PermissionDenied + + alert = get_object_or_404(Alert, pk=request.POST["alert_pk"]) + note = Note.objects.get(pk=alert.comment.id) + + if "text" in request.POST and request.POST["text"] != "": + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' + u'dans le tutoriel [{2}]({3}). Votre alerte a été traitée par **{4}** ' + u'et il vous a laissé le message suivant :' + u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !').format( + alert.author.username, + note.author.username, + note.tutorial.title, + settings.ZDS_APP['site']['url'] + note.get_absolute_url(), + request.user.username, + request.POST["text"],)) + send_mp( + bot, + [alert.author], + _(u"Résolution d'alerte : {0}").format(note.tutorial.title), + "", + msg, + False, + ) + alert.delete() + messages.success(request, _(u"L'alerte a bien été résolue.")) + return redirect(note.get_absolute_url()) + + +@login_required +@require_POST +def activ_js(request): + + # only for staff + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + tutorial = get_object_or_404(Tutorial, pk=request.POST["tutorial"]) + tutorial.js_support = "js_support" in request.POST + tutorial.save() + + return redirect(tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_note(request): + """Edit the given user's note.""" + + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + note = get_object_or_404(Note, pk=note_pk) + g_tutorial = None + if note.position >= 1: + g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) + + # Making sure the user is allowed to do that. Author of the note must to be + # the user logged. + + if note.author != request.user \ + and not request.user.has_perm("tutorial.change_note") \ + and "signal_message" not in request.POST: + raise PermissionDenied + if note.author != request.user and request.method == "GET" \ + and request.user.has_perm("tutorial.change_note"): + messages.add_message(request, messages.WARNING, + _(u'Vous éditez ce message en tant que ' + u'modérateur (auteur : {}). Soyez encore plus ' + u'prudent lors de l\'édition de ' + u'celui-ci !').format(note.author.username)) + note.alerts.all().delete() + if request.method == "POST": + if "delete_message" in request.POST: + if note.author == request.user \ + or request.user.has_perm("tutorial.change_note"): + note.alerts.all().delete() + note.is_visible = False + if request.user.has_perm("tutorial.change_note"): + note.text_hidden = request.POST["text_hidden"] + note.editor = request.user + if "show_message" in request.POST: + if request.user.has_perm("tutorial.change_note"): + note.is_visible = True + note.text_hidden = "" + if "signal_message" in request.POST: + alert = Alert() + alert.author = request.user + alert.comment = note + alert.scope = Alert.TUTORIAL + alert.text = request.POST["signal_text"] + alert.pubdate = datetime.now() + alert.save() + + # Using the preview button + if "preview" in request.POST: + form = NoteForm(g_tutorial, request.user, + initial={"text": request.POST["text"]}) + form.helper.form_action = reverse("zds.tutorial.views.edit_note") \ + + "?message=" + str(note_pk) + if request.is_ajax(): + return HttpResponse(json.dumps({"text": emarkdown(request.POST["text"])}), + content_type='application/json') + else: + return render(request, + "tutorial/comment/edit.html", + {"note": note, "tutorial": g_tutorial, "form": form}) + if "delete_message" not in request.POST and "signal_message" \ + not in request.POST and "show_message" not in request.POST: + + # The user just sent data, handle them + + if request.POST["text"].strip() != "": + note.text = request.POST["text"] + note.text_html = emarkdown(request.POST["text"]) + note.update = datetime.now() + note.editor = request.user + note.save() + return redirect(note.get_absolute_url()) + else: + form = NoteForm(g_tutorial, request.user, initial={"text": note.text}) + form.helper.form_action = reverse("zds.tutorial.views.edit_note") \ + + "?message=" + str(note_pk) + return render(request, "tutorial/comment/edit.html", {"note": note, "tutorial": g_tutorial, "form": form}) + + +@can_write_and_read_now +@login_required +def like_note(request): + """Like a note.""" + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + resp = {} + note = get_object_or_404(Note, pk=note_pk) + + user = request.user + if note.author.pk != request.user.pk: + + # Making sure the user is allowed to do that + + if CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() == 0: + like = CommentLike() + like.user = user + like.comments = note + note.like = note.like + 1 + note.save() + like.save() + if CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() > 0: + CommentDislike.objects.filter( + user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.dislike = note.dislike - 1 + note.save() + else: + CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.like = note.like - 1 + note.save() + resp["upvotes"] = note.like + resp["downvotes"] = note.dislike + if request.is_ajax(): + return HttpResponse(json_writer.dumps(resp)) + else: + return redirect(note.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def dislike_note(request): + """Dislike a note.""" + + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + resp = {} + note = get_object_or_404(Note, pk=note_pk) + user = request.user + if note.author.pk != request.user.pk: + + # Making sure the user is allowed to do that + + if CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() == 0: + dislike = CommentDislike() + dislike.user = user + dislike.comments = note + note.dislike = note.dislike + 1 + note.save() + dislike.save() + if CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() > 0: + CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.like = note.like - 1 + note.save() + else: + CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.dislike = note.dislike - 1 + note.save() + resp["upvotes"] = note.like + resp["downvotes"] = note.dislike + if request.is_ajax(): + return HttpResponse(json_writer.dumps(resp)) + else: + return redirect(note.get_absolute_url()) + + +def help_tutorial(request): + """fetch all tutorials that needs help""" + + # Retrieve type of the help. Default value is any help + type = request.GET.get('type', None) + + if type is not None: + aide = get_object_or_404(HelpWriting, slug=type) + tutos = Tutorial.objects.filter(helps=aide) \ + .all() + else: + tutos = Tutorial.objects.annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + + # Paginator + paginator = Paginator(tutos, settings.ZDS_APP['forum']['topics_per_page']) + page = request.GET.get('page') + + try: + shown_tutos = paginator.page(page) + page = int(page) + except PageNotAnInteger: + shown_tutos = paginator.page(1) + page = 1 + except EmptyPage: + shown_tutos = paginator.page(paginator.num_pages) + page = paginator.num_pages + + aides = HelpWriting.objects.all() + + return render(request, "tutorial/tutorial/help.html", { + "tutorials": shown_tutos, + "helps": aides, + "pages": paginator_range(page, paginator.num_pages), + "nb": page + }) From e1043b0eaba7a764902d69147bb4b70abb86f69a Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 22 Dec 2014 09:39:38 +0100 Subject: [PATCH 002/887] =?UTF-8?q?mod=C3=A8le=20stabilis=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 4 +- zds/tutorialv2/admin.py | 9 +- zds/tutorialv2/migrations/0001_initial.py | 290 ++++++++++++++++++++++ zds/tutorialv2/migrations/__init__.py | 0 zds/tutorialv2/models.py | 90 ++++--- zds/tutorialv2/utils.py | 10 +- zds/tutorialv2/views.py | 20 +- 7 files changed, 367 insertions(+), 56 deletions(-) create mode 100644 zds/tutorialv2/migrations/0001_initial.py create mode 100644 zds/tutorialv2/migrations/__init__.py diff --git a/zds/settings.py b/zds/settings.py index 5b76ab95f3..191be8b447 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -187,6 +187,7 @@ 'zds.article', 'zds.forum', 'zds.tutorial', + 'zds.tutorialv2', 'zds.member', # Uncomment the next line to enable the admin: 'django.contrib.admin', @@ -451,7 +452,8 @@ 'repo_public_path': os.path.join(SITE_ROOT, 'tutoriels-public'), 'default_license_pk': 7, 'home_number': 5, - 'helps_per_page': 20 + 'helps_per_page': 20, + 'max_tree_depth': 3 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py index 0907547f20..e966d5f7f4 100644 --- a/zds/tutorialv2/admin.py +++ b/zds/tutorialv2/admin.py @@ -2,12 +2,11 @@ from django.contrib import admin -from .models import Tutorial, Part, Chapter, Extract, Validation, Note +from .models import PublishableContent, Container, Extract, Validation, ContentReaction -admin.site.register(Tutorial) -admin.site.register(Part) -admin.site.register(Chapter) +admin.site.register(PublishableContent) +admin.site.register(Container) admin.site.register(Extract) admin.site.register(Validation) -admin.site.register(Note) +admin.site.register(ContentReaction) diff --git a/zds/tutorialv2/migrations/0001_initial.py b/zds/tutorialv2/migrations/0001_initial.py new file mode 100644 index 0000000000..5dca0239ca --- /dev/null +++ b/zds/tutorialv2/migrations/0001_initial.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Container' + db.create_table(u'tutorialv2_container', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), + ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), + ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal(u'tutorialv2', ['Container']) + + # Adding model 'PublishableContent' + db.create_table(u'tutorialv2_publishablecontent', ( + (u'container_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('source', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Image'], null=True, on_delete=models.SET_NULL, blank=True)), + ('gallery', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Gallery'], null=True, blank=True)), + ('creation_date', self.gf('django.db.models.fields.DateTimeField')()), + ('pubdate', self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True)), + ('update_date', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('sha_public', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_beta', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_validation', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_draft', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('licence', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['utils.Licence'], null=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=10, db_index=True)), + ('images', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('last_note', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='last_note', null=True, to=orm['tutorialv2.ContentReaction'])), + ('is_locked', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('js_support', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'tutorialv2', ['PublishableContent']) + + # Adding M2M table for field authors on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_authors') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('user', models.ForeignKey(orm[u'auth.user'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'user_id']) + + # Adding M2M table for field subcategory on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_subcategory') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('subcategory', models.ForeignKey(orm[u'utils.subcategory'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'subcategory_id']) + + # Adding model 'ContentReaction' + db.create_table(u'tutorialv2_contentreaction', ( + (u'comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['utils.Comment'], unique=True, primary_key=True)), + ('related_content', self.gf('django.db.models.fields.related.ForeignKey')(related_name='related_content_note', to=orm['tutorialv2.PublishableContent'])), + )) + db.send_create_signal(u'tutorialv2', ['ContentReaction']) + + # Adding model 'ContentRead' + db.create_table(u'tutorialv2_contentread', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), + ('note', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.ContentReaction'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_notes_read', to=orm['auth.User'])), + )) + db.send_create_signal(u'tutorialv2', ['ContentRead']) + + # Adding model 'Extract' + db.create_table(u'tutorialv2_extract', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), + ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + )) + db.send_create_signal(u'tutorialv2', ['Extract']) + + # Adding model 'Validation' + db.create_table(u'tutorialv2_validation', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), + ('version', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('date_proposition', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('comment_authors', self.gf('django.db.models.fields.TextField')()), + ('validator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='author_content_validations', null=True, to=orm['auth.User'])), + ('date_reserve', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('date_validation', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('comment_validator', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('status', self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=10)), + )) + db.send_create_signal(u'tutorialv2', ['Validation']) + + + def backwards(self, orm): + # Deleting model 'Container' + db.delete_table(u'tutorialv2_container') + + # Deleting model 'PublishableContent' + db.delete_table(u'tutorialv2_publishablecontent') + + # Removing M2M table for field authors on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_authors')) + + # Removing M2M table for field subcategory on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_subcategory')) + + # Deleting model 'ContentReaction' + db.delete_table(u'tutorialv2_contentreaction') + + # Deleting model 'ContentRead' + db.delete_table(u'tutorialv2_contentread') + + # Deleting model 'Extract' + db.delete_table(u'tutorialv2_extract') + + # Deleting model 'Validation' + db.delete_table(u'tutorialv2_validation') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.container': { + 'Meta': {'object_name': 'Container'}, + 'compatibility_pk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'conclusion': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'introduction': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'position_in_parent': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.extract': { + 'Meta': {'object_name': 'Extract'}, + 'container': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'position_in_container': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent', '_ormbases': [u'tutorialv2.Container']}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + u'container_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['tutorialv2.Container']", 'unique': 'True', 'primary_key': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'images': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/migrations/__init__.py b/zds/tutorialv2/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cb0ea3b437..1224e01b1a 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -24,6 +24,7 @@ from zds.utils import slugify, get_current_user from zds.utils.models import SubCategory, Licence, Comment from zds.utils.tutorials import get_blob, export_tutorial +from zds.settings import ZDS_APP TYPE_CHOICES = ( @@ -76,6 +77,9 @@ class Meta: null=False, default=1) + #integer key used to represent the tutorial or article old identifier for url compatibility + compatibility_pk = models.IntegerField(null=False, default=0) + def get_children(self): """get this container children""" if self.has_extract(): @@ -95,10 +99,24 @@ def get_last_child_position(self): """Get the relative position of the last child""" return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + def get_tree_depth(self): + """get the tree depth, basically you don't want to have more than 3 levels : + - tutorial/article + - Part + - Chapter + """ + depth = 0 + current = self + while current.parent is not None: + current = current.parent + depth += 1 + + return depth + def add_container(self, container): """add a child container. A container can only be added if no extract had already been added in this container""" - if not self.has_extract(): + if not self.has_extract() and self.get_tree_depth() == ZDS_APP['tutorial']['max_tree_depth']: container.parent = self container.position_in_parent = container.get_last_child_position() + 1 container.save() @@ -106,11 +124,16 @@ def add_container(self, container): raise InvalidOperationError("Can't add a container if this container contains extracts.") def get_phy_slug(self): - """Get the physical path as stored in git file system""" + """gets the slugified title that is used to store the content into the filesystem""" base = "" if self.parent is not None: base = self.parent.get_phy_slug() - return os.path.join(base, self.slug) + + used_pk = self.compatibility_pk + if used_pk == 0: + used_pk = self.pk + + return os.path.join(base,used_pk + '_' + self.slug) def update_children(self): for child in self.get_children(): @@ -129,7 +152,7 @@ def add_extract(self, extract): -class PubliableContent(Container): +class PublishableContent(Container): """A tutorial whatever its size or an aticle.""" class Meta: @@ -145,19 +168,21 @@ class Meta: verbose_name='Sous-Catégorie', blank=True, null=True, db_index=True) + # store the thumbnail for tutorial or article image = models.ForeignKey(Image, verbose_name='Image du tutoriel', blank=True, null=True, on_delete=models.SET_NULL) + # every publishable content has its own gallery to manage images gallery = models.ForeignKey(Gallery, verbose_name='Galerie d\'images', blank=True, null=True, db_index=True) - create_at = models.DateTimeField('Date de création') + creation_date = models.DateTimeField('Date de création') pubdate = models.DateTimeField('Date de publication', blank=True, null=True, db_index=True) - update = models.DateTimeField('Date de mise à jour', + update_date = models.DateTimeField('Date de mise à jour', blank=True, null=True) sha_public = models.CharField('Sha1 de la version publique', @@ -181,7 +206,7 @@ class Meta: null=True, max_length=200) - last_note = models.ForeignKey('Note', blank=True, null=True, + last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', verbose_name='Derniere note') is_locked = models.BooleanField('Est verrouillé', default=False) @@ -190,10 +215,8 @@ class Meta: def __unicode__(self): return self.title - def get_phy_slug(self): - return str(self.pk) + "_" + self.slug - def get_absolute_url(self): + """gets the url to access the tutorial when offline""" return reverse('zds.tutorial.views.view_tutorial', args=[ self.pk, slugify(self.title) ]) @@ -215,11 +238,6 @@ def get_edit_url(self): return reverse('zds.tutorial.views.modify_tutorial') + \ '?tutorial={0}'.format(self.pk) - def get_subcontainers(self): - return Container.objects.all()\ - .filter(tutorial__pk=self.pk)\ - .order_by('position_in_parent') - def in_beta(self): return (self.sha_beta is not None) and (self.sha_beta.strip() != '') @@ -235,14 +253,15 @@ def on_line(self): def is_article(self): return self.type == 'ARTICLE' - def is_tuto(self): + def is_tutorial(self): return self.type == 'TUTO' def get_path(self, relative=False): if relative: return '' else: - return os.path.join(settings.ZDS_APP['tutorial']['repo_path'], self.get_phy_slug()) + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) def get_prod_path(self, sha=None): data = self.load_json_for_public(sha) @@ -422,22 +441,22 @@ def delete_entity_and_tree(self): def save(self, *args, **kwargs): self.slug = slugify(self.title) - super(PubliableContent, self).save(*args, **kwargs) + super(PublishableContent, self).save(*args, **kwargs) def get_note_count(self): """Return the number of notes in the tutorial.""" - return Note.objects.filter(tutorial__pk=self.pk).count() + return ContentReaction.objects.filter(tutorial__pk=self.pk).count() def get_last_note(self): """Gets the last answer in the thread, if any.""" - return Note.objects.all()\ + return ContentReaction.objects.all()\ .filter(tutorial__pk=self.pk)\ .order_by('-pubdate')\ .first() def first_note(self): """Return the first post of a topic, written by topic's author.""" - return Note.objects\ + return ContentReaction.objects\ .filter(tutorial=self)\ .order_by('pubdate')\ .first() @@ -449,7 +468,7 @@ def last_read_note(self): .select_related()\ .filter(tutorial=self, user=get_current_user())\ .latest('note__pubdate').note - except Note.DoesNotExist: + except ContentReaction.DoesNotExist: return self.first_post() def first_unread_note(self): @@ -459,7 +478,7 @@ def first_unread_note(self): .filter(tutorial=self, user=get_current_user())\ .latest('note__pubdate').note - next_note = Note.objects.filter( + next_note = ContentReaction.objects.filter( tutorial__pk=self.pk, pubdate__gt=last_note.pubdate)\ .select_related("author").first() @@ -481,7 +500,7 @@ def antispam(self, user=None): if user is None: user = get_current_user() - last_user_notes = Note.objects\ + last_user_notes = ContentReaction.objects\ .filter(tutorial=self)\ .filter(author=user.pk)\ .order_by('-position') @@ -526,14 +545,15 @@ def have_epub(self): ".epub")) -class Note(Comment): +class ContentReaction(Comment): """A comment written by an user about a Publiable content he just read.""" class Meta: - verbose_name = 'note sur un tutoriel' - verbose_name_plural = 'notes sur un tutoriel' + verbose_name = 'note sur un contenu' + verbose_name_plural = 'notes sur un contenu' - related_content = models.ForeignKey(PubliableContent, verbose_name='Tutoriel', db_index=True) + related_content = models.ForeignKey(PublishableContent, verbose_name='Contenu', + related_name="related_content_note", db_index=True) def __unicode__(self): """Textual form of a post.""" @@ -557,12 +577,12 @@ class ContentRead(models.Model): """ class Meta: - verbose_name = 'Tutoriel lu' - verbose_name_plural = 'Tutoriels lus' + verbose_name = 'Contenu lu' + verbose_name_plural = 'Contenu lus' - tutorial = models.ForeignKey(PubliableContent, db_index=True) - note = models.ForeignKey(Note, db_index=True) - user = models.ForeignKey(User, related_name='tuto_notes_read', db_index=True) + tutorial = models.ForeignKey(PublishableContent, db_index=True) + note = models.ForeignKey(ContentReaction, db_index=True) + user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) def __unicode__(self): return u''.format(self.tutorial, @@ -716,7 +736,7 @@ class Meta: verbose_name = 'Validation' verbose_name_plural = 'Validations' - tutorial = models.ForeignKey(PubliableContent, null=True, blank=True, + tutorial = models.ForeignKey(PublishableContent, null=True, blank=True, verbose_name='Tutoriel proposé', db_index=True) version = models.CharField('Sha1 de la version', blank=True, null=True, max_length=80, db_index=True) @@ -724,7 +744,7 @@ class Meta: comment_authors = models.TextField('Commentaire de l\'auteur') validator = models.ForeignKey(User, verbose_name='Validateur', - related_name='author_validations', + related_name='author_content_validations', blank=True, null=True, db_index=True) date_reserve = models.DateTimeField('Date de réservation', blank=True, null=True) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 365abcf760..56e6168989 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -1,13 +1,13 @@ # coding: utf-8 -from zds.tutorialv2.models import PubliableContent, ContentRead +from zds.tutorialv2.models import PublishableContent, ContentRead from zds import settings from zds.utils import get_current_user def get_last_tutorials(): """get the last issued tutorials""" n = settings.ZDS_APP['tutorial']['home_number'] - tutorials = PubliableContent.objects.all()\ + tutorials = PublishableContent.objects.all()\ .exclude(type="ARTICLE")\ .exclude(sha_public__isnull=True)\ .exclude(sha_public__exact='')\ @@ -19,7 +19,7 @@ def get_last_tutorials(): def get_last_articles(): """get the last issued articles""" n = settings.ZDS_APP['tutorial']['home_number'] - articles = PubliableContent.objects.all()\ + articles = PublishableContent.objects.all()\ .exclude(type="TUTO")\ .exclude(sha_public__isnull=True)\ .exclude(sha_public__exact='')\ @@ -29,7 +29,7 @@ def get_last_articles(): def never_read(tutorial, user=None): - """Check if a topic has been read by an user since it last post was + """Check if the tutorial note feed has been read by an user since its last post was added.""" if user is None: user = get_current_user() @@ -40,7 +40,7 @@ def never_read(tutorial, user=None): def mark_read(tutorial): - """Mark a tutorial as read for the user.""" + """Mark the last tutorial note as read for the user.""" if tutorial.last_note is not None: ContentRead.objects.filter( tutorial=tutorial, diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index efd77f81f0..373794a27e 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -43,7 +43,7 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ - mark_read, Note, HelpWriting + mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile @@ -997,7 +997,7 @@ def view_tutorial_online(request, tutorial_pk, tutorial_slug): # Find all notes of the tutorial. - notes = Note.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() + notes = ContentReaction.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() # Retrieve pk of the last note. If there aren't notes for the tutorial, we # initialize this last note at 0. @@ -3284,7 +3284,7 @@ def answer(request): # Retrieve 3 last notes of the current tutorial. - notes = Note.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] + notes = ContentReaction.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] # If there is a last notes for the tutorial, we save his pk. Otherwise, we # save 0. @@ -3294,7 +3294,7 @@ def answer(request): last_note_pk = tutorial.last_note.pk # Retrieve lasts notes of the current tutorial. - notes = Note.objects.filter(tutorial=tutorial) \ + notes = ContentReaction.objects.filter(tutorial=tutorial) \ .prefetch_related() \ .order_by("-pubdate")[:settings.ZDS_APP['forum']['posts_per_page']] @@ -3327,7 +3327,7 @@ def answer(request): form = NoteForm(tutorial, request.user, request.POST) if form.is_valid(): data = form.data - note = Note() + note = ContentReaction() note.related_content = tutorial note.author = request.user note.text = data["text"] @@ -3356,7 +3356,7 @@ def answer(request): if "cite" in request.GET: note_cite_pk = request.GET["cite"] - note_cite = Note.objects.get(pk=note_cite_pk) + note_cite = ContentReaction.objects.get(pk=note_cite_pk) if not note_cite.is_visible: raise PermissionDenied @@ -3390,7 +3390,7 @@ def solve_alert(request): raise PermissionDenied alert = get_object_or_404(Alert, pk=request.POST["alert_pk"]) - note = Note.objects.get(pk=alert.comment.id) + note = ContentReaction.objects.get(pk=alert.comment.id) if "text" in request.POST and request.POST["text"] != "": bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) @@ -3443,7 +3443,7 @@ def edit_note(request): note_pk = request.GET["message"] except KeyError: raise Http404 - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) g_tutorial = None if note.position >= 1: g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) @@ -3526,7 +3526,7 @@ def like_note(request): except KeyError: raise Http404 resp = {} - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) user = request.user if note.author.pk != request.user.pk: @@ -3571,7 +3571,7 @@ def dislike_note(request): except KeyError: raise Http404 resp = {} - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) user = request.user if note.author.pk != request.user.pk: From 441d4eb115e6c20738f484edcfac537a4ddb39c2 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 25 Dec 2014 21:33:21 +0100 Subject: [PATCH 003/887] Petit refactoring - Docstring - PEP8 - Des todos pour en discuter --- zds/tutorialv2/__init__.py | 1 - zds/tutorialv2/models.py | 506 ++++++++++++++++++++++++------------- zds/tutorialv2/utils.py | 37 ++- zds/tutorialv2/views.py | 10 +- 4 files changed, 362 insertions(+), 192 deletions(-) diff --git a/zds/tutorialv2/__init__.py b/zds/tutorialv2/__init__.py index 8b13789179..e69de29bb2 100644 --- a/zds/tutorialv2/__init__.py +++ b/zds/tutorialv2/__init__.py @@ -1 +0,0 @@ - diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 1224e01b1a..afb9289fd3 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -28,7 +28,7 @@ TYPE_CHOICES = ( - ('TUTO', 'Tutoriel'), + ('TUTORIAL', 'Tutoriel'), ('ARTICLE', 'Article'), ) @@ -45,8 +45,16 @@ class InvalidOperationError(RuntimeError): class Container(models.Model): + """ + A container, which can have sub-Containers or Extracts. + + A Container has a title, a introduction and a conclusion, a parent (which can be None) and a position into this + parent (which is 1 by default). + + It has also a tree depth. - """A container (tuto/article, part, chapter).""" + There is a `compatibility_pk` for compatibility with older versions. + """ class Meta: verbose_name = 'Container' verbose_name_plural = 'Containers' @@ -68,63 +76,85 @@ class Meta: max_length=200) parent = models.ForeignKey("self", - verbose_name='Conteneur parent', - blank=True, null=True, - on_delete=models.SET_NULL) + verbose_name='Conteneur parent', + blank=True, null=True, + on_delete=models.SET_NULL) position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', blank=False, null=False, default=1) - #integer key used to represent the tutorial or article old identifier for url compatibility + # TODO: thumbnails ? + + # integer key used to represent the tutorial or article old identifier for url compatibility compatibility_pk = models.IntegerField(null=False, default=0) def get_children(self): - """get this container children""" + """ + :return: children of this Container, ordered by position + """ if self.has_extract(): return Extract.objects.filter(container_pk=self.pk) - return Container.objects.filter(parent_pk=self.pk) + return Container.objects.filter(parent_pk=self.pk).order_by('position_in_parent') def has_extract(self): - """Check this container has content extracts""" - return Extract.objects.filter(chapter=self).count() > 0 + """ + :return: `True` if the Container has extract as children, `False` otherwise. + """ + return Extract.objects.filter(parent=self).count() > 0 - def has_sub_part(self): - """Check this container has a sub container""" - return Container.objects.filter(parent=self).count() > 0 + def has_sub_container(self): + """ + :return: `True` if the Container has other Containers as children, `False` otherwise. + """ + return Container.objects.filter(container=self).count() > 0 def get_last_child_position(self): - """Get the relative position of the last child""" - return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + """ + :return: the relative position of the last child + """ + return Container.objects.filter(parent=self).count() + Extract.objects.filter(container=self).count() def get_tree_depth(self): - """get the tree depth, basically you don't want to have more than 3 levels : - - tutorial/article - - Part - - Chapter + """ + Tree depth is no more than 2, because there is 3 levels for Containers : + - PublishableContent (0), + - Part (1), + - Chapter (2) + Note that `'max_tree_depth` is `2` to ensure that there is no more than 3 levels + :return: Tree depth """ depth = 0 current = self while current.parent is not None: current = current.parent depth += 1 - return depth def add_container(self, container): - """add a child container. A container can only be added if - no extract had already been added in this container""" - if not self.has_extract() and self.get_tree_depth() == ZDS_APP['tutorial']['max_tree_depth']: - container.parent = self - container.position_in_parent = container.get_last_child_position() + 1 - container.save() + """ + Add a child Container, but only if no extract were previously added and tree depth is < 2. + :param container: the new Container + """ + if not self.has_extract(): + if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + container.parent = self + container.position_in_parent = container.get_last_child_position() + 1 + container.save() + else: + raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") + # TODO: limitation if article ? def get_phy_slug(self): - """gets the slugified title that is used to store the content into the filesystem""" + """ + The slugified title is used to store physically the information in filesystem. + A "compatibility pk" can be used instead of real pk to ensure compatibility with previous versions. + :return: the slugified title + """ base = "" if self.parent is not None: base = self.parent.get_phy_slug() @@ -133,9 +163,12 @@ def get_phy_slug(self): if used_pk == 0: used_pk = self.pk - return os.path.join(base,used_pk + '_' + self.slug) + return os.path.join(base, str(used_pk) + '_' + self.slug) def update_children(self): + """ + Update all children of the container. + """ for child in self.get_children(): if child is Container: self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") @@ -143,22 +176,46 @@ def update_children(self): self.save() child.update_children() else: - child.text = child.get_path(relative=True) + child.text = child.get_path(relative=True) child.save() def add_extract(self, extract): - if not self.has_sub_part(): - extract.chapter = self - + """ + Add a child container, but only if no container were previously added + :param extract: the new Extract + """ + if not self.has_sub_container(): + extract.container = self + extract.save() + # TODO: + # - rewrite save() + # - get_absolute_url_*() stuffs, get_path(), get_prod_path() + # - __unicode__() + # - get_introduction_*(), get_conclusion_*() + # - a `top_parent()` function to access directly to the parent PublishableContent and avoid the + # `container.parent.parent.parent` stuff ? + # - a nice `delete_entity_and_tree()` function ? (which also remove the file) + # - the `maj_repo_*()` stuffs should probably be into the model ? class PublishableContent(Container): + """ + A tutorial whatever its size or an article. - """A tutorial whatever its size or an aticle.""" + A PublishableContent is a tree depth 0 Container (no parent) with additional information, such as + - authors, description, source (if the content comes from another website), subcategory and licence ; + - Thumbnail and gallery ; + - Creation, publication and update date ; + - Public, beta, validation and draft sha, for versioning ; + - Comment support ; + - Type, which is either "ARTICLE" or "TUTORIAL" + + These are two repositories : draft and online. + """ class Meta: verbose_name = 'Tutoriel' verbose_name_plural = 'Tutoriels' - + # TODO: "Contenu" ? description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) @@ -183,7 +240,7 @@ class Meta: pubdate = models.DateTimeField('Date de publication', blank=True, null=True, db_index=True) update_date = models.DateTimeField('Date de mise à jour', - blank=True, null=True) + blank=True, null=True) sha_public = models.CharField('Sha1 de la version publique', blank=True, null=True, max_length=80, db_index=True) @@ -205,6 +262,7 @@ class Meta: blank=True, null=True, max_length=200) + # TODO: rename this field ? (`relative_image_path` ?) last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', @@ -216,70 +274,120 @@ def __unicode__(self): return self.title def get_absolute_url(self): - """gets the url to access the tutorial when offline""" - return reverse('zds.tutorial.views.view_tutorial', args=[ - self.pk, slugify(self.title) - ]) + """ + :return: the url to access the tutorial when offline + """ + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) def get_absolute_url_online(self): - return reverse('zds.tutorial.views.view_tutorial_online', args=[ - self.pk, slugify(self.title) - ]) + """ + :return: the url to access the tutorial when online + """ + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ if self.sha_beta is not None: - return reverse('zds.tutorial.views.view_tutorial', args=[ - self.pk, slugify(self.title) - ]) + '?version=' + self.sha_beta + return self.get_absolute_url() + '?version=' + self.sha_beta else: return self.get_absolute_url() def get_edit_url(self): - return reverse('zds.tutorial.views.modify_tutorial') + \ - '?tutorial={0}'.format(self.pk) + """ + :return: the url to edit the tutorial + """ + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) def in_beta(self): + """ + A tutorial is not in beta if sha_beta is `None` or empty + :return: `True` if the tutorial is in beta, `False` otherwise + """ return (self.sha_beta is not None) and (self.sha_beta.strip() != '') def in_validation(self): + """ + A tutorial is not in validation if sha_validation is `None` or empty + :return: `True` if the tutorial is in validation, `False` otherwise + """ return (self.sha_validation is not None) and (self.sha_validation.strip() != '') def in_drafting(self): + """ + A tutorial is not in draft if sha_draft is `None` or empty + :return: `True` if the tutorial is in draft, `False` otherwise + """ + # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') def on_line(self): + """ + A tutorial is not in on line if sha_public is `None` or empty + :return: `True` if the tutorial is on line, `False` otherwise + """ + # TODO: for the logic with previous method, why not `in_public()` ? return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_article(self): + """ + :return: `True` if article, `False` otherwise + """ return self.type == 'ARTICLE' def is_tutorial(self): - return self.type == 'TUTO' + """ + :return: `True` if tutorial, `False` otherwise + """ + return self.type == 'TUTORIAL' + + def get_phy_slug(self): + """ + :return: the physical slug, used to represent data in filesystem + """ + return str(self.pk) + "_" + self.slug def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ if relative: return '' else: # get the full path (with tutorial/article before it) return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + # TODO: versionning ?!? def get_prod_path(self, sha=None): + """ + Get the physical path to the public version of the content + :param sha: version of the content, if `None`, public version is used + :return: physical path + """ data = self.load_json_for_public(sha) return os.path.join( - settings.ZDS_APP['tutorial']['repo_public_path'], + settings.ZDS_APP[self.type.lower()]['repo_public_path'], str(self.pk) + '_' + slugify(data['title'])) def load_dic(self, mandata, sha=None): - '''fill mandata with informations from database model''' + """ + Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. + :param mandata: a dictionary from JSON file + :param sha: current version, used to fill the `is_*` fields by comparison with the corresponding `sha_*` + """ + # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', - 'have_epub', 'get_path', 'in_beta', 'in_validation', 'on_line' + 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'get_path', 'in_beta', + 'in_validation', 'on_line' ] attrs = [ - 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', - 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' + 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', + 'sha_validation', 'sha_public' ] # load functions and attributs in tree @@ -291,37 +399,37 @@ def load_dic(self, mandata, sha=None): # general information mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha - mandata['is_validation'] = self.in_validation() \ - and self.sha_validation == sha + mandata['is_validation'] = self.in_validation() and self.sha_validation == sha mandata['is_on_line'] = self.on_line() and self.sha_public == sha # url: - mandata['get_absolute_url'] = reverse( - 'zds.tutorial.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) if self.in_beta(): mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', + 'zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']] ) + '?version=' + self.sha_beta else: mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', + 'zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']] ) mandata['get_absolute_url_online'] = reverse( - 'zds.tutorial.views.view_tutorial_online', + 'zds.tutorialv2.views.view_tutorial_online', args=[self.pk, mandata['slug']] ) def load_introduction_and_conclusion(self, mandata, sha=None, public=False): - '''Explicitly load introduction and conclusion to avoid useless disk - access in load_dic() - ''' + """ + Explicitly load introduction and conclusion to avoid useless disk access in `load_dic()` + :param mandata: dictionary from JSON file + :param sha: version + :param public: if `True`, get introduction and conclusion from the public version instead of the draft one + (`sha` is not used in this case) + """ if public: mandata['get_introduction_online'] = self.get_introduction_online() @@ -331,17 +439,29 @@ def load_introduction_and_conclusion(self, mandata, sha=None, public=False): mandata['get_conclusion'] = self.get_conclusion(sha) def load_json_for_public(self, sha=None): + """ + Fetch the public version of the JSON file for this content. + :param sha: version + :return: a dictionary containing the structure of the JSON file. + """ if sha is None: sha = self.sha_public - repo = Repo(self.get_path()) + repo = Repo(self.get_path()) # should be `get_prod_path()` !?! mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') data = json_reader.loads(mantuto) if 'licence' in data: data['licence'] = Licence.objects.filter(code=data['licence']).first() return data + # TODO: redundant with next function def load_json(self, path=None, online=False): - + """ + Fetch a specific version of the JSON file for this content. + :param sha: version + :param path: path to the repository. If None, the `get_[prod_]path()` function is used + :param public: if `True`fetch the public version instead of the private one + :return: a dictionary containing the structure of the JSON file. + """ if path is None: if online: man_path = os.path.join(self.get_prod_path(), 'manifest.json') @@ -359,6 +479,10 @@ def load_json(self, path=None, online=False): return data def dump_json(self, path=None): + """ + Write the JSON into file + :param path: path to the file. If `None`, use default path. + """ if path is None: man_path = os.path.join(self.get_path(), 'manifest.json') else: @@ -371,6 +495,11 @@ def dump_json(self, path=None): json_data.close() def get_introduction(self, sha=None): + """ + Get the introduction content of a specific version + :param sha: version, if `None`, use draft one + :return: the introduction (as a string) + """ # find hash code if sha is None: sha = self.sha_draft @@ -385,7 +514,10 @@ def get_introduction(self, sha=None): return get_blob(repo.commit(sha).tree, path_content_intro) def get_introduction_online(self): - """get the introduction content for a particular version if sha is not None""" + """ + Get introduction content of the public version + :return: the introduction (as a string) + """ if self.on_line(): intro = open( os.path.join( @@ -399,7 +531,11 @@ def get_introduction_online(self): return intro_contenu.decode('utf-8') def get_conclusion(self, sha=None): - """get the conclusion content for a particular version if sha is not None""" + """ + Get the conclusion content of a specific version + :param sha: version, if `None`, use draft one + :return: the conclusion (as a string) + """ # find hash code if sha is None: sha = self.sha_draft @@ -414,7 +550,10 @@ def get_conclusion(self, sha=None): return get_blob(repo.commit(sha).tree, path_content_ccl) def get_conclusion_online(self): - """get the conclusion content for the online version of the current publiable content""" + """ + Get conclusion content of the public version + :return: the conclusion (as a string) + """ if self.on_line(): conclusion = open( os.path.join( @@ -428,7 +567,9 @@ def get_conclusion_online(self): return conlusion_content.decode('utf-8') def delete_entity_and_tree(self): - """deletes the entity and its filesystem counterpart""" + """ + Delete the entities and their filesystem counterparts + """ shutil.rmtree(self.get_path(), 0) Validation.objects.filter(tutorial=self).delete() @@ -437,6 +578,7 @@ def delete_entity_and_tree(self): if self.on_line(): shutil.rmtree(self.get_prod_path()) self.delete() + # TODO: should use the "git" version of `delete()` !!! def save(self, *args, **kwargs): self.slug = slugify(self.title) @@ -444,25 +586,33 @@ def save(self, *args, **kwargs): super(PublishableContent, self).save(*args, **kwargs) def get_note_count(self): - """Return the number of notes in the tutorial.""" + """ + :return : umber of notes in the tutorial. + """ return ContentReaction.objects.filter(tutorial__pk=self.pk).count() def get_last_note(self): - """Gets the last answer in the thread, if any.""" + """ + :return: the last answer in the thread, if any. + """ return ContentReaction.objects.all()\ .filter(tutorial__pk=self.pk)\ .order_by('-pubdate')\ .first() def first_note(self): - """Return the first post of a topic, written by topic's author.""" + """ + :return: the first post of a topic, written by topic's author, if any. + """ return ContentReaction.objects\ .filter(tutorial=self)\ .order_by('pubdate')\ .first() def last_read_note(self): - """Return the last post the user has read.""" + """ + :return: the last post the user has read. + """ try: return ContentRead.objects\ .select_related()\ @@ -472,7 +622,9 @@ def last_read_note(self): return self.first_post() def first_unread_note(self): - """Return the first note the user has unread.""" + """ + :return: Return the first note the user has unread. + """ try: last_note = ContentRead.objects\ .filter(tutorial=self, user=get_current_user())\ @@ -482,20 +634,15 @@ def first_unread_note(self): tutorial__pk=self.pk, pubdate__gt=last_note.pubdate)\ .select_related("author").first() - return next_note - except: + except: # TODO: `except:` is bad. return self.first_note() def antispam(self, user=None): - """Check if the user is allowed to post in an tutorial according to the - SPAM_LIMIT_SECONDS value. - - If user shouldn't be able to note, then antispam is activated - and this method returns True. Otherwise time elapsed between - user's last note and now is enough, and the method will return - False. - + """ + Check if the user is allowed to post in an tutorial according to the SPAM_LIMIT_SECONDS value. + :param user: the user to check antispam. If `None`, current user is used. + :return: `True` if the user is not able to note (the elapsed time is not enough), `False` otherwise. """ if user is None: user = get_current_user() @@ -513,41 +660,47 @@ def antispam(self, user=None): return False def change_type(self, new_type): - """Allow someone to change the content type, basicaly from tutorial to article""" + """ + Allow someone to change the content type, basically from tutorial to article + :param new_type: the new type, either `"ARTICLE"` or `"TUTORIAL"` + """ if new_type not in TYPE_CHOICES: raise ValueError("This type of content does not exist") - self.type = new_type - def have_markdown(self): - """Check the markdown zip archive is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".md")) + """ + Check if the markdown zip archive is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".md")) def have_html(self): - """Check the html version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".html")) + """ + Check if the html version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".html")) def have_pdf(self): - """Check the pdf version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".pdf")) + """ + Check if the pdf version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".pdf")) def have_epub(self): - """Check the standard epub version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".epub")) + """ + Check if the standard epub version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) class ContentReaction(Comment): - - """A comment written by an user about a Publiable content he just read.""" + """ + A comment written by any user about a PublishableContent he just read. + """ class Meta: verbose_name = 'note sur un contenu' verbose_name_plural = 'notes sur un contenu' @@ -556,43 +709,40 @@ class Meta: related_name="related_content_note", db_index=True) def __unicode__(self): - """Textual form of a post.""" return u''.format(self.related_content, self.pk) def get_absolute_url(self): + """ + :return: the url of the comment + """ page = int(ceil(float(self.position) / settings.ZDS_APP['forum']['posts_per_page'])) - - return '{0}?page={1}#p{2}'.format( - self.related_content.get_absolute_url_online(), - page, - self.pk) + return '{0}?page={1}#p{2}'.format(self.related_content.get_absolute_url_online(), page, self.pk) class ContentRead(models.Model): + """ + Small model which keeps track of the user viewing tutorials. - """Small model which keeps track of the user viewing tutorials. - - It remembers the topic he looked and what was the last Note at this - time. - + It remembers the PublishableContent he looked and what was the last Note at this time. """ class Meta: verbose_name = 'Contenu lu' verbose_name_plural = 'Contenu lus' - tutorial = models.ForeignKey(PublishableContent, db_index=True) + content = models.ForeignKey(PublishableContent, db_index=True) note = models.ForeignKey(ContentReaction, db_index=True) user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) def __unicode__(self): - return u''.format(self.tutorial, - self.user, - self.note.pk) + return u''.format(self.content, self.user, self.note.pk) class Extract(models.Model): + """ + A content extract from a Container. - """A content extract from a chapter.""" + It has a title, a position in the parent container and a text. + """ class Meta: verbose_name = 'Extrait' verbose_name_plural = 'Extraits' @@ -611,64 +761,57 @@ def __unicode__(self): return u''.format(self.title) def get_absolute_url(self): + """ + :return: the url to access the tutorial offline + """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url(), - self.position_in_chapter, + self.position_in_container, slugify(self.title) ) def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_online(), - self.position_in_chapter, + self.position_in_container, slugify(self.title) ) - def get_path(self, relative=False): - if relative: - if self.container.tutorial: - chapter_path = '' - else: - chapter_path = os.path.join( - self.container.part.get_phy_slug(), - self.container.get_phy_slug()) - else: - if self.container.tutorial: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - self.container.tutorial.get_phy_slug()) - else: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - self.container.part.tutorial.get_phy_slug(), - self.container.part.get_phy_slug(), - self.container.get_phy_slug()) + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_beta(), + self.position_in_container, + slugify(self.title) + ) - return os.path.join(chapter_path, str(self.pk) + "_" + slugify(self.title)) + '.md' + def get_phy_slug(self): + """ + :return: the physical slug + """ + return str(self.pk) + '_' + slugify(self.title) - def get_prod_path(self): + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the extract. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + # TODO: versionning ? - if self.container.tutorial: - data = self.container.tutorial.load_json_for_public() - mandata = self.container.tutorial.load_dic(data) - if "chapter" in mandata: - for ext in mandata["chapter"]["extracts"]: - if ext['pk'] == self.pk: - return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], - str(self.container.tutorial.pk) + '_' + slugify(mandata['title']), - str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' - else: - data = self.container.part.tutorial.load_json_for_public() - mandata = self.container.part.tutorial.load_dic(data) - for part in mandata["parts"]: - for chapter in part["chapters"]: - for ext in chapter["extracts"]: - if ext['pk'] == self.pk: - return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], - str(mandata['pk']) + '_' + slugify(mandata['title']), - str(part['pk']) + "_" + slugify(part['title']), - str(chapter['pk']) + "_" + slugify(chapter['title']), - str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' + def get_prod_path(self, sha=None): + """ + Get the physical path to the public version of a specific version of the extract. + :param sha: version of the content, if `None`, `sha_public` is used + :return: physical path + """ + return os.path.join(self.container.get_prod_path(sha), self.get_phy_slug()) + '.md.html' def get_text(self, sha=None): @@ -730,14 +873,15 @@ def get_text_online(self): class Validation(models.Model): - - """Tutorial validation.""" + """ + Content validation. + """ class Meta: verbose_name = 'Validation' verbose_name_plural = 'Validations' - tutorial = models.ForeignKey(PublishableContent, null=True, blank=True, - verbose_name='Tutoriel proposé', db_index=True) + content = models.ForeignKey(PublishableContent, null=True, blank=True, + verbose_name='Contenu proposé', db_index=True) version = models.CharField('Sha1 de la version', blank=True, null=True, max_length=80, db_index=True) date_proposition = models.DateTimeField('Date de proposition', db_index=True) @@ -758,16 +902,32 @@ class Meta: default='PENDING') def __unicode__(self): - return self.tutorial.title + return self.content.title def is_pending(self): + """ + Check if the validation is pending + :return: `True` if status is pending, `False` otherwise + """ return self.status == 'PENDING' def is_pending_valid(self): + """ + Check if the validation is pending (but there is a validator) + :return: `True` if status is pending, `False` otherwise + """ return self.status == 'PENDING_V' def is_accept(self): + """ + Check if the content is accepted + :return: `True` if status is accepted, `False` otherwise + """ return self.status == 'ACCEPT' def is_reject(self): + """ + Check if the content is rejected + :return: `True` if status is rejected, `False` otherwise + """ return self.status == 'REJECT' diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 56e6168989..693b09b1d7 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -4,8 +4,11 @@ from zds import settings from zds.utils import get_current_user + def get_last_tutorials(): - """get the last issued tutorials""" + """ + :return: last issued tutorials + """ n = settings.ZDS_APP['tutorial']['home_number'] tutorials = PublishableContent.objects.all()\ .exclude(type="ARTICLE")\ @@ -17,7 +20,9 @@ def get_last_tutorials(): def get_last_articles(): - """get the last issued articles""" + """ + :return: last issued articles + """ n = settings.ZDS_APP['tutorial']['home_number'] articles = PublishableContent.objects.all()\ .exclude(type="TUTO")\ @@ -28,25 +33,31 @@ def get_last_articles(): return articles -def never_read(tutorial, user=None): - """Check if the tutorial note feed has been read by an user since its last post was - added.""" +def never_read(content, user=None): + """ + Check if a content note feed has been read by an user since its last post was added. + :param content: the content to check + :return: `True` if it is the case, `False` otherwise + """ if user is None: user = get_current_user() return ContentRead.objects\ - .filter(note=tutorial.last_note, tutorial=tutorial, user=user)\ + .filter(note=content.last_note, content=content, user=user)\ .count() == 0 -def mark_read(tutorial): - """Mark the last tutorial note as read for the user.""" - if tutorial.last_note is not None: +def mark_read(content): + """ + Mark the last tutorial note as read for the user. + :param content: the content to mark + """ + if content.last_note is not None: ContentRead.objects.filter( - tutorial=tutorial, + content=content, user=get_current_user()).delete() a = ContentRead( - note=tutorial.last_note, - tutorial=tutorial, + note=content.last_note, + content=content, user=get_current_user()) - a.save() \ No newline at end of file + a.save() diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 373794a27e..b90966b8c6 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -195,7 +195,7 @@ def reservation(request, validation_pk): _(u"Le tutoriel a bien été \ réservé par {0}.").format(request.user.username)) return redirect( - validation.tutorial.get_absolute_url() + + validation.content.get_absolute_url() + "?version=" + validation.version ) @@ -468,7 +468,7 @@ def ask_validation(request): # We create and save validation object of the tutorial. validation = Validation() - validation.tutorial = tutorial + validation.content = tutorial validation.date_proposition = datetime.now() validation.comment_authors = request.POST["text"] validation.version = request.POST["version"] @@ -491,9 +491,9 @@ def ask_validation(request): False, ) validation.save() - validation.tutorial.source = request.POST["source"] - validation.tutorial.sha_validation = request.POST["version"] - validation.tutorial.save() + validation.content.source = request.POST["source"] + validation.content.sha_validation = request.POST["version"] + validation.content.save() messages.success(request, _(u"Votre demande de validation a été envoyée à l'équipe.")) return redirect(tutorial.get_absolute_url()) From 7718d96e66fa023fc843c0bb42e64a96cb4f67d4 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 11:08:15 +0100 Subject: [PATCH 004/887] =?UTF-8?q?D=C3=A9but=20r=C3=A9facto=20des=20vues.?= =?UTF-8?q?=20Feed=20migr=C3=A9s=20Pagination=20param=C3=A9tr=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 3 +- zds/tutorialv2/feeds.py | 41 +++++++++++++++++++++++-- zds/tutorialv2/urls.py | 9 +++++- zds/tutorialv2/views.py | 67 ++++++++++++++++++++++------------------- 4 files changed, 85 insertions(+), 35 deletions(-) diff --git a/zds/settings.py b/zds/settings.py index 191be8b447..454e4e4e92 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -453,7 +453,8 @@ 'default_license_pk': 7, 'home_number': 5, 'helps_per_page': 20, - 'max_tree_depth': 3 + 'max_tree_depth': 3, + 'content_per_page': 50 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index f2b6a6174e..0c7b10a716 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -5,7 +5,7 @@ from django.utils.feedgenerator import Atom1Feed -from .models import Tutorial +from .models import PublishableContent class LastTutorialsFeedRSS(Feed): @@ -14,7 +14,8 @@ class LastTutorialsFeedRSS(Feed): description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) def items(self): - return Tutorial.objects\ + return PublishableContent.objects\ + .filter(type="TUTO")\ .filter(sha_public__isnull=False)\ .order_by('-pubdate')[:5] @@ -42,3 +43,39 @@ def item_link(self, item): class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description + +class LastArticlesFeedRSS(Feed): + title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) + link = "/articles/" + description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + def items(self): + return PublishableContent.objects\ + .filter(type="ARTICLE")\ + .filter(sha_public__isnull=False)\ + .order_by('-pubdate')[:5] + + def item_title(self, item): + return item.title + + def item_pubdate(self, item): + return item.pubdate + + def item_description(self, item): + return item.description + + def item_author_name(self, item): + authors_list = item.authors.all() + authors = [] + for authors_obj in authors_list: + authors.append(authors_obj.username) + authors = ", ".join(authors) + return authors + + def item_link(self, item): + return item.get_absolute_url_online() + + +class LastTutorialsFeedATOM(LastArticlesFeedRSS): + feed_type = Atom1Feed + subtitle = LastTutorialsFeedRSS.description diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 15e3f3e8d3..97a24e5ce3 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -4,8 +4,15 @@ from . import views from . import feeds +from .views import * urlpatterns = patterns('', + # viewing articles + url(r'^articles/$', ArticleList.as_view(), name="index-article"), + url(r'^articles/flux/rss/$', feeds.LastArticlesFeedRSS(), name='article-feed-rss'), + url(r'^articles/flux/atom/$', feeds.LastArticlesFeedATOM(), name='article-feed-atom'), + + # Viewing url(r'^flux/rss/$', feeds.LastTutorialsFeedRSS(), name='tutorial-feed-rss'), url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), @@ -65,7 +72,7 @@ url(r'^nouveau/extrait/$', 'zds.tutorial.views.add_extract'), - url(r'^$', 'zds.tutorial.views.index'), + url(r'^$', TutorialList.as_view, name='index-tutorial'), url(r'^importer/$', 'zds.tutorial.views.import_tuto'), url(r'^import_local/$', 'zds.tutorial.views.local_import'), diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index b90966b8c6..5ed99a824f 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -45,6 +45,7 @@ from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image +from .models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -60,6 +61,41 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ +from django.views.generic import ListView, DetailView, UpdateView + + +class ArticleList(ListView): + + """Displays the list of published articles.""" + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type="ARTICLE" + template_name = 'article/index.html' + tag = None + + def get_queryset(self): + """filter the content to obtain the list of only articles. If tag parameter is provided, only article + which has this category will be listed.""" + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') + + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + return context + + +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" + + context_object_name = 'tutorials' + type="TUTO" + template_name = 'tutorial/index.html' def render_chapter_form(chapter): @@ -74,37 +110,6 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) -def index(request): - """Display all public tutorials of the website.""" - - # The tag indicate what the category tutorial the user would like to - # display. We can display all subcategories for tutorials. - - try: - tag = get_object_or_404(SubCategory, slug=request.GET["tag"]) - except (KeyError, Http404): - tag = None - if tag is None: - tutorials = \ - Tutorial.objects.filter(sha_public__isnull=False).exclude(sha_public="") \ - .order_by("-pubdate") \ - .all() - else: - # The tag isn't None and exist in the system. We can use it to retrieve - # all tutorials in the subcategory specified. - - tutorials = Tutorial.objects.filter( - sha_public__isnull=False, - subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() - - tuto_versions = [] - for tutorial in tutorials: - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata) - tuto_versions.append(mandata) - return render(request, "tutorial/index.html", {"tutorials": tuto_versions, "tag": tag}) - - # Staff actions. From a3f0035d77df79f86b2e10b31fdea97bf0d3146a Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 26 Dec 2014 13:40:30 +0100 Subject: [PATCH 005/887] Petit refactoring - PEP8 - Docstring - TODO a discuter - Corrections "evidentes" --- zds/tutorialv2/feeds.py | 51 +++++++++++++++++++++++++++++++++------- zds/tutorialv2/models.py | 16 +++++++++---- zds/tutorialv2/views.py | 20 +++++++++------- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index 0c7b10a716..c2cc90b347 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -5,19 +5,27 @@ from django.utils.feedgenerator import Atom1Feed -from .models import PublishableContent +from models import PublishableContent -class LastTutorialsFeedRSS(Feed): - title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) - link = "/tutoriels/" - description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) +class LastContentFeedRSS(Feed): + """ + RSS feed for any type of content. + """ + title = u"Contenu sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers contenus parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + link = "" + content_type = None def items(self): - return PublishableContent.objects\ - .filter(type="TUTO")\ - .filter(sha_public__isnull=False)\ - .order_by('-pubdate')[:5] + """ + :return: The last 5 contents (sorted by publication date). If `self.type` is not `None`, the contents will only + be of this type. + """ + contents = PublishableContent.objects.filter(sha_public__isnull=False) + if self.content_type is not None: + contents.filter(type=self.content_type) + return contents.order_by('-pubdate')[:5] def item_title(self, item): return item.title @@ -40,6 +48,16 @@ def item_link(self, item): return item.get_absolute_url_online() +class LastTutorialsFeedRSS(LastContentFeedRSS): + """ + Redefinition of `LastContentFeedRSS` for tutorials only + """ + content_type = "TUTORIAL" + link = "/tutoriels/" + title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description @@ -79,3 +97,18 @@ def item_link(self, item): class LastTutorialsFeedATOM(LastArticlesFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description + + +class LastArticlesFeedRSS(LastContentFeedRSS): + """ + Redefinition of `LastContentFeedRSS` for articles only + """ + content_type = "ARTICLE" + link = "/articles/" + title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + +class LastArticlesFeedATOM(LastArticlesFeedRSS): + feed_type = Atom1Feed + subtitle = LastArticlesFeedRSS.description diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index afb9289fd3..24544ea3ac 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -59,6 +59,8 @@ class Meta: verbose_name = 'Container' verbose_name_plural = 'Containers' + # TODO: clear all database related information ? + title = models.CharField('Titre', max_length=80) slug = models.SlugField(max_length=80) @@ -92,7 +94,7 @@ class Meta: def get_children(self): """ - :return: children of this Container, ordered by position + :return: children of this container, ordered by position """ if self.has_extract(): return Extract.objects.filter(container_pk=self.pk) @@ -101,13 +103,13 @@ def get_children(self): def has_extract(self): """ - :return: `True` if the Container has extract as children, `False` otherwise. + :return: `True` if the container has extract as children, `False` otherwise. """ return Extract.objects.filter(parent=self).count() > 0 def has_sub_container(self): """ - :return: `True` if the Container has other Containers as children, `False` otherwise. + :return: `True` if the container has other Containers as children, `False` otherwise. """ return Container.objects.filter(container=self).count() > 0 @@ -136,7 +138,7 @@ def get_tree_depth(self): def add_container(self, container): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. - :param container: the new Container + :param container: the new container """ if not self.has_extract(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: @@ -182,7 +184,7 @@ def update_children(self): def add_extract(self, extract): """ Add a child container, but only if no container were previously added - :param extract: the new Extract + :param extract: the new extract """ if not self.has_sub_container(): extract.container = self @@ -270,6 +272,8 @@ class Meta: is_locked = models.BooleanField('Est verrouillé', default=False) js_support = models.BooleanField('Support du Javascript', default=False) + # TODO : split this class in two part (one for the DB object, another one for JSON [versionned] file) ? + def __unicode__(self): return self.title @@ -747,6 +751,8 @@ class Meta: verbose_name = 'Extrait' verbose_name_plural = 'Extraits' + # TODO: clear all database related information ? + title = models.CharField('Titre', max_length=80) container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) position_in_container = models.IntegerField('Position dans le parent', db_index=True) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 5ed99a824f..52e5f1e770 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -45,7 +45,7 @@ from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image -from .models import PublishableContent +from models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -61,21 +61,25 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView, UpdateView +from django.views.generic import ListView, DetailView# , UpdateView class ArticleList(ListView): - - """Displays the list of published articles.""" + """ + Displays the list of published articles. + """ context_object_name = 'articles' paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type="ARTICLE" + type = "ARTICLE" template_name = 'article/index.html' tag = None def get_queryset(self): - """filter the content to obtain the list of only articles. If tag parameter is provided, only article - which has this category will be listed.""" + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ if self.request.GET.get('tag') is not None: self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ @@ -94,7 +98,7 @@ class TutorialList(ArticleList): """Displays the list of published tutorials.""" context_object_name = 'tutorials' - type="TUTO" + type = "TUTORIAL" template_name = 'tutorial/index.html' From d997cb013ce42244fcca646907d9e9161cff4d54 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 15:24:51 +0100 Subject: [PATCH 006/887] travail sur l'affichage DetailView --- zds/tutorialv2/views.py | 121 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 52e5f1e770..bc6ae57810 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,8 +42,8 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ - mark_read, ContentReaction, HelpWriting +from models import PublishableContent, Container, Extract, Validation +from utils import never_read from zds.gallery.models import Gallery, UserGallery, Image from models import PublishableContent from zds.member.decorator import can_write_and_read_now @@ -208,6 +208,123 @@ def reservation(request, validation_pk): "?version=" + validation.version ) +class DisplayContent(DetailView): + model = PublishableContent + type = "TUTO" + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p + + cpt_c = 1 + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary + chapter["slug"] = slugify(chapter["title"]) + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + self.compatibility_chapter(content, repo, sha, chapter) + cpt_c += 1 + + def compatibility_chapter(self,content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = get_blob(repo.commit(sha).tree, + "introduction.md") + dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" + ) + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt += 1 + + + def get_object(self): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + + def get_context_data(self, **kwargs): + """Show the given offline tutorial if exists.""" + + context = super(DisplayContent, self).get_context_data(**kwargs) + content = context[self.context_object_name] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the content. + + try: + sha = self.request.GET.get("version") + except KeyError: + sha = content.sha_draft + + # check that if we ask for beta, we also ask for the sha version + is_beta = (sha == content.sha_beta and content.in_beta()) + + # Only authors of the tutorial and staff can view tutorial in offline. + + if self.request.user not in content.authors.all() and not is_beta: + # if we are not author of this content or if we did not ask for beta + # the only members that can display and modify the tutorial are validators + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + + # Find the good manifest file + + repo = Repo(content.get_path()) + + # Load the tutorial. + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + content.load_dic(mandata, sha) + content.load_introduction_and_conclusion(mandata, sha) + children_tree = {} + + if 'chapter' in mandata: + # compatibility with old "Mini Tuto" + self.compatibility_chapter(content, repo, sha, mandata["chapter"]) + children_tree = mandata['chapter'] + elif 'parts' in mandata: + # compatibility with old "big tuto". + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + self.compatibility_parts(content, repo, sha, part, cpt_p) + cpt_p += 1 + children_tree = parts + validation = Validation.objects.filter(tutorial__pk=content.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": content.js_support}) + + if content.source: + form_ask_validation = AskValidationForm(initial={"source": content.source}) + form_valid = ValidForm(initial={"source": content.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + if content.js_support: + is_js = "js" + else: + is_js = "" + context["tutorial"] = mandata # TODO : change to "content" + context["children"] = children_tree + context["version"] = sha + context["validation"] = validation + context["formAskValidation"] = form_ask_validation + context["formJs"] = form_js + context["formValid"] = form_valid + context["formReject"] = form_reject, + context["is_js"] = is_js + + return context + @login_required def diff(request, tutorial_pk, tutorial_slug): From db5ae9541a70f27bfccad243da8c71efd4b0ae89 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 15:29:34 +0100 Subject: [PATCH 007/887] merge conflict --- zds/tutorialv2/views.py | 195 ++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index bc6ae57810..1d9f7227ff 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -114,103 +114,9 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) -# Staff actions. - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -def list_validation(request): - """Display tutorials list in validation.""" - - # Retrieve type of the validation. Default value is all validations. - - try: - type = request.GET["type"] - except KeyError: - type = None - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - - # Orphan validation. There aren't validator attached to the validations. - - if type == "orphan": - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=True, - status="PENDING").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=True, - status="PENDING", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - elif type == "reserved": - - # Reserved validation. There are a validator attached to the - # validations. - - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=False, - status="PENDING_V").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=False, - status="PENDING_V", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - else: - - # Default, we display all validations. - - if subcategory is None: - validations = Validation.objects.filter( - Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() - else: - validations = Validation.objects.filter(Q(status="PENDING") - | Q(status="PENDING_V" - )).filter(tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition")\ - .all() - return render(request, "tutorial/validation/index.html", - {"validations": validations}) - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -@require_POST -def reservation(request, validation_pk): - """Display tutorials list in validation.""" - - validation = get_object_or_404(Validation, pk=validation_pk) - if validation.validator: - validation.validator = None - validation.date_reserve = None - validation.status = "PENDING" - validation.save() - messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) - return redirect(reverse("zds.tutorial.views.list_validation")) - else: - validation.validator = request.user - validation.date_reserve = datetime.now() - validation.status = "PENDING_V" - validation.save() - messages.info(request, - _(u"Le tutoriel a bien été \ - réservé par {0}.").format(request.user.username)) - return redirect( - validation.content.get_absolute_url() + - "?version=" + validation.version - ) - class DisplayContent(DetailView): model = PublishableContent - type = "TUTO" + type = "TUTORIAL" def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content @@ -326,6 +232,105 @@ def get_context_data(self, **kwargs): return context +class DisplayArticle(DisplayContent): + type = "Article" + + +# Staff actions. + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +def list_validation(request): + """Display tutorials list in validation.""" + + # Retrieve type of the validation. Default value is all validations. + + try: + type = request.GET["type"] + except KeyError: + type = None + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + + # Orphan validation. There aren't validator attached to the validations. + + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": + + # Reserved validation. There are a validator attached to the + # validations. + + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + else: + + # Default, we display all validations. + + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" + + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.content.get_absolute_url() + + "?version=" + validation.version + ) + + @login_required def diff(request, tutorial_pk, tutorial_slug): try: From 89fe4f37e6b22a18aa8fe4836990cc5e5e3418f7 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 16:05:34 +0100 Subject: [PATCH 008/887] =?UTF-8?q?rend=20param=C3=A9trable=20la=20longeur?= =?UTF-8?q?=20des=20feeds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 3 ++- zds/tutorialv2/feeds.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zds/settings.py b/zds/settings.py index 454e4e4e92..90f40d7651 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -454,7 +454,8 @@ 'home_number': 5, 'helps_per_page': 20, 'max_tree_depth': 3, - 'content_per_page': 50 + 'content_per_page': 50, + 'feed_length': 5 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index c2cc90b347..c5b48eb2c1 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -6,7 +6,7 @@ from django.utils.feedgenerator import Atom1Feed from models import PublishableContent - +from zds.settings import ZDS_APP class LastContentFeedRSS(Feed): """ @@ -19,13 +19,13 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last 5 contents (sorted by publication date). If `self.type` is not `None`, the contents will only + :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: contents.filter(type=self.content_type) - return contents.order_by('-pubdate')[:5] + return contents.order_by('-pubdate')[:ZDS_APP['tutorial']['feed_length']] def item_title(self, item): return item.title From 8e72bd81362227c1abfa85b3e2f6923bb5f08e72 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 20:56:23 +0100 Subject: [PATCH 009/887] Migration online_view --- zds/tutorialv2/models.py | 35 +-- zds/tutorialv2/views.py | 450 ++++++++++++++------------------------- 2 files changed, 159 insertions(+), 326 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 24544ea3ac..385fbfefee 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -326,7 +326,7 @@ def in_drafting(self): # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') - def on_line(self): + def is_online(self): """ A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise @@ -404,7 +404,7 @@ def load_dic(self, mandata, sha=None): mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.on_line() and self.sha_public == sha + mandata['is_on_line'] = self.is_online() and self.sha_public == sha # url: mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) @@ -456,31 +456,6 @@ def load_json_for_public(self, sha=None): if 'licence' in data: data['licence'] = Licence.objects.filter(code=data['licence']).first() return data - # TODO: redundant with next function - - def load_json(self, path=None, online=False): - """ - Fetch a specific version of the JSON file for this content. - :param sha: version - :param path: path to the repository. If None, the `get_[prod_]path()` function is used - :param public: if `True`fetch the public version instead of the private one - :return: a dictionary containing the structure of the JSON file. - """ - if path is None: - if online: - man_path = os.path.join(self.get_prod_path(), 'manifest.json') - else: - man_path = os.path.join(self.get_path(), 'manifest.json') - else: - man_path = path - - if os.path.isfile(man_path): - json_data = open(man_path) - data = json_reader.load(json_data) - json_data.close() - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data def dump_json(self, path=None): """ @@ -522,7 +497,7 @@ def get_introduction_online(self): Get introduction content of the public version :return: the introduction (as a string) """ - if self.on_line(): + if self.is_online(): intro = open( os.path.join( self.get_prod_path(), @@ -558,7 +533,7 @@ def get_conclusion_online(self): Get conclusion content of the public version :return: the conclusion (as a string) """ - if self.on_line(): + if self.is_online(): conclusion = open( os.path.join( self.get_prod_path(), @@ -579,7 +554,7 @@ def delete_entity_and_tree(self): if self.gallery is not None: self.gallery.delete() - if self.on_line(): + if self.is_online(): shutil.rmtree(self.get_prod_path()) self.delete() # TODO: should use the "git" version of `delete()` !!! diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 1d9f7227ff..de4cfd43d2 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,10 +42,9 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation -from utils import never_read +from models import PublishableContent, Container, Extract, Validation, ContentRead, ContentReaction +from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image -from models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -115,8 +114,12 @@ def render_chapter_form(chapter): class DisplayContent(DetailView): + """Base class that can show any content in any state, by default it shows offline tutorials""" + model = PublishableContent + template_name = 'tutorial/view.html' type = "TUTORIAL" + is_public = False def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content @@ -149,12 +152,33 @@ def compatibility_chapter(self,content, repo, sha, dictionary): ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt += 1 + def get_forms(self, context, content): + """get all the auxiliary forms about validation, js fiddle...""" + validation = Validation.objects.filter(tutorial__pk=content.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": content.js_support}) + + if content.source: + form_ask_validation = AskValidationForm(initial={"source": content.source}) + form_valid = ValidForm(initial={"source": content.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + context["validation"] = validation + context["formAskValidation"] = form_ask_validation + context["formJs"] = form_js + context["formValid"] = form_valid + context["formReject"] = form_reject, + def get_object(self): return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) def get_context_data(self, **kwargs): - """Show the given offline tutorial if exists.""" + """Show the given tutorial if exists.""" context = super(DisplayContent, self).get_context_data(**kwargs) content = context[self.context_object_name] @@ -164,30 +188,32 @@ def get_context_data(self, **kwargs): try: sha = self.request.GET.get("version") except KeyError: - sha = content.sha_draft + if self.sha is not None: + sha = self.sha + else: + sha = content.sha_draft # check that if we ask for beta, we also ask for the sha version is_beta = (sha == content.sha_beta and content.in_beta()) - + # check that if we ask for public version, we also ask for the sha version + is_online = (sha == content.sha_public and content.is_online()) # Only authors of the tutorial and staff can view tutorial in offline. - if self.request.user not in content.authors.all() and not is_beta: + if self.request.user not in content.authors.all() and not is_beta and not is_online: # if we are not author of this content or if we did not ask for beta # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Find the good manifest file repo = Repo(content.get_path()) # Load the tutorial. - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) + mandata = content.load_json_for_public(sha) content.load_dic(mandata, sha) - content.load_introduction_and_conclusion(mandata, sha) + content.load_introduction_and_conclusion(mandata, sha, sha == content.sha_public) children_tree = {} if 'chapter' in mandata: @@ -202,38 +228,132 @@ def get_context_data(self, **kwargs): self.compatibility_parts(content, repo, sha, part, cpt_p) cpt_p += 1 children_tree = parts - validation = Validation.objects.filter(tutorial__pk=content.pk)\ - .order_by("-date_proposition")\ - .first() - form_js = ActivJsForm(initial={"js_support": content.js_support}) - - if content.source: - form_ask_validation = AskValidationForm(initial={"source": content.source}) - form_valid = ValidForm(initial={"source": content.source}) - else: - form_ask_validation = AskValidationForm() - form_valid = ValidForm() - form_reject = RejectForm() + # check whether this tuto support js fiddle if content.js_support: is_js = "js" else: is_js = "" + context["is_js"] = is_js context["tutorial"] = mandata # TODO : change to "content" context["children"] = children_tree context["version"] = sha - context["validation"] = validation - context["formAskValidation"] = form_ask_validation - context["formJs"] = form_js - context["formValid"] = form_valid - context["formReject"] = form_reject, - context["is_js"] = is_js + self.get_forms(context, content) return context class DisplayArticle(DisplayContent): - type = "Article" + type = "ARTICLE" + + +class DisplayOnlineContent(DisplayContent): + """Display online tutorial""" + type = "TUTORIAL" + template_name = "tutorial/view_online.html" + + def get_forms(self, context, content): + + # Build form to send a note for the current tutorial. + context['form'] = NoteForm(content, self.request.user) + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p + + cpt_c = 1 + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary + chapter["slug"] = slugify(chapter["title"]) + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + self.compatibility_chapter(content, repo, sha, chapter) + cpt_c += 1 + + def compatibility_chapter(self,content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_prod_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), + "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), + "conclusion.md" + ".html"), "r") + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_prod_path() + text = open(os.path.join(content.get_prod_path(), ext["text"] + + ".html"), "r") + ext["txt"] = text.read() + cpt += 1 + + def get_context_data(self, **kwargs): + content = self.get_object() + # If the tutorial isn't online, we raise 404 error. + if not content.is_online(): + raise Http404 + self.sha = content.sha_public + context = super(DisplayOnlineContent, self).get_context_data(**kwargs) + + context["tutorial"]["update"] = content.update + context["tutorial"]["get_note_count"] = content.get_note_count() + + if self.request.user.is_authenticated(): + # If the user is authenticated, he may want to tell the world how cool the content is + # We check if he can post a not or not with + # antispam filter. + context['tutorial']['antispam'] = content.antispam() + + # If the user has never read this before, we mark this tutorial read. + if never_read(content): + mark_read(content) + + # Find all notes of the tutorial. + + notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() + + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. + + last_note_pk = 0 + if content.last_note: + last_note_pk = content.last_note.pk + + # Handle pagination + + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) + try: + page_nbr = int(self.request.GET.get("page")) + except KeyError: + page_nbr = 1 + except ValueError: + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + + res = [] + if page_nbr != 1: + + # Show the last note of the previous page + + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) + + context['notes'] = res + context['last_note_pk'] = last_note_pk + context['pages'] = paginator_range(page_nbr, paginator.num_pages) + context['nb'] = page_nbr # Staff actions. @@ -917,268 +1037,6 @@ def modify_tutorial(request): raise PermissionDenied -# Tutorials. - - -@login_required -def view_tutorial(request, tutorial_pk, tutorial_slug): - """Show the given offline tutorial if exists.""" - - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - - # Retrieve sha given by the user. This sha must to be exist. If it doesn't - # exist, we take draft version of the article. - - try: - sha = request.GET["version"] - except KeyError: - sha = tutorial.sha_draft - - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() - - # Only authors of the tutorial and staff can view tutorial in offline. - - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - - # Two variables to handle two distinct cases (large/small tutorial) - - chapter = None - parts = None - - # Find the good manifest file - - repo = Repo(tutorial.get_path()) - - # Load the tutorial. - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha) - tutorial.load_introduction_and_conclusion(mandata, sha) - - # If it's a small tutorial, fetch its chapter - - if tutorial.type == "MINI": - if 'chapter' in mandata: - chapter = mandata["chapter"] - chapter["path"] = tutorial.get_path() - chapter["type"] = "MINI" - chapter["pk"] = Chapter.objects.get(tutorial=tutorial).pk - chapter["intro"] = get_blob(repo.commit(sha).tree, - "introduction.md") - chapter["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" - ) - cpt = 1 - for ext in chapter["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = tutorial.get_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt += 1 - else: - chapter = None - else: - - # If it's a big tutorial, fetch parts. - - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - part["tutorial"] = tutorial - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt_e += 1 - cpt_c += 1 - cpt_p += 1 - validation = Validation.objects.filter(tutorial__pk=tutorial.pk)\ - .order_by("-date_proposition")\ - .first() - form_js = ActivJsForm(initial={"js_support": tutorial.js_support}) - - if tutorial.source: - form_ask_validation = AskValidationForm(initial={"source": tutorial.source}) - form_valid = ValidForm(initial={"source": tutorial.source}) - else: - form_ask_validation = AskValidationForm() - form_valid = ValidForm() - form_reject = RejectForm() - - if tutorial.js_support: - is_js = "js" - else: - is_js = "" - return render(request, "tutorial/tutorial/view.html", { - "tutorial": mandata, - "chapter": chapter, - "parts": parts, - "version": sha, - "validation": validation, - "formAskValidation": form_ask_validation, - "formJs": form_js, - "formValid": form_valid, - "formReject": form_reject, - "is_js": is_js - }) - - -def view_tutorial_online(request, tutorial_pk, tutorial_slug): - """Display a tutorial.""" - - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - - # If the tutorial isn't online, we raise 404 error. - if not tutorial.on_line(): - raise Http404 - - # Two variables to handle two distinct cases (large/small tutorial) - - chapter = None - parts = None - - # find the good manifest file - - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - tutorial.load_introduction_and_conclusion(mandata, public=True) - mandata["update"] = tutorial.update - mandata["get_note_count"] = tutorial.get_note_count() - - # If it's a small tutorial, fetch its chapter - - if tutorial.type == "MINI": - if "chapter" in mandata: - chapter = mandata["chapter"] - chapter["path"] = tutorial.get_prod_path() - chapter["type"] = "MINI" - intro = open(os.path.join(tutorial.get_prod_path(), - mandata["introduction"] + ".html"), "r") - chapter["intro"] = intro.read() - intro.close() - conclu = open(os.path.join(tutorial.get_prod_path(), - mandata["conclusion"] + ".html"), "r") - chapter["conclu"] = conclu.read() - conclu.close() - cpt = 1 - for ext in chapter["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = tutorial.get_prod_path() - text = open(os.path.join(tutorial.get_prod_path(), ext["text"] - + ".html"), "r") - ext["txt"] = text.read() - text.close() - cpt += 1 - else: - chapter = None - else: - - # chapter = Chapter.objects.get(tutorial=tutorial) - - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - part["tutorial"] = mandata - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - cpt_e += 1 - cpt_c += 1 - part["get_chapters"] = part["chapters"] - cpt_p += 1 - - mandata['get_parts'] = parts - - # If the user is authenticated - if request.user.is_authenticated(): - # We check if he can post a tutorial or not with - # antispam filter. - mandata['antispam'] = tutorial.antispam() - - # If the user is never read, we mark this tutorial read. - if never_read(tutorial): - mark_read(tutorial) - - # Find all notes of the tutorial. - - notes = ContentReaction.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() - - # Retrieve pk of the last note. If there aren't notes for the tutorial, we - # initialize this last note at 0. - - last_note_pk = 0 - if tutorial.last_note: - last_note_pk = tutorial.last_note.pk - - # Handle pagination - - paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) - try: - page_nbr = int(request.GET["page"]) - except KeyError: - page_nbr = 1 - except ValueError: - raise Http404 - - try: - notes = paginator.page(page_nbr) - except PageNotAnInteger: - notes = paginator.page(1) - except EmptyPage: - raise Http404 - - res = [] - if page_nbr != 1: - - # Show the last note of the previous page - - last_page = paginator.page(page_nbr - 1).object_list - last_note = last_page[len(last_page) - 1] - res.append(last_note) - for note in notes: - res.append(note) - - # Build form to send a note for the current tutorial. - - form = NoteForm(tutorial, request.user) - return render(request, "tutorial/tutorial/view_online.html", { - "tutorial": mandata, - "chapter": chapter, - "parts": parts, - "notes": res, - "pages": paginator_range(page_nbr, paginator.num_pages), - "nb": page_nbr, - "last_note_pk": last_note_pk, - "form": form, - }) - @can_write_and_read_now @login_required @@ -1384,7 +1242,7 @@ def edit_tutorial(request): tutorial.save() return redirect(tutorial.get_absolute_url()) else: - json = tutorial.load_json() + json = tutorial.load_json_for_public(tutorial.sha_draft) if "licence" in json: licence = json['licence'] else: @@ -1497,7 +1355,7 @@ def view_part_online( """Display a part.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.on_line(): + if not tutorial.is_online(): raise Http404 # find the good manifest file @@ -1868,7 +1726,7 @@ def view_chapter_online( """View chapter.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.on_line(): + if not tutorial.is_online(): raise Http404 # find the good manifest file @@ -3077,7 +2935,7 @@ def download(request): repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) sha = tutorial.sha_draft - if 'online' in request.GET and tutorial.on_line(): + if 'online' in request.GET and tutorial.is_online(): sha = tutorial.sha_public elif request.user not in tutorial.authors.all(): if not request.user.has_perm('tutorial.change_tutorial'): From 3048eb2f224230095aeaeb921f67a7094813b95c Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 11:23:19 +0100 Subject: [PATCH 010/887] =?UTF-8?q?compatibilit=C3=A9=20ZEP03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 385fbfefee..ed3343f8de 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -25,6 +25,7 @@ from zds.utils.models import SubCategory, Licence, Comment from zds.utils.tutorials import get_blob, export_tutorial from zds.settings import ZDS_APP +from zds.utils.models import HelpWriting TYPE_CHOICES = ( @@ -256,8 +257,10 @@ class Meta: licence = models.ForeignKey(Licence, verbose_name='Licence', blank=True, null=True, db_index=True) - # as of ZEP 12 this fiels is no longer the size but the type of content (article/tutorial) + # as of ZEP 12 this field is no longer the size but the type of content (article/tutorial) type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) + #zep03 field + helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) images = models.CharField( 'chemin relatif images', From e663c0c4743a65a104c868c52289e0846010cb3b Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 12:27:21 +0100 Subject: [PATCH 011/887] =?UTF-8?q?compatibilit=C3=A9=20ZEP03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/urls.py | 24 +++++++++++++++--------- zds/tutorialv2/views.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 97a24e5ce3..9871095ea8 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -21,28 +21,34 @@ url(r'^recherche/(?P\d+)/$', 'zds.tutorial.views.find_tuto'), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_chapter', name="view-chapter-url"), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_part', name="view-part-url"), - url(r'^off/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_tutorial'), + url(r'^tutorial/off/(?P\d+)/(?P.+)/$', + DisplayContent.as_view()), + + url(r'^article/off/(?P\d+)/(?P.+)/$', + DisplayArticle.as_view()), # View online - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_chapter_online', name="view-chapter-url-online"), - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_part_online', name="view-part-url-online"), - url(r'^(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_tutorial_online'), + url(r'^tutoriel/(?P\d+)/(?P.+)/$', + DisplayOnlineContent.as_view()), + + url(r'^article/(?P\d+)/(?P.+)/$', + DisplayOnlineArticle.as_view()), # Editing url(r'^editer/tutoriel/$', @@ -125,6 +131,6 @@ # Help url(r'^aides/$', - 'zds.tutorial.views.help_tutorial'), + TutorialWithHelp.as_view()), ) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index de4cfd43d2..5fe7a3a922 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -52,7 +52,7 @@ from zds.utils import slugify from zds.utils.models import Alert from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ - SubCategory + SubCategory, HelpWriting from zds.utils.mps import send_mp from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic from zds.utils.paginator import paginator_range @@ -113,6 +113,29 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorial/help.html' + + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects.exclude(Q(helps_count=0) and Q(sha_beta='')) + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context + + class DisplayContent(DetailView): """Base class that can show any content in any state, by default it shows offline tutorials""" @@ -356,6 +379,10 @@ def get_context_data(self, **kwargs): context['nb'] = page_nbr +class DisplayOnlineArticle(DisplayOnlineContent): + type = "ARTICLE" + + # Staff actions. From a7547cd311f557194c8b2d06fc3c7a0f0e4df35d Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sat, 27 Dec 2014 14:13:23 +0100 Subject: [PATCH 012/887] Separation des objets "BDD" et "JSON" --- zds/tutorialv2/admin.py | 4 +- zds/tutorialv2/feeds.py | 41 +- ...t__del_field_validation_tutorial__add_f.py | 262 ++++++ zds/tutorialv2/models.py | 781 ++++++++---------- zds/tutorialv2/tests/__init__.py | 0 zds/tutorialv2/tests/tests_models.py | 32 + zds/tutorialv2/views.py | 47 +- zds/utils/tutorialv2.py | 55 ++ 8 files changed, 733 insertions(+), 489 deletions(-) create mode 100644 zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py create mode 100644 zds/tutorialv2/tests/__init__.py create mode 100644 zds/tutorialv2/tests/tests_models.py create mode 100644 zds/utils/tutorialv2.py diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py index e966d5f7f4..79f515425f 100644 --- a/zds/tutorialv2/admin.py +++ b/zds/tutorialv2/admin.py @@ -2,11 +2,9 @@ from django.contrib import admin -from .models import PublishableContent, Container, Extract, Validation, ContentReaction +from .models import PublishableContent, Validation, ContentReaction admin.site.register(PublishableContent) -admin.site.register(Container) -admin.site.register(Extract) admin.site.register(Validation) admin.site.register(ContentReaction) diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index c5b48eb2c1..6f92671c3e 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -8,6 +8,7 @@ from models import PublishableContent from zds.settings import ZDS_APP + class LastContentFeedRSS(Feed): """ RSS feed for any type of content. @@ -19,8 +20,8 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the contents will only - be of this type. + :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the + contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: @@ -62,42 +63,6 @@ class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description -class LastArticlesFeedRSS(Feed): - title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) - link = "/articles/" - description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) - - def items(self): - return PublishableContent.objects\ - .filter(type="ARTICLE")\ - .filter(sha_public__isnull=False)\ - .order_by('-pubdate')[:5] - - def item_title(self, item): - return item.title - - def item_pubdate(self, item): - return item.pubdate - - def item_description(self, item): - return item.description - - def item_author_name(self, item): - authors_list = item.authors.all() - authors = [] - for authors_obj in authors_list: - authors.append(authors_obj.username) - authors = ", ".join(authors) - return authors - - def item_link(self, item): - return item.get_absolute_url_online() - - -class LastTutorialsFeedATOM(LastArticlesFeedRSS): - feed_type = Atom1Feed - subtitle = LastTutorialsFeedRSS.description - class LastArticlesFeedRSS(LastContentFeedRSS): """ diff --git a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py new file mode 100644 index 0000000000..a979a2e9df --- /dev/null +++ b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting model 'Container' + db.delete_table(u'tutorialv2_container') + + # Deleting model 'Extract' + db.delete_table(u'tutorialv2_extract') + + # Deleting field 'Validation.tutorial' + db.delete_column(u'tutorialv2_validation', 'tutorial_id') + + # Adding field 'Validation.content' + db.add_column(u'tutorialv2_validation', 'content', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), + keep_default=False) + + # Deleting field 'PublishableContent.container_ptr' + db.delete_column(u'tutorialv2_publishablecontent', u'container_ptr_id') + + # Deleting field 'PublishableContent.images' + db.delete_column(u'tutorialv2_publishablecontent', 'images') + + # Adding field 'PublishableContent.id' + db.add_column(u'tutorialv2_publishablecontent', u'id', + self.gf('django.db.models.fields.AutoField')(default=0, primary_key=True), + keep_default=False) + + # Adding field 'PublishableContent.title' + db.add_column(u'tutorialv2_publishablecontent', 'title', + self.gf('django.db.models.fields.CharField')(default='', max_length=80), + keep_default=False) + + # Adding field 'PublishableContent.relative_images_path' + db.add_column(u'tutorialv2_publishablecontent', 'relative_images_path', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), + keep_default=False) + + # Deleting field 'ContentRead.tutorial' + db.delete_column(u'tutorialv2_contentread', 'tutorial_id') + + # Adding field 'ContentRead.content' + db.add_column(u'tutorialv2_contentread', 'content', + self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['tutorialv2.PublishableContent']), + keep_default=False) + + + def backwards(self, orm): + # Adding model 'Container' + db.create_table(u'tutorialv2_container', ( + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + )) + db.send_create_signal(u'tutorialv2', ['Container']) + + # Adding model 'Extract' + db.create_table(u'tutorialv2_extract', ( + ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal(u'tutorialv2', ['Extract']) + + # Adding field 'Validation.tutorial' + db.add_column(u'tutorialv2_validation', 'tutorial', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), + keep_default=False) + + # Deleting field 'Validation.content' + db.delete_column(u'tutorialv2_validation', 'content_id') + + + # User chose to not deal with backwards NULL issues for 'PublishableContent.container_ptr' + raise RuntimeError("Cannot reverse this migration. 'PublishableContent.container_ptr' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'PublishableContent.container_ptr' + db.add_column(u'tutorialv2_publishablecontent', u'container_ptr', + self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True), + keep_default=False) + + # Adding field 'PublishableContent.images' + db.add_column(u'tutorialv2_publishablecontent', 'images', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), + keep_default=False) + + # Deleting field 'PublishableContent.id' + db.delete_column(u'tutorialv2_publishablecontent', u'id') + + # Deleting field 'PublishableContent.title' + db.delete_column(u'tutorialv2_publishablecontent', 'title') + + # Deleting field 'PublishableContent.relative_images_path' + db.delete_column(u'tutorialv2_publishablecontent', 'relative_images_path') + + + # User chose to not deal with backwards NULL issues for 'ContentRead.tutorial' + raise RuntimeError("Cannot reverse this migration. 'ContentRead.tutorial' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'ContentRead.tutorial' + db.add_column(u'tutorialv2_contentread', 'tutorial', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent']), + keep_default=False) + + # Deleting field 'ContentRead.content' + db.delete_column(u'tutorialv2_contentread', 'content_id') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index ed3343f8de..cb3643d8c6 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -23,7 +23,8 @@ from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user from zds.utils.models import SubCategory, Licence, Comment -from zds.utils.tutorials import get_blob, export_tutorial +from zds.utils.tutorials import get_blob +from zds.utils.tutorialv2 import export_content from zds.settings import ZDS_APP from zds.utils.models import HelpWriting @@ -45,7 +46,7 @@ class InvalidOperationError(RuntimeError): pass -class Container(models.Model): +class Container: """ A container, which can have sub-Containers or Extracts. @@ -54,71 +55,54 @@ class Container(models.Model): It has also a tree depth. - There is a `compatibility_pk` for compatibility with older versions. + A container could be either a tutorial/article, a part or a chapter. """ - class Meta: - verbose_name = 'Container' - verbose_name_plural = 'Containers' - - # TODO: clear all database related information ? - - title = models.CharField('Titre', max_length=80) - - slug = models.SlugField(max_length=80) - - introduction = models.CharField( - 'chemin relatif introduction', - blank=True, - null=True, - max_length=200) - - conclusion = models.CharField( - 'chemin relatif conclusion', - blank=True, - null=True, - max_length=200) - - parent = models.ForeignKey("self", - verbose_name='Conteneur parent', - blank=True, null=True, - on_delete=models.SET_NULL) - position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', - blank=False, - null=False, - default=1) + pk = 0 + title = '' + slug = '' + introduction = None + conclusion = None + parent = None + position_in_parent = 1 + children = [] # TODO: thumbnails ? - # integer key used to represent the tutorial or article old identifier for url compatibility - compatibility_pk = models.IntegerField(null=False, default=0) + def __init__(self, pk, title, parent=None, position_in_parent=1): + self.pk = pk + self.title = title + self.slug = slugify(title) + self.parent = parent + self.position_in_parent = position_in_parent + self.children = [] # even if you want, do NOT remove this line - def get_children(self): - """ - :return: children of this container, ordered by position - """ - if self.has_extract(): - return Extract.objects.filter(container_pk=self.pk) - - return Container.objects.filter(parent_pk=self.pk).order_by('position_in_parent') + def __unicode__(self): + return u''.format(self.title) - def has_extract(self): + def has_extracts(self): """ + Note : this function rely on the fact that the children can only be of one type. :return: `True` if the container has extract as children, `False` otherwise. """ - return Extract.objects.filter(parent=self).count() > 0 + if len(self.children) == 0: + return False + return isinstance(self.children[0], Extract) - def has_sub_container(self): + def has_sub_containers(self): """ - :return: `True` if the container has other Containers as children, `False` otherwise. + Note : this function rely on the fact that the children can only be of one type. + :return: `True` if the container has containers as children, `False` otherwise. """ - return Container.objects.filter(container=self).count() > 0 + if len(self.children) == 0: + return False + return isinstance(self.children[0], Container) def get_last_child_position(self): """ - :return: the relative position of the last child + :return: the position of the last child """ - return Container.objects.filter(parent=self).count() + Extract.objects.filter(container=self).count() + return len(self.children) def get_tree_depth(self): """ @@ -136,76 +120,330 @@ def get_tree_depth(self): depth += 1 return depth + def top_container(self): + """ + :return: Top container (for which parent is `None`) + """ + current = self + while current.parent is not None: + current = current.parent + return current + def add_container(self, container): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. :param container: the new container """ - if not self.has_extract(): + if not self.has_extracts(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: container.parent = self - container.position_in_parent = container.get_last_child_position() + 1 - container.save() + container.position_in_parent = self.get_last_child_position() + 1 + self.children.append(container) else: raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") # TODO: limitation if article ? - def get_phy_slug(self): + def add_extract(self, extract): """ - The slugified title is used to store physically the information in filesystem. - A "compatibility pk" can be used instead of real pk to ensure compatibility with previous versions. - :return: the slugified title + Add a child container, but only if no container were previously added + :param extract: the new extract """ - base = "" - if self.parent is not None: - base = self.parent.get_phy_slug() - - used_pk = self.compatibility_pk - if used_pk == 0: - used_pk = self.pk + if not self.has_sub_containers(): + extract.container = self + extract.position_in_parent = self.get_last_child_position() + 1 + self.children.append(extract) - return os.path.join(base, str(used_pk) + '_' + self.slug) + def get_phy_slug(self): + """ + :return: the physical slug, used to represent data in filesystem + """ + return str(self.pk) + '_' + self.slug def update_children(self): """ - Update all children of the container. + Update the path for introduction and conclusion for the container and all its children. If the children is an + extract, update the path to the text instead. This function is useful when `self.pk` or `self.title` has + changed. + Note : this function does not account for a different arrangement of the files. """ - for child in self.get_children(): - if child is Container: - self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") - self.conclusion = os.path.join(self.get_phy_slug(), "conclusion.md") - self.save() + self.introduction = os.path.join(self.get_path(relative=True), "introduction.md") + self.conclusion = os.path.join(self.get_path(relative=True), "conclusion.md") + for child in self.children: + if isinstance(child, Container): child.update_children() - else: + elif isinstance(child, Extract): child.text = child.get_path(relative=True) - child.save() - def add_extract(self, extract): + def get_path(self, relative=False): """ - Add a child container, but only if no container were previously added - :param extract: the new extract + Get the physical path to the draft version of the container. + Note: this function rely on the fact that the top container is VersionedContainer. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path """ - if not self.has_sub_container(): - extract.container = self - extract.save() + base = '' + if self.parent: + base = self.parent.get_path(relative=relative) + return os.path.join(base, self.get_phy_slug()) + + def get_prod_path(self): + """ + Get the physical path to the public version of the container. + Note: this function rely on the fact that the top container is VersionedContainer. + :return: physical path + """ + base = '' + if self.parent: + base = self.parent.get_prod_path() + return os.path.join(base, self.get_phy_slug()) + + def get_introduction(self): + """ + :return: the introduction from the file in `self.introduction` + """ + if self.introduction: + return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, + self.introduction) + + def get_introduction_online(self): + """ + Get introduction content of the public version + :return: the introduction + """ + path = self.top_container().get_prod_path() + self.introduction + '.html' + if os.path.exists(path): + intro = open(path) + intro_content = intro.read() + intro.close() + return intro_content.decode('utf-8') + + def get_conclusion(self): + """ + :return: the conclusion from the file in `self.conclusion` + """ + if self.introduction: + return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, + self.conclusion) + + def get_conclusion_online(self): + """ + Get conclusion content of the public version + :return: the conclusion + """ + path = self.top_container().get_prod_path() + self.conclusion + '.html' + if os.path.exists(path): + conclusion = open(path) + conclusion_content = conclusion.read() + conclusion.close() + return conclusion_content.decode('utf-8') + # TODO: - # - rewrite save() - # - get_absolute_url_*() stuffs, get_path(), get_prod_path() - # - __unicode__() - # - get_introduction_*(), get_conclusion_*() - # - a `top_parent()` function to access directly to the parent PublishableContent and avoid the - # `container.parent.parent.parent` stuff ? - # - a nice `delete_entity_and_tree()` function ? (which also remove the file) + # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) # - the `maj_repo_*()` stuffs should probably be into the model ? -class PublishableContent(Container): +class Extract: + """ + A content extract from a Container. + + It has a title, a position in the parent container and a text. + """ + + title = '' + container = None + position_in_container = 1 + text = None + pk = 0 + + def __init__(self, pk, title, container=None, position_in_container=1): + self.pk = pk + self.title = title + self.container = container + self.position_in_container = position_in_container + + def __unicode__(self): + return u''.format(self.title) + + def get_absolute_url(self): + """ + :return: the url to access the tutorial offline + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url(), + self.position_in_container, + slugify(self.title) + ) + + def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_online(), + self.position_in_container, + slugify(self.title) + ) + + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_beta(), + self.position_in_container, + slugify(self.title) + ) + + def get_phy_slug(self): + """ + :return: the physical slug + """ + return str(self.pk) + '_' + slugify(self.title) + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the extract. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + + def get_prod_path(self): + """ + Get the physical path to the public version of a specific version of the extract. + :return: physical path + """ + return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' + + def get_text(self): + if self.text: + return get_blob( + self.container.top_container().repository.commit(self.container.top_container().current_version).tree, + self.text) + + def get_text_online(self): + path = self.container.top_container().get_prod_path() + self.text + '.html' + if os.path.exists(path): + txt = open(path) + txt_content = txt.read() + txt.close() + return txt_content.decode('utf-8') + + +class VersionedContent(Container): + """ + This class is used to handle a specific version of a tutorial. + + It is created from the "manifest.json" file, and could dump information in it. + + For simplicity, it also contains DB information (but cannot modified them!), filled at the creation. + """ + + current_version = None + repository = None + + description = '' + type = '' + licence = '' + + # Information from DB + sha_draft = None + sha_beta = None + sha_public = None + sha_validation = None + is_beta = False + is_validation = False + is_public = False + + # TODO `load_dic()` provide more information, actually + + def __init__(self, current_version, pk, _type, title): + Container.__init__(self, pk, title) + self.current_version = current_version + self.type = _type + self.repository = Repo(self.get_path()) + # so read JSON ? + + def __unicode__(self): + return self.title + + def get_absolute_url(self): + """ + :return: the url to access the tutorial when offline + """ + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) + + def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) + + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + if self.is_beta: + return self.get_absolute_url() + '?version=' + self.sha_beta + else: + return self.get_absolute_url() + + def get_edit_url(self): + """ + :return: the url to edit the tutorial + """ + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + if relative: + return '' + else: + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + + def get_prod_path(self): + """ + Get the physical path to the public version of the content + :return: physical path + """ + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.get_phy_slug()) + + def get_json(self): + """ + :return: raw JSON file + """ + dct = export_content(self) + data = json_writer.dumps(dct, indent=4, ensure_ascii=False) + return data + + def dump_json(self, path=None): + """ + Write the JSON into file + :param path: path to the file. If `None`, write in "manifest.json" + """ + if path is None: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + json_data = open(man_path, "w") + json_data.write(self.get_json().encode('utf-8')) + json_data.close() + + +class PublishableContent(models.Model): """ A tutorial whatever its size or an article. - A PublishableContent is a tree depth 0 Container (no parent) with additional information, such as + A PublishableContent retains metadata about a content in database, such as + - authors, description, source (if the content comes from another website), subcategory and licence ; - Thumbnail and gallery ; - Creation, publication and update date ; @@ -216,10 +454,10 @@ class PublishableContent(Container): These are two repositories : draft and online. """ class Meta: - verbose_name = 'Tutoriel' - verbose_name_plural = 'Tutoriels' - # TODO: "Contenu" ? + verbose_name = 'Contenu' + verbose_name_plural = 'Contenus' + title = models.CharField('Titre', max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -262,12 +500,11 @@ class Meta: #zep03 field helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) - images = models.CharField( + relative_images_path = models.CharField( 'chemin relatif images', blank=True, null=True, max_length=200) - # TODO: rename this field ? (`relative_image_path` ?) last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', @@ -275,38 +512,9 @@ class Meta: is_locked = models.BooleanField('Est verrouillé', default=False) js_support = models.BooleanField('Support du Javascript', default=False) - # TODO : split this class in two part (one for the DB object, another one for JSON [versionned] file) ? - def __unicode__(self): return self.title - def get_absolute_url(self): - """ - :return: the url to access the tutorial when offline - """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) - - def get_absolute_url_online(self): - """ - :return: the url to access the tutorial when online - """ - return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) - - def get_absolute_url_beta(self): - """ - :return: the url to access the tutorial when in beta - """ - if self.sha_beta is not None: - return self.get_absolute_url() + '?version=' + self.sha_beta - else: - return self.get_absolute_url() - - def get_edit_url(self): - """ - :return: the url to edit the tutorial - """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) - def in_beta(self): """ A tutorial is not in beta if sha_beta is `None` or empty @@ -326,10 +534,9 @@ def in_drafting(self): A tutorial is not in draft if sha_draft is `None` or empty :return: `True` if the tutorial is in draft, `False` otherwise """ - # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') - def is_online(self): + def in_public(self): """ A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise @@ -349,37 +556,24 @@ def is_tutorial(self): """ return self.type == 'TUTORIAL' - def get_phy_slug(self): - """ - :return: the physical slug, used to represent data in filesystem - """ - return str(self.pk) + "_" + self.slug - - def get_path(self, relative=False): - """ - Get the physical path to the draft version of the Content. - :param relative: if `True`, the path will be relative, absolute otherwise. - :return: physical path - """ - if relative: - return '' - else: - # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) - # TODO: versionning ?!? - - def get_prod_path(self, sha=None): + def load_json_for_public(self, sha=None): """ - Get the physical path to the public version of the content - :param sha: version of the content, if `None`, public version is used - :return: physical path + Fetch the public version of the JSON file for this content. + :param sha: version + :return: a dictionary containing the structure of the JSON file. """ - data = self.load_json_for_public(sha) - return os.path.join( - settings.ZDS_APP[self.type.lower()]['repo_public_path'], - str(self.pk) + '_' + slugify(data['title'])) + if sha is None: + sha = self.sha_public + repo = Repo(self.get_path()) # should be `get_prod_path()` !?! + mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') + data = json_reader.loads(mantuto) + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + # TODO: mix that with next function def load_dic(self, mandata, sha=None): + # TODO should load JSON and store it in VersionedContent """ Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. :param mandata: a dictionary from JSON file @@ -407,7 +601,7 @@ def load_dic(self, mandata, sha=None): mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.is_online() and self.sha_public == sha + mandata['is_on_line'] = self.in_public() and self.sha_public == sha # url: mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) @@ -429,139 +623,6 @@ def load_dic(self, mandata, sha=None): args=[self.pk, mandata['slug']] ) - def load_introduction_and_conclusion(self, mandata, sha=None, public=False): - """ - Explicitly load introduction and conclusion to avoid useless disk access in `load_dic()` - :param mandata: dictionary from JSON file - :param sha: version - :param public: if `True`, get introduction and conclusion from the public version instead of the draft one - (`sha` is not used in this case) - """ - - if public: - mandata['get_introduction_online'] = self.get_introduction_online() - mandata['get_conclusion_online'] = self.get_conclusion_online() - else: - mandata['get_introduction'] = self.get_introduction(sha) - mandata['get_conclusion'] = self.get_conclusion(sha) - - def load_json_for_public(self, sha=None): - """ - Fetch the public version of the JSON file for this content. - :param sha: version - :return: a dictionary containing the structure of the JSON file. - """ - if sha is None: - sha = self.sha_public - repo = Repo(self.get_path()) # should be `get_prod_path()` !?! - mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') - data = json_reader.loads(mantuto) - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data - - def dump_json(self, path=None): - """ - Write the JSON into file - :param path: path to the file. If `None`, use default path. - """ - if path is None: - man_path = os.path.join(self.get_path(), 'manifest.json') - else: - man_path = path - - dct = export_tutorial(self) - data = json_writer.dumps(dct, indent=4, ensure_ascii=False) - json_data = open(man_path, "w") - json_data.write(data.encode('utf-8')) - json_data.close() - - def get_introduction(self, sha=None): - """ - Get the introduction content of a specific version - :param sha: version, if `None`, use draft one - :return: the introduction (as a string) - """ - # find hash code - if sha is None: - sha = self.sha_draft - repo = Repo(self.get_path()) - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - content_version = json_reader.loads(manifest) - if "introduction" in content_version: - path_content_intro = content_version["introduction"] - - if path_content_intro: - return get_blob(repo.commit(sha).tree, path_content_intro) - - def get_introduction_online(self): - """ - Get introduction content of the public version - :return: the introduction (as a string) - """ - if self.is_online(): - intro = open( - os.path.join( - self.get_prod_path(), - self.introduction + - '.html'), - "r") - intro_contenu = intro.read() - intro.close() - - return intro_contenu.decode('utf-8') - - def get_conclusion(self, sha=None): - """ - Get the conclusion content of a specific version - :param sha: version, if `None`, use draft one - :return: the conclusion (as a string) - """ - # find hash code - if sha is None: - sha = self.sha_draft - repo = Repo(self.get_path()) - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - content_version = json_reader.loads(manifest) - if "introduction" in content_version: - path_content_ccl = content_version["conclusion"] - - if path_content_ccl: - return get_blob(repo.commit(sha).tree, path_content_ccl) - - def get_conclusion_online(self): - """ - Get conclusion content of the public version - :return: the conclusion (as a string) - """ - if self.is_online(): - conclusion = open( - os.path.join( - self.get_prod_path(), - self.conclusion + - '.html'), - "r") - conlusion_content = conclusion.read() - conclusion.close() - - return conlusion_content.decode('utf-8') - - def delete_entity_and_tree(self): - """ - Delete the entities and their filesystem counterparts - """ - shutil.rmtree(self.get_path(), 0) - Validation.objects.filter(tutorial=self).delete() - - if self.gallery is not None: - self.gallery.delete() - if self.is_online(): - shutil.rmtree(self.get_prod_path()) - self.delete() - # TODO: should use the "git" version of `delete()` !!! - def save(self, *args, **kwargs): self.slug = slugify(self.title) @@ -678,6 +739,19 @@ def have_epub(self): """ return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) + def delete_entity_and_tree(self): + """ + Delete the entities and their filesystem counterparts + """ + shutil.rmtree(self.get_path(), False) + Validation.objects.filter(tutorial=self).delete() + + if self.gallery is not None: + self.gallery.delete() + if self.in_public(): + shutil.rmtree(self.get_prod_path()) + self.delete() + class ContentReaction(Comment): """ @@ -719,143 +793,6 @@ def __unicode__(self): return u''.format(self.content, self.user, self.note.pk) -class Extract(models.Model): - """ - A content extract from a Container. - - It has a title, a position in the parent container and a text. - """ - class Meta: - verbose_name = 'Extrait' - verbose_name_plural = 'Extraits' - - # TODO: clear all database related information ? - - title = models.CharField('Titre', max_length=80) - container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) - position_in_container = models.IntegerField('Position dans le parent', db_index=True) - - text = models.CharField( - 'chemin relatif du texte', - blank=True, - null=True, - max_length=200) - - def __unicode__(self): - return u''.format(self.title) - - def get_absolute_url(self): - """ - :return: the url to access the tutorial offline - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url(), - self.position_in_container, - slugify(self.title) - ) - - def get_absolute_url_online(self): - """ - :return: the url to access the tutorial when online - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url_online(), - self.position_in_container, - slugify(self.title) - ) - - def get_absolute_url_beta(self): - """ - :return: the url to access the tutorial when in beta - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url_beta(), - self.position_in_container, - slugify(self.title) - ) - - def get_phy_slug(self): - """ - :return: the physical slug - """ - return str(self.pk) + '_' + slugify(self.title) - - def get_path(self, relative=False): - """ - Get the physical path to the draft version of the extract. - :param relative: if `True`, the path will be relative, absolute otherwise. - :return: physical path - """ - return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' - # TODO: versionning ? - - def get_prod_path(self, sha=None): - """ - Get the physical path to the public version of a specific version of the extract. - :param sha: version of the content, if `None`, `sha_public` is used - :return: physical path - """ - return os.path.join(self.container.get_prod_path(sha), self.get_phy_slug()) + '.md.html' - - def get_text(self, sha=None): - - if self.container.tutorial: - tutorial = self.container.tutorial - else: - tutorial = self.container.part.tutorial - repo = Repo(tutorial.get_path()) - - # find hash code - if sha is None: - sha = tutorial.sha_draft - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - tutorial_version = json_reader.loads(manifest) - if "parts" in tutorial_version: - for part in tutorial_version["parts"]: - if "chapters" in part: - for chapter in part["chapters"]: - if "extracts" in chapter: - for extract in chapter["extracts"]: - if extract["pk"] == self.pk: - path_ext = extract["text"] - break - if "chapter" in tutorial_version: - chapter = tutorial_version["chapter"] - if "extracts" in chapter: - for extract in chapter["extracts"]: - if extract["pk"] == self.pk: - path_ext = extract["text"] - break - - if path_ext: - return get_blob(repo.commit(sha).tree, path_ext) - else: - return None - - def get_text_online(self): - - if self.container.tutorial: - path = os.path.join( - self.container.tutorial.get_prod_path(), - self.text + - '.html') - else: - path = os.path.join( - self.container.part.tutorial.get_prod_path(), - self.text + - '.html') - - if os.path.isfile(path): - text = open(path, "r") - text_contenu = text.read() - text.close() - - return text_contenu.decode('utf-8') - else: - return None - - class Validation(models.Model): """ Content validation. diff --git a/zds/tutorialv2/tests/__init__.py b/zds/tutorialv2/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py new file mode 100644 index 0000000000..ca9f27c9ed --- /dev/null +++ b/zds/tutorialv2/tests/tests_models.py @@ -0,0 +1,32 @@ +# coding: utf-8 + +# NOTE : this file is only there for tests purpose, it will be deleted in final version + +import os + +from django.test import TestCase +from django.test.utils import override_settings +from zds.settings import SITE_ROOT + +from zds.tutorialv2.models import Container, Extract, VersionedContent + + +@override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) +@override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) +@override_settings(REPO_PATH_PROD=os.path.join(SITE_ROOT, 'tutoriels-public-test')) +@override_settings(REPO_ARTICLE_PATH=os.path.join(SITE_ROOT, 'articles-data-test')) +class ContentTests(TestCase): + + def setUp(self): + self.start_version = 'ca5508a' # real version, adapt it ! + self.content = VersionedContent(self.start_version, 1, 'TUTORIAL', 'Mon tutoriel no1') + + self.container = Container(1, 'Mon chapitre no1') + self.content.add_container(self.container) + + self.extract = Extract(1, 'Un premier extrait') + self.container.add_extract(self.extract) + self.content.update_children() + + def test_workflow_content(self): + print(self.container.position_in_parent) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 5fe7a3a922..ad7a7b4199 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,7 +42,7 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation, ContentRead, ContentReaction +from models import PublishableContent, Container, Extract, Validation, ContentReaction # , ContentRead from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now @@ -60,7 +60,9 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView# , UpdateView +from django.views.generic import ListView, DetailView # , UpdateView +# until we completely get rid of these, import them : +from zds.tutorial.models import Tutorial, Chapter, Part, HelpWriting class ArticleList(ListView): @@ -159,15 +161,13 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - def compatibility_chapter(self,content, repo, sha, dictionary): + def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" dictionary["path"] = content.get_path() dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = get_blob(repo.commit(sha).tree, - "introduction.md") - dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" - ) + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") + dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md") cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt @@ -191,12 +191,11 @@ def get_forms(self, context, content): form_reject = RejectForm() context["validation"] = validation - context["formAskValidation"] = form_ask_validation + context["formAskValidation"] = form_ask_validation context["formJs"] = form_js context["formValid"] = form_valid context["formReject"] = form_reject, - def get_object(self): return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) @@ -219,7 +218,7 @@ def get_context_data(self, **kwargs): # check that if we ask for beta, we also ask for the sha version is_beta = (sha == content.sha_beta and content.in_beta()) # check that if we ask for public version, we also ask for the sha version - is_online = (sha == content.sha_public and content.is_online()) + is_online = (sha == content.sha_public and content.in_public()) # Only authors of the tutorial and staff can view tutorial in offline. if self.request.user not in content.authors.all() and not is_beta and not is_online: @@ -258,7 +257,7 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = mandata # TODO : change to "content" + context["tutorial"] = mandata # TODO : change to "content" context["children"] = children_tree context["version"] = sha self.get_forms(context, content) @@ -295,28 +294,25 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - def compatibility_chapter(self,content, repo, sha, dictionary): + def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" dictionary["path"] = content.get_prod_path() dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = open(os.path.join(content.get_prod_path(), - "introduction.md" + ".html"), "r") - dictionary["conclu"] = open(os.path.join(content.get_prod_path(), - "conclusion.md" + ".html"), "r") + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt ext["path"] = content.get_prod_path() - text = open(os.path.join(content.get_prod_path(), ext["text"] - + ".html"), "r") + text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") ext["txt"] = text.read() cpt += 1 def get_context_data(self, **kwargs): content = self.get_object() - # If the tutorial isn't online, we raise 404 error. - if not content.is_online(): + # If the tutorial isn't online, we raise 404 error. + if not content.in_public(): raise Http404 self.sha = content.sha_public context = super(DisplayOnlineContent, self).get_context_data(**kwargs) @@ -1064,7 +1060,6 @@ def modify_tutorial(request): raise PermissionDenied - @can_write_and_read_now @login_required def add_tutorial(request): @@ -1382,7 +1377,7 @@ def view_part_online( """Display a part.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.is_online(): + if not tutorial.in_public(): raise Http404 # find the good manifest file @@ -1753,7 +1748,7 @@ def view_chapter_online( """View chapter.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.is_online(): + if not tutorial.in_public(): raise Http404 # find the good manifest file @@ -2962,7 +2957,7 @@ def download(request): repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) sha = tutorial.sha_draft - if 'online' in request.GET and tutorial.is_online(): + if 'online' in request.GET and tutorial.in_public(): sha = tutorial.sha_public elif request.user not in tutorial.authors.all(): if not request.user.has_perm('tutorial.change_tutorial'): diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py new file mode 100644 index 0000000000..6959b0c60d --- /dev/null +++ b/zds/utils/tutorialv2.py @@ -0,0 +1,55 @@ +from collections import OrderedDict + + +def export_extract(extract): + """ + Export an extract to a dictionary + :param extract: extract to export + :return: dictionary containing the information + """ + dct = OrderedDict() + dct['pk'] = extract.pk + dct['title'] = extract.title + dct['text'] = extract.text + return dct + + +def export_container(container): + """ + Export a container to a dictionary + :param container: the container + :return: dictionary containing the information + """ + dct = OrderedDict() + dct['pk'] = container.pk + dct['title'] = container.title + dct['obj_type'] = "container" + dct['introduction'] = container.introduction + dct['conclusion'] = container.conclusion + dct['children'] = [] + + if container.has_sub_containers(): + for child in container.children: + dct['children'].append(export_container(child)) + elif container.has_extracts(): + for child in container.children: + dct['children'].append(export_extract(child)) + + return dct + + +def export_content(content): + """ + Export a content to dictionary in order to store them in a JSON file + :param content: content to be exported + :return: dictionary containing the information + """ + dct = export_container(content) + + # append metadata : + dct['description'] = content.description + dct['type'] = content.type + if content.licence: + dct['licence'] = content.licence.code + + return dct From c4c80d4aacedc244d5a13989c3e66ec4ffa32d7d Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 15:33:00 +0100 Subject: [PATCH 013/887] travail sur les factory zep12 --- zds/tutorialv2/factories.py | 101 ++++++++++++++---------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index dd9147ee72..38e625545b 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -7,7 +7,7 @@ import factory -from zds.tutorial.models import Tutorial, Part, Chapter, Extract, Note,\ +from models import PublishableContent, Container, Extract, ContentReaction,\ Validation from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory @@ -32,12 +32,12 @@ class BigTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) description = factory.Sequence( lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'BIG' + type = 'TUTORIAL' create_at = datetime.now() introduction = 'introduction.md' conclusion = 'conclusion.md' @@ -79,12 +79,12 @@ def _prepare(cls, create, **kwargs): class MiniTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) description = factory.Sequence( lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'MINI' + type = 'TUTORIAL' create_at = datetime.now() introduction = 'introduction.md' conclusion = 'conclusion.md' @@ -130,7 +130,7 @@ def _prepare(cls, create, **kwargs): class PartFactory(factory.DjangoModelFactory): - FACTORY_FOR = Part + FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) @@ -138,7 +138,7 @@ class PartFactory(factory.DjangoModelFactory): def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) part = super(PartFactory, cls)._prepare(create, **kwargs) - tutorial = kwargs.pop('tutorial', None) + parent = kwargs.pop('tutorial', None) real_content = content if light: @@ -154,20 +154,20 @@ def _prepare(cls, create, **kwargs): part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') part.save() - f = open(os.path.join(tutorial.get_path(), part.introduction), "w") + f = open(os.path.join(parent.get_path(), part.introduction), "w") f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.introduction]) - f = open(os.path.join(tutorial.get_path(), part.conclusion), "w") + f = open(os.path.join(parent.get_path(), part.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.conclusion]) - if tutorial: - tutorial.save() + if parent: + parent.save() - man = export_tutorial(tutorial) - f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + man = export_tutorial(parent) + f = open(os.path.join(parent.get_path(), 'manifest.json'), "w") f.write( json_writer.dumps( man, @@ -179,15 +179,15 @@ def _prepare(cls, create, **kwargs): cm = repo.index.commit("Init Part") - if tutorial: - tutorial.sha_draft = cm.hexsha - tutorial.save() + if parent: + parent.sha_draft = cm.hexsha + parent.save() return part class ChapterFactory(factory.DjangoModelFactory): - FACTORY_FOR = Chapter + FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) @@ -196,8 +196,7 @@ def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) - tutorial = kwargs.pop('tutorial', None) - part = kwargs.pop('part', None) + parent = kwargs.pop('part', None) real_content = content if light: @@ -207,53 +206,37 @@ def _prepare(cls, create, **kwargs): if not os.path.isdir(path): os.makedirs(path, mode=0o777) - if tutorial: - chapter.introduction = '' - chapter.conclusion = '' - tutorial.save() - repo = Repo(tutorial.get_path()) - - man = export_tutorial(tutorial) - f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - repo.index.add(['manifest.json']) - - elif part: + if parent: chapter.introduction = os.path.join( - part.get_phy_slug(), + parent.get_phy_slug(), chapter.get_phy_slug(), 'introduction.md') chapter.conclusion = os.path.join( - part.get_phy_slug(), + parent.get_phy_slug(), chapter.get_phy_slug(), 'conclusion.md') chapter.save() f = open( os.path.join( - part.tutorial.get_path(), + parent.tutorial.get_path(), chapter.introduction), "w") f.write(real_content.encode('utf-8')) f.close() f = open( os.path.join( - part.tutorial.get_path(), + parent.tutorial.get_path(), chapter.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() - part.tutorial.save() - repo = Repo(part.tutorial.get_path()) + parent.tutorial.save() + repo = Repo(parent.tutorial.get_path()) - man = export_tutorial(part.tutorial) + man = export_tutorial(parent.tutorial) f = open( os.path.join( - part.tutorial.get_path(), + parent.parent.get_path(), 'manifest.json'), "w") f.write( @@ -268,15 +251,11 @@ def _prepare(cls, create, **kwargs): cm = repo.index.commit("Init Chapter") - if tutorial: - tutorial.sha_draft = cm.hexsha - tutorial.save() - chapter.tutorial = tutorial - elif part: - part.tutorial.sha_draft = cm.hexsha - part.tutorial.save() - part.save() - chapter.part = part + if parent: + parent.parent.sha_draft = cm.hexsha + parent.parent.save() + parent.save() + chapter.parent = parent return chapter @@ -289,14 +268,14 @@ class ExtractFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - chapter = kwargs.pop('chapter', None) - if chapter: - if chapter.tutorial: - chapter.tutorial.sha_draft = 'EXTRACT-AAAA' - chapter.tutorial.save() - elif chapter.part: - chapter.part.tutorial.sha_draft = 'EXTRACT-AAAA' - chapter.part.tutorial.save() + container = kwargs.pop('container', None) + if container: + if container.parent is PublishableContent: + container.parent.sha_draft = 'EXTRACT-AAAA' + container.parent.save() + elif container.parent.parent is PublishableContent: + container.parent.parent.sha_draft = 'EXTRACT-AAAA' + container.parent.parent.tutorial.save() return extract From 0bc3b2e033a0fe34d08ed339df92ec4f9033a68c Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 16:59:29 +0100 Subject: [PATCH 014/887] =?UTF-8?q?pep8=20+=20diff=20migr=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/factories.py | 4 +- zds/tutorialv2/feeds.py | 4 +- zds/tutorialv2/models.py | 2 +- zds/tutorialv2/views.py | 184 ++++++++++++++++-------------------- 4 files changed, 86 insertions(+), 108 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 38e625545b..91fe32412d 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -281,7 +281,7 @@ def _prepare(cls, create, **kwargs): class NoteFactory(factory.DjangoModelFactory): - FACTORY_FOR = Note + FACTORY_FOR = ContentReaction ip_address = '192.168.3.1' text = 'Bonjour, je me présente, je m\'appelle l\'homme au texte bidonné' @@ -323,7 +323,7 @@ def _prepare(cls, create, **kwargs): class PublishedMiniTutorial(MiniTutorialFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent @classmethod def _prepare(cls, create, **kwargs): diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index 6f92671c3e..f1dcbfff93 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -20,8 +20,8 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the - contents will only be of this type. + :return: The last (typically 5) contents (sorted by publication date). + If `self.type` is not `None`, the contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cb3643d8c6..16d9ea5d53 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -497,7 +497,7 @@ class Meta: blank=True, null=True, db_index=True) # as of ZEP 12 this field is no longer the size but the type of content (article/tutorial) type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) - #zep03 field + # zep03 field helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) relative_images_path = models.CharField( diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index ad7a7b4199..c2b875b4c3 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -123,7 +123,10 @@ class TutorialWithHelp(TutorialList): def get_queryset(self): """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects.exclude(Q(helps_count=0) and Q(sha_beta='')) + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() try: type_filter = self.request.GET.get('type') query_set = query_set.filter(helps_title__in=[type_filter]) @@ -131,6 +134,7 @@ def get_queryset(self): # if no filter, no need to change pass return query_set + def get_context_data(self, **kwargs): """Add all HelpWriting objects registered to the context so that the template can use it""" context = super(TutorialWithHelp, self).get_context_data(**kwargs) @@ -265,6 +269,40 @@ def get_context_data(self, **kwargs): return context +class DisplayDiff(DetailView): + """Display the difference between two version of a content. + Reference is always HEAD and compared version is a GET query parameter named sha + this class has no reason to be adapted to any content type""" + model = PublishableContent + template_name = "tutorial/diff.html" + context_object_name = "tutorial" + + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + + def get_context_data(self, **kwargs): + + context = super(DisplayDiff, self).get_context_data(**kwargs) + + try: + sha = self.request.GET.get("sha") + except KeyError: + sha = self.get_object().sha_draft + + if self.request.user not in context[self.context_object_name].authors.all(): + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + # open git repo and find diff between displayed version and head + repo = Repo(context[self.context_object_name].get_path()) + current_version_commit = repo.commit(sha) + diff_with_head = current_version_commit.diff("HEAD~1") + context["path_add"] = diff_with_head.iter_change_type("A") + context["path_ren"] = diff_with_head.iter_change_type("R") + context["path_del"] = diff_with_head.iter_change_type("D") + context["path_maj"] = diff_with_head.iter_change_type("M") + return context + + class DisplayArticle(DisplayContent): type = "ARTICLE" @@ -301,6 +339,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") + # load extracts cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt @@ -474,33 +513,11 @@ def reservation(request, validation_pk): ) -@login_required -def diff(request, tutorial_pk, tutorial_slug): - try: - sha = request.GET["sha"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - repo = Repo(tutorial.get_path()) - hcommit = repo.commit(sha) - tdiff = hcommit.diff("HEAD~1") - return render(request, "tutorial/tutorial/diff.html", { - "tutorial": tutorial, - "path_add": tdiff.iter_change_type("A"), - "path_ren": tdiff.iter_change_type("R"), - "path_del": tdiff.iter_change_type("D"), - "path_maj": tdiff.iter_change_type("M"), - }) - - @login_required def history(request, tutorial_pk, tutorial_slug): """History of the tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied @@ -517,7 +534,7 @@ def history(request, tutorial_pk, tutorial_slug): def history_validation(request, tutorial_pk): """History of the validation of a tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Get subcategory to filter validations. @@ -552,7 +569,7 @@ def reject_tutorial(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, version=tutorial.sha_validation).latest("date_proposition") @@ -617,7 +634,7 @@ def valid_tutorial(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, version=tutorial.sha_validation).latest("date_proposition") @@ -685,7 +702,7 @@ def invalid_tutorial(request, tutorial_pk): # Retrieve current tutorial - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) un_mep(tutorial) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, @@ -720,7 +737,7 @@ def ask_validation(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -781,7 +798,7 @@ def delete_tutorial(request, tutorial_pk): # Retrieve current tutorial - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -835,7 +852,7 @@ def delete_tutorial(request, tutorial_pk): @require_POST def modify_tutorial(request): tutorial_pk = request.POST["tutorial"] - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # User actions if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): @@ -1072,7 +1089,7 @@ def add_tutorial(request): # Creating a tutorial - tutorial = Tutorial() + tutorial = PublishableContent() tutorial.title = data["title"] tutorial.description = data["description"] tutorial.type = data["type"] @@ -1139,7 +1156,7 @@ def add_tutorial(request): # If it's a small tutorial, create its corresponding chapter if tutorial.type == "MINI": - chapter = Chapter() + chapter = Container() chapter.tutorial = tutorial chapter.save() tutorial.save() @@ -1173,7 +1190,7 @@ def edit_tutorial(request): tutorial_pk = request.GET["tutoriel"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -1297,7 +1314,7 @@ def view_part( ): """Display a part.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) try: sha = request.GET["version"] except KeyError: @@ -1376,7 +1393,7 @@ def view_part_online( ): """Display a part.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if not tutorial.in_public(): raise Http404 @@ -1442,7 +1459,7 @@ def add_part(request): tutorial_pk = request.GET["tutoriel"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Make sure it's a big tutorial, just in case @@ -1457,7 +1474,7 @@ def add_part(request): form = PartForm(request.POST) if form.is_valid(): data = form.data - part = Part() + part = Container() part.tutorial = tutorial part.title = data["title"] part.position_in_tutorial = tutorial.get_parts().count() + 1 @@ -1500,7 +1517,7 @@ def modify_part(request): if not request.method == "POST": raise Http404 part_pk = request.POST["part"] - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) # Make sure the user is allowed to do that @@ -1527,7 +1544,7 @@ def modify_part(request): elif "delete" in request.POST: # Delete all chapters belonging to the part - Chapter.objects.all().filter(part=part).delete() + Container.objects.all().filter(part=part).delete() # Move other parts @@ -1566,7 +1583,7 @@ def edit_part(request): except ValueError: raise Http404 - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) introduction = os.path.join(part.get_path(), "introduction.md") conclusion = os.path.join(part.get_path(), "conclusion.md") # Make sure the user is allowed to do that @@ -1628,7 +1645,7 @@ def edit_part(request): }) -# Chapters. +# Containers. @login_required @@ -1643,7 +1660,7 @@ def view_chapter( ): """View chapter.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) try: sha = request.GET["version"] @@ -1747,7 +1764,7 @@ def view_chapter_online( ): """View chapter.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if not tutorial.in_public(): raise Http404 @@ -1849,7 +1866,7 @@ def add_chapter(request): part_pk = request.GET["partie"] except KeyError: raise Http404 - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) # Make sure the user is allowed to do that @@ -1859,7 +1876,7 @@ def add_chapter(request): form = ChapterForm(request.POST, request.FILES) if form.is_valid(): data = form.data - chapter = Chapter() + chapter = Container() chapter.title = data["title"] chapter.part = part chapter.position_in_part = part.get_chapters().count() + 1 @@ -1934,7 +1951,7 @@ def modify_chapter(request): chapter_pk = request.POST["chapter"] except KeyError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) # Make sure the user is allowed to do that @@ -1988,7 +2005,7 @@ def modify_chapter(request): # Update all the position_in_tutorial fields for the next chapters for tut_c in \ - Chapter.objects.filter(position_in_tutorial__gt=old_tut_pos): + Container.objects.filter(position_in_tutorial__gt=old_tut_pos): tut_c.update_position_in_tutorial() tut_c.save() @@ -2017,7 +2034,7 @@ def edit_chapter(request): except ValueError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) big = chapter.part small = chapter.tutorial @@ -2106,7 +2123,7 @@ def add_extract(request): except ValueError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) part = chapter.part # If part exist, we check if the user is in authors of the tutorial of the @@ -2321,7 +2338,7 @@ def find_tuto(request, pk_user): type = None display_user = get_object_or_404(User, pk=pk_user) if type == "beta": - tutorials = Tutorial.objects.all().filter( + tutorials = PublishableContent.objects.all().filter( authors__in=[display_user], sha_beta__isnull=False).exclude(sha_beta="").order_by("-pubdate") @@ -2334,7 +2351,7 @@ def find_tuto(request, pk_user): return render(request, "tutorial/member/beta.html", {"tutorials": tuto_versions, "usr": display_user}) else: - tutorials = Tutorial.objects.all().filter( + tutorials = PublishableContent.objects.all().filter( authors__in=[display_user], sha_public__isnull=False).exclude(sha_public="").order_by("-pubdate") @@ -2393,7 +2410,7 @@ def import_content( images, logo, ): - tutorial = Tutorial() + tutorial = PublishableContent() # add create date @@ -2451,7 +2468,7 @@ def import_content( + str(part_count) + "]/introduction")[0] part_conclu = tree.xpath("/bigtuto/parties/partie[" + str(part_count) + "]/conclusion")[0] - part = Part() + part = Container() part.title = part_title.text.strip() part.position_in_tutorial = part_count part.tutorial = tutorial @@ -2493,7 +2510,7 @@ def import_content( "]/chapitres/chapitre[" + str(chapter_count) + "]/conclusion")[0] - chapter = Chapter() + chapter = Container() chapter.title = chapter_title.text.strip() chapter.position_in_part = chapter_count chapter.position_in_tutorial = part_count * chapter_count @@ -2601,7 +2618,7 @@ def import_content( action="add", ) tutorial.authors.add(request.user) - chapter = Chapter() + chapter = Container() chapter.tutorial = tutorial chapter.save() extract_count = 1 @@ -2952,7 +2969,7 @@ def insert_into_zip(zip_file, git_tree): def download(request): """Download a tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) @@ -2976,7 +2993,7 @@ def download(request): def download_markdown(request): """Download a markdown tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -2992,7 +3009,7 @@ def download_markdown(request): def download_html(request): """Download a pdf tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3010,7 +3027,7 @@ def download_html(request): def download_pdf(request): """Download a pdf tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3028,7 +3045,7 @@ def download_pdf(request): def download_epub(request): """Download an epub tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3281,7 +3298,7 @@ def answer(request): # Retrieve current tutorial. - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Making sure reactioning is allowed @@ -3438,7 +3455,7 @@ def activ_js(request): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - tutorial = get_object_or_404(Tutorial, pk=request.POST["tutorial"]) + tutorial = get_object_or_404(PublishableContent, pk=request.POST["tutorial"]) tutorial.js_support = "js_support" in request.POST tutorial.save() @@ -3457,7 +3474,7 @@ def edit_note(request): note = get_object_or_404(ContentReaction, pk=note_pk) g_tutorial = None if note.position >= 1: - g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) + g_tutorial = get_object_or_404(PublishableContent, pk=note.related_content.pk) # Making sure the user is allowed to do that. Author of the note must to be # the user logged. @@ -3613,42 +3630,3 @@ def dislike_note(request): return HttpResponse(json_writer.dumps(resp)) else: return redirect(note.get_absolute_url()) - - -def help_tutorial(request): - """fetch all tutorials that needs help""" - - # Retrieve type of the help. Default value is any help - type = request.GET.get('type', None) - - if type is not None: - aide = get_object_or_404(HelpWriting, slug=type) - tutos = Tutorial.objects.filter(helps=aide) \ - .all() - else: - tutos = Tutorial.objects.annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - - # Paginator - paginator = Paginator(tutos, settings.ZDS_APP['forum']['topics_per_page']) - page = request.GET.get('page') - - try: - shown_tutos = paginator.page(page) - page = int(page) - except PageNotAnInteger: - shown_tutos = paginator.page(1) - page = 1 - except EmptyPage: - shown_tutos = paginator.page(paginator.num_pages) - page = paginator.num_pages - - aides = HelpWriting.objects.all() - - return render(request, "tutorial/tutorial/help.html", { - "tutorials": shown_tutos, - "helps": aides, - "pages": paginator_range(page, paginator.num_pages), - "nb": page - }) From e6123cbb7ed9405790f3406c2432a81d631436bd Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sat, 27 Dec 2014 17:28:33 +0100 Subject: [PATCH 015/887] Separation DB et JSON : partie 2 --- zds/tutorialv2/factories.py | 314 +++++------------- zds/tutorialv2/migrations/0001_initial.py | 89 ++--- ...t__del_field_validation_tutorial__add_f.py | 262 --------------- zds/tutorialv2/models.py | 144 +++++--- zds/tutorialv2/tests/tests_models.py | 46 ++- zds/tutorialv2/views.py | 2 +- zds/utils/tutorialv2.py | 4 +- 7 files changed, 254 insertions(+), 607 deletions(-) delete mode 100644 zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 91fe32412d..e0e8a78330 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -2,285 +2,142 @@ from datetime import datetime from git.repo import Repo -import json as json_writer import os import factory from models import PublishableContent, Container, Extract, ContentReaction,\ - Validation from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory -from zds.utils.tutorials import export_tutorial -from zds.tutorial.views import mep - -content = ( - u'Ceci est un contenu de tutoriel utile et à tester un peu partout\n\n ' - u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits \n\n ' - u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc \n\n ' - u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)\n\n ' - u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)\n\n ' - u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)\n\n ' - u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)\n\n ' - u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2' - u'F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)\n\n ' - u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)\n\n ' - u'\n Attention les tests ne doivent pas crasher \n\n \n\n \n\n ' - u'qu\'un sujet abandonné !\n\n ') - -content_light = u'Un contenu light pour quand ce n\'est pas vraiment ça qui est testé' - - -class BigTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = PublishableContent - title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) - description = factory.Sequence( - lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'TUTORIAL' - create_at = datetime.now() - introduction = 'introduction.md' - conclusion = 'conclusion.md' +text_content = u'Ceci est un texte bidon' - @classmethod - def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) - path = tuto.get_path() - real_content = content - if light: - real_content = content_light - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - man = export_tutorial(tuto) - repo = Repo.init(path, bare=False) - repo = Repo(path) - - f = open(os.path.join(path, 'manifest.json'), "w") - f.write(json_writer.dumps(man, indent=4, ensure_ascii=False).encode('utf-8')) - f.close() - f = open(os.path.join(path, tuto.introduction), "w") - f.write(real_content.encode('utf-8')) - f.close() - f = open(os.path.join(path, tuto.conclusion), "w") - f.write(real_content.encode('utf-8')) - f.close() - repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) - cm = repo.index.commit("Init Tuto") - - tuto.sha_draft = cm.hexsha - tuto.sha_beta = None - tuto.gallery = GalleryFactory() - for author in tuto.authors.all(): - UserGalleryFactory(user=author, gallery=tuto.gallery) - return tuto - - -class MiniTutorialFactory(factory.DjangoModelFactory): +class PublishableContentFactory(factory.DjangoModelFactory): FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) - description = factory.Sequence( - lambda n: 'Description du Tutoriel No{0}'.format(n)) + description = factory.Sequence(lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'TUTORIAL' type = 'TUTORIAL' - create_at = datetime.now() - introduction = 'introduction.md' - conclusion = 'conclusion.md' @classmethod def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - tuto = super(MiniTutorialFactory, cls)._prepare(create, **kwargs) - real_content = content - - if light: - real_content = content_light - path = tuto.get_path() + publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) + path = publishable_content.get_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) - man = export_tutorial(tuto) - repo = Repo.init(path, bare=False) + FACTORY_FOR = PublishableContent + type = 'TUTORIAL' + introduction = 'introduction.md' + conclusion = 'conclusion.md' + versioned_content = VersionedContent(None, + publishable_content.pk, + publishable_content.type, + publishable_content.title) + versioned_content.introduction = introduction + versioned_content.conclusion = conclusion + + Repo.init(path, bare=False) repo = Repo(path) - file = open(os.path.join(path, 'manifest.json'), "w") - file.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - file.close() - file = open(os.path.join(path, tuto.introduction), "w") - file.write(real_content.encode('utf-8')) - file.close() - file = open(os.path.join(path, tuto.conclusion), "w") - file.write(real_content.encode('utf-8')) - file.close() - - repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + versioned_content.dump_json() + f = open(os.path.join(path, introduction), "w") + f.write(text_content.encode('utf-8')) + f.close() + f = open(os.path.join(path, conclusion), "w") + f.write(text_content.encode('utf-8')) + f.close() + repo.index.add(['manifest.json', introduction, conclusion]) cm = repo.index.commit("Init Tuto") - tuto.sha_draft = cm.hexsha - tuto.gallery = GalleryFactory() - for author in tuto.authors.all(): - UserGalleryFactory(user=author, gallery=tuto.gallery) - return tuto + publishable_content.sha_draft = cm.hexsha + publishable_content.sha_beta = None + publishable_content.gallery = GalleryFactory() + for author in publishable_content.authors.all(): + UserGalleryFactory(user=author, gallery=publishable_content.gallery) + return publishable_content -class PartFactory(factory.DjangoModelFactory): +class ContainerFactory(factory.Factory): FACTORY_FOR = Container - title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) + title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n+1)) + pk = factory.Sequence(lambda n: n+1) @classmethod def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - part = super(PartFactory, cls)._prepare(create, **kwargs) - parent = kwargs.pop('tutorial', None) - - real_content = content - if light: - real_content = content_light + db_object = kwargs.pop('db_object', None) + container = super(ContainerFactory, cls)._prepare(create, **kwargs) + container.parent.add_container(container) - path = part.get_path() - repo = Repo(part.tutorial.get_path()) + path = container.get_path() + repo = Repo(container.top_container().get_path()) if not os.path.isdir(path): os.makedirs(path, mode=0o777) - part.introduction = os.path.join(part.get_phy_slug(), 'introduction.md') - part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') - part.save() + container.introduction = os.path.join(container.get_path(relative=True), 'introduction.md') + container.conclusion = os.path.join(container.get_path(relative=True), 'conclusion.md') f = open(os.path.join(parent.get_path(), part.introduction), "w") - f.write(real_content.encode('utf-8')) + f.write(text_content.encode('utf-8')) f.close() - repo.index.add([part.introduction]) + repo.index.add([container.introduction]) f = open(os.path.join(parent.get_path(), part.conclusion), "w") - f.write(real_content.encode('utf-8')) + f.write(text_content.encode('utf-8')) f.close() - repo.index.add([part.conclusion]) + repo.index.add([container.conclusion]) if parent: parent.save() + repo.index.add(['manifest.json']) - man = export_tutorial(parent) - f = open(os.path.join(parent.get_path(), 'manifest.json'), "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - - repo.index.add(['manifest.json']) + cm = repo.index.commit("Add container") - cm = repo.index.commit("Init Part") + if db_object: + db_object.sha_draft = cm.hexsha + db_object.save() - if parent: - parent.sha_draft = cm.hexsha - parent.save() + return container - return part +class ExtractFactory(factory.Factory): + FACTORY_FOR = Extract -class ChapterFactory(factory.DjangoModelFactory): - FACTORY_FOR = Container - - title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) + title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n+1)) + pk = factory.Sequence(lambda n: n+1) @classmethod def _prepare(cls, create, **kwargs): + db_object = kwargs.pop('db_object', None) + extract = super(ExtractFactory, cls)._prepare(create, **kwargs) + extract.container.add_extract(extract) - light = kwargs.pop('light', False) - chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) - parent = kwargs.pop('part', None) - - real_content = content - if light: - real_content = content_light - path = chapter.get_path() - - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - if parent: - chapter.introduction = os.path.join( - parent.get_phy_slug(), - chapter.get_phy_slug(), - 'introduction.md') - chapter.conclusion = os.path.join( - parent.get_phy_slug(), - chapter.get_phy_slug(), - 'conclusion.md') - chapter.save() - f = open( - os.path.join( - parent.tutorial.get_path(), - chapter.introduction), - "w") - f.write(real_content.encode('utf-8')) - f.close() - f = open( - os.path.join( - parent.tutorial.get_path(), - chapter.conclusion), - "w") - f.write(real_content.encode('utf-8')) - f.close() - parent.tutorial.save() - repo = Repo(parent.tutorial.get_path()) - - man = export_tutorial(parent.tutorial) - f = open( - os.path.join( - parent.parent.get_path(), - 'manifest.json'), - "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - - repo.index.add([chapter.introduction, chapter.conclusion]) - repo.index.add(['manifest.json']) - - cm = repo.index.commit("Init Chapter") + extract.text = extract.get_path(relative=True) - if parent: - parent.parent.sha_draft = cm.hexsha - parent.parent.save() - parent.save() - chapter.parent = parent - - return chapter + top_container = extract.container.top_container() + repo = Repo(top_container.get_path()) + f = open(extract.get_path(), 'w') + f.write(text_content.encode('utf-8')) + f.close() + repo.index.add([extract.text]) -class ExtractFactory(factory.DjangoModelFactory): - FACTORY_FOR = Extract + top_container.dump_json() + repo.index.add(['manifest.json']) - title = factory.Sequence(lambda n: 'Mon Extrait No{0}'.format(n)) + cm = repo.index.commit("Add extract") - @classmethod - def _prepare(cls, create, **kwargs): - extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - container = kwargs.pop('container', None) - if container: - if container.parent is PublishableContent: - container.parent.sha_draft = 'EXTRACT-AAAA' - container.parent.save() - elif container.parent.parent is PublishableContent: - container.parent.parent.sha_draft = 'EXTRACT-AAAA' - container.parent.parent.tutorial.save() + if db_object: + db_object.sha_draft = cm.hexsha + db_object.save() return extract -class NoteFactory(factory.DjangoModelFactory): +class ContentReactionFactory(factory.DjangoModelFactory): FACTORY_FOR = ContentReaction ip_address = '192.168.3.1' @@ -288,13 +145,13 @@ class NoteFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): - note = super(NoteFactory, cls)._prepare(create, **kwargs) + note = super(ContentReactionFactory, cls)._prepare(create, **kwargs) note.pubdate = datetime.now() note.save() - tutorial = kwargs.pop('tutorial', None) - if tutorial: - tutorial.last_note = note - tutorial.save() + content = kwargs.pop('tutorial', None) + if content: + content.last_note = note + content.save() return note @@ -321,17 +178,4 @@ def _prepare(cls, create, **kwargs): licence = super(LicenceFactory, cls)._prepare(create, **kwargs) return licence - -class PublishedMiniTutorial(MiniTutorialFactory): - FACTORY_FOR = PublishableContent - - @classmethod - def _prepare(cls, create, **kwargs): - tutorial = super(PublishedMiniTutorial, cls)._prepare(create, **kwargs) - tutorial.pubdate = datetime.now() - tutorial.sha_public = tutorial.sha_draft - tutorial.source = '' - tutorial.sha_validation = None - mep(tutorial, tutorial.sha_draft) - tutorial.save() - return tutorial + FACTORY_FOR = PublishableContent \ No newline at end of file diff --git a/zds/tutorialv2/migrations/0001_initial.py b/zds/tutorialv2/migrations/0001_initial.py index 5dca0239ca..8c4b758aec 100644 --- a/zds/tutorialv2/migrations/0001_initial.py +++ b/zds/tutorialv2/migrations/0001_initial.py @@ -8,22 +8,10 @@ class Migration(SchemaMigration): def forwards(self, orm): - # Adding model 'Container' - db.create_table(u'tutorialv2_container', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), - ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), - ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), - )) - db.send_create_signal(u'tutorialv2', ['Container']) - # Adding model 'PublishableContent' db.create_table(u'tutorialv2_publishablecontent', ( - (u'container_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), ('description', self.gf('django.db.models.fields.CharField')(max_length=200)), ('source', self.gf('django.db.models.fields.CharField')(max_length=200)), ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Image'], null=True, on_delete=models.SET_NULL, blank=True)), @@ -37,7 +25,7 @@ def forwards(self, orm): ('sha_draft', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), ('licence', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['utils.Licence'], null=True, blank=True)), ('type', self.gf('django.db.models.fields.CharField')(max_length=10, db_index=True)), - ('images', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('relative_images_path', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), ('last_note', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='last_note', null=True, to=orm['tutorialv2.ContentReaction'])), ('is_locked', self.gf('django.db.models.fields.BooleanField')(default=False)), ('js_support', self.gf('django.db.models.fields.BooleanField')(default=False)), @@ -62,6 +50,15 @@ def forwards(self, orm): )) db.create_unique(m2m_table_name, ['publishablecontent_id', 'subcategory_id']) + # Adding M2M table for field helps on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_helps') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('helpwriting', models.ForeignKey(orm[u'utils.helpwriting'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'helpwriting_id']) + # Adding model 'ContentReaction' db.create_table(u'tutorialv2_contentreaction', ( (u'comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['utils.Comment'], unique=True, primary_key=True)), @@ -72,26 +69,16 @@ def forwards(self, orm): # Adding model 'ContentRead' db.create_table(u'tutorialv2_contentread', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), + ('content', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), ('note', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.ContentReaction'])), ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_notes_read', to=orm['auth.User'])), )) db.send_create_signal(u'tutorialv2', ['ContentRead']) - # Adding model 'Extract' - db.create_table(u'tutorialv2_extract', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), - ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), - ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - )) - db.send_create_signal(u'tutorialv2', ['Extract']) - # Adding model 'Validation' db.create_table(u'tutorialv2_validation', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), + ('content', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), ('version', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), ('date_proposition', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), ('comment_authors', self.gf('django.db.models.fields.TextField')()), @@ -105,9 +92,6 @@ def forwards(self, orm): def backwards(self, orm): - # Deleting model 'Container' - db.delete_table(u'tutorialv2_container') - # Deleting model 'PublishableContent' db.delete_table(u'tutorialv2_publishablecontent') @@ -117,15 +101,15 @@ def backwards(self, orm): # Removing M2M table for field subcategory on 'PublishableContent' db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_subcategory')) + # Removing M2M table for field helps on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_helps')) + # Deleting model 'ContentReaction' db.delete_table(u'tutorialv2_contentreaction') # Deleting model 'ContentRead' db.delete_table(u'tutorialv2_contentread') - # Deleting model 'Extract' - db.delete_table(u'tutorialv2_extract') - # Deleting model 'Validation' db.delete_table(u'tutorialv2_validation') @@ -187,17 +171,6 @@ def backwards(self, orm): 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, - u'tutorialv2.container': { - 'Meta': {'object_name': 'Container'}, - 'compatibility_pk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'conclusion': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'introduction': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'position_in_parent': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, u'tutorialv2.contentreaction': { 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), @@ -205,39 +178,33 @@ def backwards(self, orm): }, u'tutorialv2.contentread': { 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), - 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) }, - u'tutorialv2.extract': { - 'Meta': {'object_name': 'Extract'}, - 'container': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'position_in_container': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), - 'text': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, u'tutorialv2.publishablecontent': { - 'Meta': {'object_name': 'PublishableContent', '_ormbases': [u'tutorialv2.Container']}, + 'Meta': {'object_name': 'PublishableContent'}, 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), - u'container_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['tutorialv2.Container']", 'unique': 'True', 'primary_key': 'True'}), 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'helps': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['utils.HelpWriting']", 'db_index': 'True', 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'images': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, @@ -245,12 +212,12 @@ def backwards(self, orm): 'Meta': {'object_name': 'Validation'}, 'comment_authors': ('django.db.models.fields.TextField', [], {}), 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), - 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) }, @@ -270,6 +237,14 @@ def backwards(self, orm): 'text_html': ('django.db.models.fields.TextField', [], {}), 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, + u'utils.helpwriting': { + 'Meta': {'object_name': 'HelpWriting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '20'}), + 'tablelabel': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, u'utils.licence': { 'Meta': {'object_name': 'Licence'}, 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), diff --git a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py deleted file mode 100644 index a979a2e9df..0000000000 --- a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py +++ /dev/null @@ -1,262 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Deleting model 'Container' - db.delete_table(u'tutorialv2_container') - - # Deleting model 'Extract' - db.delete_table(u'tutorialv2_extract') - - # Deleting field 'Validation.tutorial' - db.delete_column(u'tutorialv2_validation', 'tutorial_id') - - # Adding field 'Validation.content' - db.add_column(u'tutorialv2_validation', 'content', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), - keep_default=False) - - # Deleting field 'PublishableContent.container_ptr' - db.delete_column(u'tutorialv2_publishablecontent', u'container_ptr_id') - - # Deleting field 'PublishableContent.images' - db.delete_column(u'tutorialv2_publishablecontent', 'images') - - # Adding field 'PublishableContent.id' - db.add_column(u'tutorialv2_publishablecontent', u'id', - self.gf('django.db.models.fields.AutoField')(default=0, primary_key=True), - keep_default=False) - - # Adding field 'PublishableContent.title' - db.add_column(u'tutorialv2_publishablecontent', 'title', - self.gf('django.db.models.fields.CharField')(default='', max_length=80), - keep_default=False) - - # Adding field 'PublishableContent.relative_images_path' - db.add_column(u'tutorialv2_publishablecontent', 'relative_images_path', - self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), - keep_default=False) - - # Deleting field 'ContentRead.tutorial' - db.delete_column(u'tutorialv2_contentread', 'tutorial_id') - - # Adding field 'ContentRead.content' - db.add_column(u'tutorialv2_contentread', 'content', - self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['tutorialv2.PublishableContent']), - keep_default=False) - - - def backwards(self, orm): - # Adding model 'Container' - db.create_table(u'tutorialv2_container', ( - ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - )) - db.send_create_signal(u'tutorialv2', ['Container']) - - # Adding model 'Extract' - db.create_table(u'tutorialv2_extract', ( - ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - )) - db.send_create_signal(u'tutorialv2', ['Extract']) - - # Adding field 'Validation.tutorial' - db.add_column(u'tutorialv2_validation', 'tutorial', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), - keep_default=False) - - # Deleting field 'Validation.content' - db.delete_column(u'tutorialv2_validation', 'content_id') - - - # User chose to not deal with backwards NULL issues for 'PublishableContent.container_ptr' - raise RuntimeError("Cannot reverse this migration. 'PublishableContent.container_ptr' and its values cannot be restored.") - - # The following code is provided here to aid in writing a correct migration # Adding field 'PublishableContent.container_ptr' - db.add_column(u'tutorialv2_publishablecontent', u'container_ptr', - self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True), - keep_default=False) - - # Adding field 'PublishableContent.images' - db.add_column(u'tutorialv2_publishablecontent', 'images', - self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), - keep_default=False) - - # Deleting field 'PublishableContent.id' - db.delete_column(u'tutorialv2_publishablecontent', u'id') - - # Deleting field 'PublishableContent.title' - db.delete_column(u'tutorialv2_publishablecontent', 'title') - - # Deleting field 'PublishableContent.relative_images_path' - db.delete_column(u'tutorialv2_publishablecontent', 'relative_images_path') - - - # User chose to not deal with backwards NULL issues for 'ContentRead.tutorial' - raise RuntimeError("Cannot reverse this migration. 'ContentRead.tutorial' and its values cannot be restored.") - - # The following code is provided here to aid in writing a correct migration # Adding field 'ContentRead.tutorial' - db.add_column(u'tutorialv2_contentread', 'tutorial', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent']), - keep_default=False) - - # Deleting field 'ContentRead.content' - db.delete_column(u'tutorialv2_contentread', 'content_id') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'gallery.gallery': { - 'Meta': {'object_name': 'Gallery'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'gallery.image': { - 'Meta': {'object_name': 'Image'}, - 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'tutorialv2.contentreaction': { - 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, - u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), - 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) - }, - u'tutorialv2.contentread': { - 'Meta': {'object_name': 'ContentRead'}, - 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) - }, - u'tutorialv2.publishablecontent': { - 'Meta': {'object_name': 'PublishableContent'}, - 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), - 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), - 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), - 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), - 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), - 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), - 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'tutorialv2.validation': { - 'Meta': {'object_name': 'Validation'}, - 'comment_authors': ('django.db.models.fields.TextField', [], {}), - 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), - 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), - 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), - 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), - 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) - }, - u'utils.comment': { - 'Meta': {'object_name': 'Comment'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), - 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), - 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'text': ('django.db.models.fields.TextField', [], {}), - 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), - 'text_html': ('django.db.models.fields.TextField', [], {}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'utils.licence': { - 'Meta': {'object_name': 'Licence'}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'description': ('django.db.models.fields.TextField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, - u'utils.subcategory': { - 'Meta': {'object_name': 'SubCategory'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - } - } - - complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 16d9ea5d53..46ed6ac433 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -60,7 +60,6 @@ class Container: pk = 0 title = '' - slug = '' introduction = None conclusion = None parent = None @@ -72,7 +71,6 @@ class Container: def __init__(self, pk, title, parent=None, position_in_parent=1): self.pk = pk self.title = title - self.slug = slugify(title) self.parent = parent self.position_in_parent = position_in_parent self.children = [] # even if you want, do NOT remove this line @@ -159,7 +157,13 @@ def get_phy_slug(self): """ :return: the physical slug, used to represent data in filesystem """ - return str(self.pk) + '_' + self.slug + return str(self.pk) + '_' + slugify(self.title) + + def slug(self): + """ + :return: slug of the object, based on title + """ + return slugify(self.title) def update_children(self): """ @@ -302,6 +306,12 @@ def get_phy_slug(self): """ return str(self.pk) + '_' + slugify(self.title) + def slug(self): + """ + :return: slug of the object, based on title + """ + return slugify(self.title) + def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. @@ -318,12 +328,18 @@ def get_prod_path(self): return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' def get_text(self): + """ + :return: versioned text + """ if self.text: return get_blob( self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) def get_text_online(self): + """ + :return: public text of the extract + """ path = self.container.top_container().get_prod_path() + self.text + '.html' if os.path.exists(path): txt = open(path) @@ -346,7 +362,7 @@ class VersionedContent(Container): description = '' type = '' - licence = '' + licence = None # Information from DB sha_draft = None @@ -438,6 +454,29 @@ def dump_json(self, path=None): json_data.close() +def fill_container_from_json(json_sub, parent): + """ + Function which call itself to fill container + :param json_sub: dictionary from "manifest.json" + :param parent: the container to fill + """ + if 'children' in json_sub: + for child in json_sub['children']: + if child['obj_type'] == 'container': + new_container = Container(child['pk'], child['title']) + new_container.introduction = child['introduction'] + new_container.conclusion = child['conclusion'] + parent.add_container(new_container) + if 'children' in child: + fill_container_from_json(child, new_container) + elif child['obj_type'] == 'extract': + new_extract = Extract(child['pk'], child['title']) + new_extract.text = child['text'] + parent.add_extract(new_extract) + else: + raise Exception('Unknown object type'+child['obj_type']) + + class PublishableContent(models.Model): """ A tutorial whatever its size or an article. @@ -450,8 +489,6 @@ class PublishableContent(models.Model): - Public, beta, validation and draft sha, for versioning ; - Comment support ; - Type, which is either "ARTICLE" or "TUTORIAL" - - These are two repositories : draft and online. """ class Meta: verbose_name = 'Contenu' @@ -515,6 +552,24 @@ class Meta: def __unicode__(self): return self.title + def get_phy_slug(self): + """ + :return: physical slug, used for filesystem representation + """ + return str(self.pk) + '_' + self.slug + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + if relative: + return '' + else: + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + def in_beta(self): """ A tutorial is not in beta if sha_beta is `None` or empty @@ -556,21 +611,48 @@ def is_tutorial(self): """ return self.type == 'TUTORIAL' - def load_json_for_public(self, sha=None): + def load_version(self, sha=None, public=False): """ - Fetch the public version of the JSON file for this content. + Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if + `public` is `True`). + Note: for practical reason, the returned object is filled with information form DB. :param sha: version - :return: a dictionary containing the structure of the JSON file. + :param public: if `True`, use `sha_public` instead of `sha_draft` if `sha` is `None` + :return: the versioned content """ + # load the good manifest.json if sha is None: - sha = self.sha_public - repo = Repo(self.get_path()) # should be `get_prod_path()` !?! - mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') - data = json_reader.loads(mantuto) - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data - # TODO: mix that with next function + if not public: + sha = self.sha_draft + else: + sha = self.sha_public + path = os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + repo = Repo(path) + data = get_blob(repo.commit(sha).tree, 'manifest.json') + json = json_reader.loads(data) + + # create and fill the container + versioned = VersionedContent(sha, self.pk, self.type, json['title']) + if 'version' in json and json['version'] == 2: + # fill metadata : + versioned.description = json['description'] + if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': + versioned.type = json['type'] + else: + versioned.type = self.type + if 'licence' in json: + versioned.licence = Licence.objects.filter(code=data['licence']).first() + versioned.introduction = json['introduction'] + versioned.conclusion = json['conclusion'] + # then, fill container with children + fill_container_from_json(json, versioned) + # TODO extra metadata from BDD + + else: + raise Exception('Importation of old version is not yet supported') + # TODO so here we can support old version !! + + return versioned def load_dic(self, mandata, sha=None): # TODO should load JSON and store it in VersionedContent @@ -582,12 +664,11 @@ def load_dic(self, mandata, sha=None): # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'get_path', 'in_beta', - 'in_validation', 'on_line' + 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public' ] attrs = [ - 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', + 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' ] @@ -598,30 +679,9 @@ def load_dic(self, mandata, sha=None): mandata[attr] = getattr(self, attr) # general information - mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.in_public() and self.sha_public == sha - - # url: - mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) - - if self.in_beta(): - mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorialv2.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + '?version=' + self.sha_beta - - else: - mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorialv2.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) - - mandata['get_absolute_url_online'] = reverse( - 'zds.tutorialv2.views.view_tutorial_online', - args=[self.pk, mandata['slug']] - ) + mandata['is_public'] = self.in_public() and self.sha_public == sha def save(self, *args, **kwargs): self.slug = slugify(self.title) diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index ca9f27c9ed..193513f183 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -3,12 +3,18 @@ # NOTE : this file is only there for tests purpose, it will be deleted in final version import os +import shutil +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from zds.settings import SITE_ROOT -from zds.tutorialv2.models import Container, Extract, VersionedContent +from zds.member.factories import ProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory +from zds.gallery.factories import GalleryFactory + +# from zds.tutorialv2.models import Container, Extract, VersionedContent @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @@ -18,15 +24,37 @@ class ContentTests(TestCase): def setUp(self): - self.start_version = 'ca5508a' # real version, adapt it ! - self.content = VersionedContent(self.start_version, 1, 'TUTORIAL', 'Mon tutoriel no1') + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + self.mas = ProfileFactory().user + settings.ZDS_APP['member']['bot_account'] = self.mas.username + + self.licence = LicenceFactory() + + self.user_author = ProfileFactory().user + self.tuto = PublishableContentFactory(type='TUTORIAL') + self.tuto.authors.add(self.user_author) + self.tuto.gallery = GalleryFactory() + self.tuto.licence = self.licence + self.tuto.save() - self.container = Container(1, 'Mon chapitre no1') - self.content.add_container(self.container) + self.tuto_draft = self.tuto.load_version() + self.chapter1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) - self.extract = Extract(1, 'Un premier extrait') - self.container.add_extract(self.extract) - self.content.update_children() + self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) def test_workflow_content(self): - print(self.container.position_in_parent) + versioned = self.tuto.load_version() + self.assertEqual(self.tuto_draft.title, versioned.title) + self.assertEqual(self.chapter1.title, versioned.children[0].title) + self.assertEqual(self.extract1.title, versioned.children[0].children[0].title) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): + shutil.rmtree(settings.ZDS_APP['tutorial']['repo_path']) + if os.path.isdir(settings.ZDS_APP['tutorial']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['tutorial']['repo_public_path']) + if os.path.isdir(settings.ZDS_APP['article']['repo_path']): + shutil.rmtree(settings.ZDS_APP['article']['repo_path']) + if os.path.isdir(settings.MEDIA_ROOT): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index c2b875b4c3..61db22d480 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -62,7 +62,7 @@ from django.utils.translation import ugettext as _ from django.views.generic import ListView, DetailView # , UpdateView # until we completely get rid of these, import them : -from zds.tutorial.models import Tutorial, Chapter, Part, HelpWriting +from zds.tutorial.models import Tutorial, Chapter, Part class ArticleList(ListView): diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py index 6959b0c60d..352be20a1c 100644 --- a/zds/utils/tutorialv2.py +++ b/zds/utils/tutorialv2.py @@ -8,6 +8,7 @@ def export_extract(extract): :return: dictionary containing the information """ dct = OrderedDict() + dct['obj_type'] = 'extract' dct['pk'] = extract.pk dct['title'] = extract.title dct['text'] = extract.text @@ -21,9 +22,9 @@ def export_container(container): :return: dictionary containing the information """ dct = OrderedDict() + dct['obj_type'] = "container" dct['pk'] = container.pk dct['title'] = container.title - dct['obj_type'] = "container" dct['introduction'] = container.introduction dct['conclusion'] = container.conclusion dct['children'] = [] @@ -47,6 +48,7 @@ def export_content(content): dct = export_container(content) # append metadata : + dct['version'] = 2 # to recognize old and new version of the content dct['description'] = content.description dct['type'] = content.type if content.licence: From 8ea7fd618cdf836e880943e8e55693809621ddd6 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Tue, 30 Dec 2014 20:41:34 +0100 Subject: [PATCH 016/887] MaJ : suppression du pk et ajout de slugs uniques --- zds/tutorial/factories.py | 18 +-- zds/tutorial/tests/tests.py | 44 +++--- zds/tutorial/views.py | 22 +-- zds/tutorialv2/factories.py | 26 ++-- zds/tutorialv2/models.py | 207 +++++++++++++++------------ zds/tutorialv2/tests/tests_models.py | 59 +++++++- zds/tutorialv2/views.py | 58 ++++---- zds/utils/tutorials.py | 6 +- zds/utils/tutorialv2.py | 8 +- 9 files changed, 259 insertions(+), 189 deletions(-) diff --git a/zds/tutorial/factories.py b/zds/tutorial/factories.py index dd9147ee72..3351339dea 100644 --- a/zds/tutorial/factories.py +++ b/zds/tutorial/factories.py @@ -47,7 +47,7 @@ def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) - path = tuto.get_path() + path = tuto.get_repo_path() real_content = content if light: real_content = content_light @@ -97,7 +97,7 @@ def _prepare(cls, create, **kwargs): if light: real_content = content_light - path = tuto.get_path() + path = tuto.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -144,8 +144,8 @@ def _prepare(cls, create, **kwargs): if light: real_content = content_light - path = part.get_path() - repo = Repo(part.tutorial.get_path()) + path = part.get_repo_path() + repo = Repo(part.tutorial.get_repo_path()) if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -202,7 +202,7 @@ def _prepare(cls, create, **kwargs): real_content = content if light: real_content = content_light - path = chapter.get_path() + path = chapter.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -235,25 +235,25 @@ def _prepare(cls, create, **kwargs): chapter.save() f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), chapter.introduction), "w") f.write(real_content.encode('utf-8')) f.close() f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), chapter.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() part.tutorial.save() - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) man = export_tutorial(part.tutorial) f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), 'manifest.json'), "w") f.write( diff --git a/zds/tutorial/tests/tests.py b/zds/tutorial/tests/tests.py index 51332aa49c..e31d91f75f 100644 --- a/zds/tutorial/tests/tests.py +++ b/zds/tutorial/tests/tests.py @@ -1045,8 +1045,8 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", 'msg_commit': u"Mise à jour de la partie", - "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + "last_hash": compute_hash([os.path.join(p2.tutorial.get_repo_path(), p2.introduction), + os.path.join(p2.tutorial.get_repo_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text=u"Partie 2 : edition de titre") @@ -1063,8 +1063,8 @@ def test_workflow_tuto(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"Mise à jour du chapitre", "last_hash": compute_hash([ - os.path.join(c3.get_path(), "introduction.md"), - os.path.join(c3.get_path(), "conclusion.md")]) + os.path.join(c3.get_repo_path(), "introduction.md"), + os.path.join(c3.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertContains(response=result, text=u"Chapitre 3 : edition de titre") @@ -1081,8 +1081,8 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : seconde edition d'introduction", 'conclusion': u"C'est terminé : seconde edition de conlusion", 'msg_commit': u"2nd Màj de la partie 2", - "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + "last_hash": compute_hash([os.path.join(p2.tutorial.get_repo_path(), p2.introduction), + os.path.join(p2.tutorial.get_repo_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text=u"Partie 2 : seconde edition de titre") @@ -1099,8 +1099,8 @@ def test_workflow_tuto(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"MàJ du chapitre 2", "last_hash": compute_hash([ - os.path.join(c2.get_path(), "introduction.md"), - os.path.join(c2.get_path(), "conclusion.md")]) + os.path.join(c2.get_repo_path(), "introduction.md"), + os.path.join(c2.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertContains(response=result, text=u"Chapitre 2 : edition de titre") @@ -1114,7 +1114,7 @@ def test_workflow_tuto(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"Agrume", - "last_hash": compute_hash([os.path.join(e2.get_path())]) + "last_hash": compute_hash([os.path.join(e2.get_repo_path())]) }, follow=True) self.assertContains(response=result, text=u"Extrait 2 : edition de titre") @@ -1561,8 +1561,8 @@ def test_conflict_does_not_destroy(self): }, follow=False) p1 = Part.objects.last() - hash = compute_hash([os.path.join(p1.tutorial.get_path(), p1.introduction), - os.path.join(p1.tutorial.get_path(), p1.conclusion)]) + hash = compute_hash([os.path.join(p1.tutorial.get_repo_path(), p1.introduction), + os.path.join(p1.tutorial.get_repo_path(), p1.conclusion)]) self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk), { @@ -1594,8 +1594,8 @@ def test_conflict_does_not_destroy(self): }, follow=False) c1 = Chapter.objects.last() - hash = compute_hash([os.path.join(c1.get_path(), "introduction.md"), - os.path.join(c1.get_path(), "conclusion.md")]) + hash = compute_hash([os.path.join(c1.get_repo_path(), "introduction.md"), + os.path.join(c1.get_repo_path(), "conclusion.md")]) self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk), { @@ -2571,8 +2571,8 @@ def test_change_update(self): 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", 'msg_commit': u"Changement de la partie", - "last_hash": compute_hash([os.path.join(part.tutorial.get_path(), part.introduction), - os.path.join(part.tutorial.get_path(), part.conclusion)]) + "last_hash": compute_hash([os.path.join(part.tutorial.get_repo_path(), part.introduction), + os.path.join(part.tutorial.get_repo_path(), part.conclusion)]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -2609,8 +2609,8 @@ def test_change_update(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"MàJ du chapitre 2 : le respect des agrumes sur ZdS", "last_hash": compute_hash([ - os.path.join(chapter.get_path(), "introduction.md"), - os.path.join(chapter.get_path(), "conclusion.md")]) + os.path.join(chapter.get_repo_path(), "introduction.md"), + os.path.join(chapter.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -2641,7 +2641,7 @@ def test_change_update(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"On ne torture pas les agrumes !", - "last_hash": compute_hash([os.path.join(extract.get_path())]) + "last_hash": compute_hash([os.path.join(extract.get_repo_path())]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -3027,7 +3027,7 @@ def test_add_extract_named_introduction(self): tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(Extract.objects.all().count(), 1) intro_path = os.path.join(tuto.get_path(), "introduction.md") - extract_path = Extract.objects.first().get_path() + extract_path = Extract.objects.first().get_repo_path() self.assertNotEqual(intro_path, extract_path) self.assertTrue(os.path.isfile(intro_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -3051,7 +3051,7 @@ def test_add_extract_named_conclusion(self): tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(Extract.objects.all().count(), 1) ccl_path = os.path.join(tuto.get_path(), "conclusion.md") - extract_path = Extract.objects.first().get_path() + extract_path = Extract.objects.first().get_repo_path() self.assertNotEqual(ccl_path, extract_path) self.assertTrue(os.path.isfile(ccl_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -3987,7 +3987,7 @@ def test_workflow_tuto(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"Edition d'introduction", - "last_hash": compute_hash([e2.get_path()]) + "last_hash": compute_hash([e2.get_repo_path()]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -4450,7 +4450,7 @@ def test_change_update(self): { 'title': u"Un autre titre", 'text': u"j'ai changé d'avis, je vais mettre un sapin synthétique", - "last_hash": compute_hash([extract.get_path()]) + "last_hash": compute_hash([extract.get_repo_path()]) }, follow=True) self.assertEqual(result.status_code, 200) diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index 7b945bafb4..ee492261fa 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -2338,9 +2338,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2731,7 +2731,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) index = repo.index # update the tutorial last edit date part.tutorial.update = datetime.now() @@ -2751,19 +2751,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = str(request.user.email) @@ -2836,10 +2836,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_path(), + man_path = os.path.join(chapter.part.tutorial.get_repo_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2906,14 +2906,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_path(relative=True)]) + index.add([extract.get_repo_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index e0e8a78330..2937933807 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -7,6 +7,7 @@ import factory from models import PublishableContent, Container, Extract, ContentReaction,\ +from zds.utils import slugify from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory @@ -24,7 +25,7 @@ class PublishableContentFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) - path = publishable_content.get_path() + path = publishable_content.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -33,9 +34,9 @@ def _prepare(cls, create, **kwargs): introduction = 'introduction.md' conclusion = 'conclusion.md' versioned_content = VersionedContent(None, - publishable_content.pk, publishable_content.type, - publishable_content.title) + publishable_content.title, + slugify(publishable_content.title)) versioned_content.introduction = introduction versioned_content.conclusion = conclusion @@ -64,16 +65,17 @@ class ContainerFactory(factory.Factory): FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n+1)) - pk = factory.Sequence(lambda n: n+1) + slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) container = super(ContainerFactory, cls)._prepare(create, **kwargs) - container.parent.add_container(container) + container.parent.add_container(container, generate_slug=True) path = container.get_path() repo = Repo(container.top_container().get_path()) + top_container = container.top_container() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -81,16 +83,15 @@ def _prepare(cls, create, **kwargs): container.introduction = os.path.join(container.get_path(relative=True), 'introduction.md') container.conclusion = os.path.join(container.get_path(relative=True), 'conclusion.md') - f = open(os.path.join(parent.get_path(), part.introduction), "w") + f = open(os.path.join(top_container.get_path(), container.introduction), "w") f.write(text_content.encode('utf-8')) f.close() - repo.index.add([container.introduction]) - f = open(os.path.join(parent.get_path(), part.conclusion), "w") + f = open(os.path.join(top_container.get_path(), container.conclusion), "w") f.write(text_content.encode('utf-8')) f.close() - repo.index.add([container.conclusion]) + repo.index.add([container.introduction, container.conclusion]) - if parent: + top_container.dump_json() parent.save() repo.index.add(['manifest.json']) @@ -107,16 +108,15 @@ class ExtractFactory(factory.Factory): FACTORY_FOR = Extract title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n+1)) - pk = factory.Sequence(lambda n: n+1) + slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - extract.container.add_extract(extract) + extract.container.add_extract(extract, generate_slug=True) extract.text = extract.get_path(relative=True) - top_container = extract.container.top_container() repo = Repo(top_container.get_path()) f = open(extract.get_path(), 'w') diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 46ed6ac433..1948ca584b 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -4,7 +4,7 @@ import shutil try: import ujson as json_reader -except: +except ImportError: try: import simplejson as json_reader except: @@ -58,22 +58,25 @@ class Container: A container could be either a tutorial/article, a part or a chapter. """ - pk = 0 title = '' + slug = '' introduction = None conclusion = None parent = None position_in_parent = 1 children = [] + children_dict = {} # TODO: thumbnails ? - def __init__(self, pk, title, parent=None, position_in_parent=1): - self.pk = pk + def __init__(self, title, slug='', parent=None, position_in_parent=1): self.title = title + self.slug = slug self.parent = parent self.position_in_parent = position_in_parent + self.children = [] # even if you want, do NOT remove this line + self.children_dict = {} def __unicode__(self): return u''.format(self.title) @@ -127,43 +130,43 @@ def top_container(self): current = current.parent return current - def add_container(self, container): + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. :param container: the new container + :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_extracts(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + if generate_slug: + container.slug = self.top_container().get_unique_slug(container.title) + else: + self.top_container().add_slug_to_pool(container.slug) container.parent = self container.position_in_parent = self.get_last_child_position() + 1 self.children.append(container) + self.children_dict[container.slug] = container else: raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") # TODO: limitation if article ? - def add_extract(self, extract): + def add_extract(self, extract, generate_slug=False): """ Add a child container, but only if no container were previously added :param extract: the new extract + :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_sub_containers(): + if generate_slug: + extract.slug = self.top_container().get_unique_slug(extract.title) + else: + self.top_container().add_slug_to_pool(extract.slug) extract.container = self extract.position_in_parent = self.get_last_child_position() + 1 self.children.append(extract) - - def get_phy_slug(self): - """ - :return: the physical slug, used to represent data in filesystem - """ - return str(self.pk) + '_' + slugify(self.title) - - def slug(self): - """ - :return: slug of the object, based on title - """ - return slugify(self.title) + self.children_dict[extract.slug] = extract def update_children(self): """ @@ -190,7 +193,7 @@ def get_path(self, relative=False): base = '' if self.parent: base = self.parent.get_path(relative=relative) - return os.path.join(base, self.get_phy_slug()) + return os.path.join(base, self.slug) def get_prod_path(self): """ @@ -201,7 +204,7 @@ def get_prod_path(self): base = '' if self.parent: base = self.parent.get_prod_path() - return os.path.join(base, self.get_phy_slug()) + return os.path.join(base, self.slug) def get_introduction(self): """ @@ -256,14 +259,14 @@ class Extract: """ title = '' + slug = '' container = None position_in_container = 1 text = None - pk = 0 - def __init__(self, pk, title, container=None, position_in_container=1): - self.pk = pk + def __init__(self, title, slug='', container=None, position_in_container=1): self.title = title + self.slug = slug self.container = container self.position_in_container = position_in_container @@ -300,32 +303,21 @@ def get_absolute_url_beta(self): slugify(self.title) ) - def get_phy_slug(self): - """ - :return: the physical slug - """ - return str(self.pk) + '_' + slugify(self.title) - - def slug(self): - """ - :return: slug of the object, based on title - """ - return slugify(self.title) - def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ - return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + return os.path.join(self.container.get_path(relative=relative), self.slug) + '.md' def get_prod_path(self): """ Get the physical path to the public version of a specific version of the extract. :return: physical path """ - return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' + return os.path.join(self.container.get_prod_path(), self.slug) + '.md.html' + # TODO: should this function exists ? (there will be no public version of a text, all in parent container) def get_text(self): """ @@ -350,7 +342,7 @@ def get_text_online(self): class VersionedContent(Container): """ - This class is used to handle a specific version of a tutorial. + This class is used to handle a specific version of a tutorial.tutorial It is created from the "manifest.json" file, and could dump information in it. @@ -360,11 +352,14 @@ class VersionedContent(Container): current_version = None repository = None + # Metadata from json : description = '' type = '' licence = None - # Information from DB + slug_pool = [] + + # Metadata from DB : sha_draft = None sha_beta = None sha_public = None @@ -372,15 +367,30 @@ class VersionedContent(Container): is_beta = False is_validation = False is_public = False - - # TODO `load_dic()` provide more information, actually - - def __init__(self, current_version, pk, _type, title): - Container.__init__(self, pk, title) + in_beta = False + in_validation = False + in_public = False + + have_markdown = False + have_html = False + have_pdf = False + have_epub = False + + authors = None + subcategory = None + image = None + creation_date = None + pubdate = None + update_date = None + source = None + + def __init__(self, current_version, _type, title, slug): + Container.__init__(self, title, slug) self.current_version = current_version self.type = _type self.repository = Repo(self.get_path()) - # so read JSON ? + + self.slug_pool = ['introduction', 'conclusion', slug] # forbidden slugs def __unicode__(self): return self.title @@ -389,13 +399,13 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.slug]) def get_absolute_url_online(self): """ :return: the url to access the tutorial when online """ - return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.slug]) def get_absolute_url_beta(self): """ @@ -410,7 +420,33 @@ def get_edit_url(self): """ :return: the url to edit the tutorial """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) + + def get_unique_slug(self, title): + """ + Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a + "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. + :param title: title from which the slug is generated (with `slugify()`) + :return: the unique slug + """ + new_slug = slugify(title) + if new_slug in self.slug_pool: + num = 1 + while new_slug + '-' + str(num) in self.slug_pool: + num += 1 + new_slug = new_slug + '-' + str(num) + self.slug_pool.append(new_slug) + return new_slug + + def add_slug_to_pool(self, slug): + """ + Add a slug to the slug pool to be taken into account when generate a unique slug + :param slug: the slug to add + """ + if slug not in self.slug_pool: + self.slug_pool.append(slug) + else: + raise Exception('slug {} already in the slug pool !'.format(slug)) def get_path(self, relative=False): """ @@ -422,14 +458,14 @@ def get_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) def get_prod_path(self): """ Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.slug) def get_json(self): """ @@ -454,27 +490,29 @@ def dump_json(self, path=None): json_data.close() -def fill_container_from_json(json_sub, parent): +def fill_containers_from_json(json_sub, parent): """ Function which call itself to fill container :param json_sub: dictionary from "manifest.json" :param parent: the container to fill """ + # TODO should be static function of `VersionedContent` + # TODO should implement fallbacks if 'children' in json_sub: for child in json_sub['children']: - if child['obj_type'] == 'container': - new_container = Container(child['pk'], child['title']) + if child['object'] == 'container': + new_container = Container(child['title'], child['slug']) new_container.introduction = child['introduction'] new_container.conclusion = child['conclusion'] parent.add_container(new_container) if 'children' in child: - fill_container_from_json(child, new_container) - elif child['obj_type'] == 'extract': - new_extract = Extract(child['pk'], child['title']) + fill_containers_from_json(child, new_container) + elif child['object'] == 'extract': + new_extract = Extract(child['title'], child['slug']) new_extract.text = child['text'] parent.add_extract(new_extract) else: - raise Exception('Unknown object type'+child['obj_type']) + raise Exception('Unknown object type'+child['object']) class PublishableContent(models.Model): @@ -552,15 +590,9 @@ class Meta: def __unicode__(self): return self.title - def get_phy_slug(self): - """ - :return: physical slug, used for filesystem representation - """ - return str(self.pk) + '_' + self.slug - - def get_path(self, relative=False): + def get_repo_path(self, relative=False): """ - Get the physical path to the draft version of the Content. + Get the path to the tutorial repository :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ @@ -568,7 +600,7 @@ def get_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) def in_beta(self): """ @@ -596,7 +628,6 @@ def in_public(self): A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise """ - # TODO: for the logic with previous method, why not `in_public()` ? return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_article(self): @@ -626,13 +657,13 @@ def load_version(self, sha=None, public=False): sha = self.sha_draft else: sha = self.sha_public - path = os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + path = self.get_repo_path() repo = Repo(path) data = get_blob(repo.commit(sha).tree, 'manifest.json') json = json_reader.loads(data) # create and fill the container - versioned = VersionedContent(sha, self.pk, self.type, json['title']) + versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: # fill metadata : versioned.description = json['description'] @@ -642,11 +673,12 @@ def load_version(self, sha=None, public=False): versioned.type = self.type if 'licence' in json: versioned.licence = Licence.objects.filter(code=data['licence']).first() + # TODO must default licence be enforced here ? versioned.introduction = json['introduction'] versioned.conclusion = json['conclusion'] # then, fill container with children - fill_container_from_json(json, versioned) - # TODO extra metadata from BDD + fill_containers_from_json(json, versioned) + self.insert_data_in_versioned(versioned) else: raise Exception('Importation of old version is not yet supported') @@ -654,37 +686,30 @@ def load_version(self, sha=None, public=False): return versioned - def load_dic(self, mandata, sha=None): - # TODO should load JSON and store it in VersionedContent + def insert_data_in_versioned(self, versioned): """ - Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. - :param mandata: a dictionary from JSON file - :param sha: current version, used to fill the `is_*` fields by comparison with the corresponding `sha_*` + Insert some additional data from database in a VersionedContent + :param versioned: the VersionedContent to fill """ - # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public' - ] - - attrs = [ - 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', - 'sha_validation', 'sha_public' + 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public', + 'authors', 'subcategory', 'image', 'creation_date', 'pubdate', 'update_date', 'source', 'sha_draft', + 'sha_beta', 'sha_validation', 'sha_public' ] - # load functions and attributs in tree + # load functions and attributs in `versioned` for fn in fns: - mandata[fn] = getattr(self, fn) - for attr in attrs: - mandata[attr] = getattr(self, attr) + setattr(versioned, fn, getattr(self, fn)) # general information - mandata['is_beta'] = self.in_beta() and self.sha_beta == sha - mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_public'] = self.in_public() and self.sha_public == sha + versioned.is_beta = self.in_beta() and self.sha_beta == versioned.current_version + versioned.is_validation = self.in_validation() and self.sha_validation == versioned.current_version + versioned.is_public = self.in_public() and self.sha_public == versioned.current_version def save(self, *args, **kwargs): self.slug = slugify(self.title) + # TODO ensure unique slug here !! super(PublishableContent, self).save(*args, **kwargs) @@ -803,7 +828,7 @@ def delete_entity_and_tree(self): """ Delete the entities and their filesystem counterparts """ - shutil.rmtree(self.get_path(), False) + shutil.rmtree(self.get_repo_path(), False) Validation.objects.filter(tutorial=self).delete() if self.gallery is not None: diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 193513f183..9c2c3b1323 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -16,11 +16,14 @@ # from zds.tutorialv2.models import Container, Extract, VersionedContent +overrided_zds_app = settings.ZDS_APP +overrided_zds_app['tutorial']['repo_path'] = os.path.join(SITE_ROOT, 'tutoriels-private-test') +overrided_zds_app['tutorial']['repo__public_path'] = os.path.join(SITE_ROOT, 'tutoriels-public-test') +overrided_zds_app['article']['repo_path'] = os.path.join(SITE_ROOT, 'article-data-test') + @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) -@override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) -@override_settings(REPO_PATH_PROD=os.path.join(SITE_ROOT, 'tutoriels-public-test')) -@override_settings(REPO_ARTICLE_PATH=os.path.join(SITE_ROOT, 'articles-data-test')) +@override_settings(ZDS_APP=overrided_zds_app) class ContentTests(TestCase): def setUp(self): @@ -38,16 +41,58 @@ def setUp(self): self.tuto.save() self.tuto_draft = self.tuto.load_version() - self.chapter1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) - self.chapter2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto) self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) def test_workflow_content(self): + """ + General tests for a content + """ + # ensure the usability of manifest versioned = self.tuto.load_version() self.assertEqual(self.tuto_draft.title, versioned.title) - self.assertEqual(self.chapter1.title, versioned.children[0].title) - self.assertEqual(self.extract1.title, versioned.children[0].children[0].title) + self.assertEqual(self.part1.title, versioned.children[0].title) + self.assertEqual(self.extract1.title, versioned.children[0].children[0].children[0].title) + + # ensure url resolution project using dictionary : + self.assertTrue(self.part1.slug in versioned.children_dict.keys()) + self.assertTrue(self.chapter1.slug in versioned.children_dict[self.part1.slug].children_dict) + + def test_ensure_unique_slug(self): + """ + Ensure that slugs for a container or extract are always unique + """ + # get draft version + versioned = self.tuto.load_version() + + # forbidden slugs : + slug_to_test = ['introduction', # forbidden slug + 'conclusion', # forbidden slug + self.tuto.slug, # forbidden slug + # slug normally already in the slug pool : + self.part1.slug, + self.chapter1.slug, + self.extract1.slug] + + for slug in slug_to_test: + new_slug = versioned.get_unique_slug(slug) + self.assertNotEqual(slug, new_slug) + self.assertTrue(new_slug in versioned.slug_pool) # ensure new slugs are in slug pool + + # then test with "real" containers and extracts : + new_chapter_1 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) + # now, slug "aa" is forbidden ! + new_chapter_2 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) + self.assertNotEqual(new_chapter_1.slug, new_chapter_2.slug) + new_extract_1 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) + self.assertNotEqual(new_extract_1.slug, new_chapter_2.slug) + self.assertNotEqual(new_extract_1.slug, new_chapter_1.slug) + new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) + self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) + self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) + print(versioned.get_json()) def tearDown(self): if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 61db22d480..c1df7929cd 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -152,7 +152,7 @@ class DisplayContent(DetailView): def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["slug"] = slugify(dictionary["title"]) dictionary["position_in_tutorial"] = cpt_p @@ -167,7 +167,7 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["type"] = self.type dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") @@ -175,7 +175,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt - ext["path"] = content.get_path() + ext["path"] = content.get_repo_path() ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt += 1 @@ -233,7 +233,7 @@ def get_context_data(self, **kwargs): # Find the good manifest file - repo = Repo(content.get_path()) + repo = Repo(content.get_repo_path()) # Load the tutorial. @@ -293,7 +293,7 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied # open git repo and find diff between displayed version and head - repo = Repo(context[self.context_object_name].get_path()) + repo = Repo(context[self.context_object_name].get_repo_path()) current_version_commit = repo.commit(sha) diff_with_head = current_version_commit.diff("HEAD~1") context["path_add"] = diff_with_head.iter_change_type("A") @@ -319,7 +319,7 @@ def get_forms(self, context, content): def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["slug"] = slugify(dictionary["title"]) dictionary["position_in_tutorial"] = cpt_p @@ -522,7 +522,7 @@ def history(request, tutorial_pk, tutorial_slug): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) logs = repo.head.reference.log() logs = sorted(logs, key=attrgetter("time"), reverse=True) return render(request, "tutorial/tutorial/history.html", @@ -1162,7 +1162,7 @@ def add_tutorial(request): tutorial.save() maj_repo_tuto( request, - new_slug_path=tutorial.get_path(), + new_slug_path=tutorial.get_repo_path(), tuto=tutorial, introduction=data["introduction"], conclusion=data["conclusion"], @@ -1198,8 +1198,8 @@ def edit_tutorial(request): if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - introduction = os.path.join(tutorial.get_path(), "introduction.md") - conclusion = os.path.join(tutorial.get_path(), "conclusion.md") + introduction = os.path.join(tutorial.get_repo_path(), "introduction.md") + conclusion = os.path.join(tutorial.get_repo_path(), "conclusion.md") if request.method == "POST": form = TutorialForm(request.POST, request.FILES) if form.is_valid(): @@ -1221,7 +1221,7 @@ def edit_tutorial(request): "last_hash": compute_hash([introduction, conclusion]), "new_version": True }) - old_slug = tutorial.get_path() + old_slug = tutorial.get_repo_path() tutorial.title = data["title"] tutorial.description = data["description"] if "licence" in data and data["licence"] != "": @@ -1332,7 +1332,7 @@ def view_part( # find the good manifest file - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) manifest = get_blob(repo.commit(sha).tree, "manifest.json") mandata = json_reader.loads(manifest) tutorial.load_dic(mandata, sha=sha) @@ -1344,7 +1344,7 @@ def view_part( if part_pk == str(part["pk"]): find = True part["tutorial"] = tutorial - part["path"] = tutorial.get_path() + part["path"] = tutorial.get_repo_path() part["slug"] = slugify(part["title"]) part["position_in_tutorial"] = cpt_p part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) @@ -1352,7 +1352,7 @@ def view_part( cpt_c = 1 for chapter in part["chapters"]: chapter["part"] = part - chapter["path"] = tutorial.get_path() + chapter["path"] = tutorial.get_repo_path() chapter["slug"] = slugify(chapter["title"]) chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c @@ -1361,7 +1361,7 @@ def view_part( for ext in chapter["extracts"]: ext["chapter"] = chapter ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() + ext["path"] = tutorial.get_repo_path() cpt_e += 1 cpt_c += 1 final_part = part @@ -1677,7 +1677,7 @@ def view_chapter( # find the good manifest file - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) manifest = get_blob(repo.commit(sha).tree, "manifest.json") mandata = json_reader.loads(manifest) tutorial.load_dic(mandata, sha=sha) @@ -1701,7 +1701,7 @@ def view_chapter( part["tutorial"] = tutorial for chapter in part["chapters"]: chapter["part"] = part - chapter["path"] = tutorial.get_path() + chapter["path"] = tutorial.get_repo_path() chapter["slug"] = slugify(chapter["title"]) chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c @@ -1719,7 +1719,7 @@ def view_chapter( for ext in chapter["extracts"]: ext["chapter"] = chapter ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() + ext["path"] = tutorial.get_repo_path() ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt_e += 1 chapter_tab.append(chapter) @@ -2371,9 +2371,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2763,7 +2763,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) index = repo.index if action == "del": shutil.rmtree(old_slug_path) @@ -2781,19 +2781,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = str(request.user.email) @@ -2862,10 +2862,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_path(), + man_path = os.path.join(chapter.part.tutorial.get_repo_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2927,14 +2927,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_path(relative=True)]) + index.add([extract.get_repo_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) diff --git a/zds/utils/tutorials.py b/zds/utils/tutorials.py index 9a8d792250..51bf9ad317 100644 --- a/zds/utils/tutorials.py +++ b/zds/utils/tutorials.py @@ -187,7 +187,7 @@ def export_tutorial_to_md(tutorial, sha=None): cpt_p = 1 for part in parts: part['tutorial'] = tutorial - part['path'] = tutorial.get_path() + part['path'] = tutorial.get_repo_path() part['slug'] = slugify(part['title']) part['position_in_tutorial'] = cpt_p intro = open( @@ -208,7 +208,7 @@ def export_tutorial_to_md(tutorial, sha=None): cpt_c = 1 for chapter in part['chapters']: chapter['part'] = part - chapter['path'] = tutorial.get_path() + chapter['path'] = tutorial.get_repo_path() chapter['slug'] = slugify(chapter['title']) chapter['type'] = 'BIG' chapter['position_in_part'] = cpt_c @@ -230,7 +230,7 @@ def export_tutorial_to_md(tutorial, sha=None): for ext in chapter['extracts']: ext['chapter'] = chapter ext['position_in_chapter'] = cpt_e - ext['path'] = tutorial.get_path() + ext['path'] = tutorial.get_repo_path() text = open( os.path.join( tutorial.get_prod_path(sha), diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py index 352be20a1c..15718e4605 100644 --- a/zds/utils/tutorialv2.py +++ b/zds/utils/tutorialv2.py @@ -8,8 +8,8 @@ def export_extract(extract): :return: dictionary containing the information """ dct = OrderedDict() - dct['obj_type'] = 'extract' - dct['pk'] = extract.pk + dct['object'] = 'extract' + dct['slug'] = extract.slug dct['title'] = extract.title dct['text'] = extract.text return dct @@ -22,8 +22,8 @@ def export_container(container): :return: dictionary containing the information """ dct = OrderedDict() - dct['obj_type'] = "container" - dct['pk'] = container.pk + dct['object'] = "container" + dct['slug'] = container.slug dct['title'] = container.title dct['introduction'] = container.introduction dct['conclusion'] = container.conclusion From 22c69f072cd685350352cb1ab53fb2db450550e6 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 1 Jan 2015 21:11:36 +0100 Subject: [PATCH 017/887] views.py + fallbacks --- templates/tutorialv2/base.html | 104 ++++++ templates/tutorialv2/view.html | 486 +++++++++++++++++++++++++++ zds/tutorialv2/models.py | 79 +++-- zds/tutorialv2/tests/tests_models.py | 1 - zds/tutorialv2/urls.py | 191 +++++------ zds/tutorialv2/views.py | 61 ++-- zds/urls.py | 1 + 7 files changed, 766 insertions(+), 157 deletions(-) create mode 100644 templates/tutorialv2/base.html create mode 100644 templates/tutorialv2/view.html diff --git a/templates/tutorialv2/base.html b/templates/tutorialv2/base.html new file mode 100644 index 0000000000..73ef8519d1 --- /dev/null +++ b/templates/tutorialv2/base.html @@ -0,0 +1,104 @@ +{% extends "base_content_page.html" %} +{% load captureas %} +{% load i18n %} + + +{% block title_base %} + • {% trans "Tutoriels" %} +{% endblock %} + + + +{% block mobile_title %} + {% trans "Tutoriels" %} +{% endblock %} + + + +{% block breadcrumb_base %} + {% if user in tutorial.authors.all %} +
  • {% trans "Mes tutoriels" %}
  • + {% else %} +
  • + +
  • + {% endif %} +{% endblock %} + + + +{% block menu_tutorial %} + current +{% endblock %} + + + +{% block sidebar %} + +{% endblock %} diff --git a/templates/tutorialv2/view.html b/templates/tutorialv2/view.html new file mode 100644 index 0000000000..c53cf43273 --- /dev/null +++ b/templates/tutorialv2/view.html @@ -0,0 +1,486 @@ +{% extends "tutorialv2/base.html" %} +{% load emarkdown %} +{% load repo_reader %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load roman %} +{% load i18n %} + + +{% block title %} + {{ tutorial.title }} +{% endblock %} + + + +{% block breadcrumb %} +
  • {{ tutorial.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if tutorial.licence %} +

    + {{ tutorial.licence }} +

    + {% endif %} + +

    + {% if tutorial.image %} + + {% endif %} + {{ tutorial.title }} +

    + + {% if tutorial.description %} +

    + {{ tutorial.description }} +

    + {% endif %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial add_author=True %} + {% else %} + {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial %} + {% endif %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if tutorial.in_validation %} + {% if validation.version == version %} + {% if validation.is_pending %} +

    + {% trans "Ce tutoriel est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + {% trans "Le tutoriel est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% if validation.comment_authors %} +
    +

    + {% trans "Le message suivant a été laissé à destination des validateurs" %} : +

    + +
    + {{ validation.comment_authors|emarkdown }} +
    +
    + {% endif %} + {% else %} + {% if validation.is_pending %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if tutorial.is_beta %} +
    +
    + {% blocktrans %} + Cette version du tutoriel est en "BÊTA" ! + {% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + +{% block content %} + {% if tutorial.get_introduction and tutorial.get_introduction != "None" %} + {{ tutorial.get_introduction|emarkdown:is_js }} + {% elif not tutorial.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + {% if tutorial.is_mini %} + {# Small tutorial #} + + {% include "tutorial/includes/chapter.part.html" with authors=tutorial.authors.all %} + {% else %} + {# Large tutorial #} + +
    + + {% for part in parts %} +

    + + {{ part.position_in_tutorial|roman }} - {{ part.title }} + +

    + {% include "tutorial/includes/part.part.html" %} + {% empty %} +

    + {% trans "Il n'y a actuellement aucune partie dans ce tutoriel" %}. +

    + {% endfor %} + +
    + + {% endif %} + + {% if tutorial.get_conclusion and tutorial.get_conclusion != "None" %} + {{ tutorial.get_conclusion|emarkdown:is_js }} + {% elif not tutorial.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if tutorial.sha_draft = version %} + {% if not tutorial.is_mini %} + + {% trans "Ajouter une partie" %} + + {% else %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + + + {% trans "Éditer" %} + + {% else %} + + {% trans "Version brouillon" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} +
  • + + {% trans "Ajouter un auteur" %} + + +
  • +
  • + + {% trans "Gérer les auteurs" %} + + +
  • + + {% if tutorial.sha_public %} +
  • + + {% blocktrans %} + Voir la version en ligne + {% endblocktrans %} + +
  • + {% endif %} + + {% if not tutorial.in_beta %} +
  • + + {% trans "Mettre cette version en bêta" %} + + +
  • + {% else %} + {% if not tutorial.is_beta %} +
  • + + {% blocktrans %} + Voir la version en bêta + {% endblocktrans %} + +
  • +
  • + + {% trans "Mettre à jour la bêta avec cette version" %} + + +
  • + {% else %} +
  • + + {% trans "Cette version est déjà en bêta" %} + +
  • + {% endif %} +
  • + + {% trans "Désactiver la bêta" %} + + +
  • + {% endif %} + +
  • + + {% trans "Historique des versions" %} + +
  • + + {% if not tutorial.in_validation %} +
  • + + {% trans "Demander la validation" %} + +
  • + {% else %} + {% if not tutorial.is_validation %} +
  • + + {% trans "Mettre à jour la version en validation" %} + +
  • + {% endif %} +
  • + {% trans "En attente de validation" %} +
  • + {% endif %} + + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {% if perms.tutorial.change_tutorial %} + + {% endif %} + + {% include "tutorial/includes/summary.part.html" %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + + {% endif %} +{% endblock %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 1948ca584b..cd6af0f5ae 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -357,7 +357,7 @@ class VersionedContent(Container): type = '' licence = None - slug_pool = [] + slug_pool = {} # Metadata from DB : sha_draft = None @@ -390,7 +390,7 @@ def __init__(self, current_version, _type, title, slug): self.type = _type self.repository = Repo(self.get_path()) - self.slug_pool = ['introduction', 'conclusion', slug] # forbidden slugs + self.slug_pool = {'introduction': 1, 'conclusion': 1, slug: 1} # forbidden slugs def __unicode__(self): return self.title @@ -399,7 +399,7 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.slug]) + return reverse('view-tutorial-url', args=[self.slug]) def get_absolute_url_online(self): """ @@ -429,13 +429,16 @@ def get_unique_slug(self, title): :param title: title from which the slug is generated (with `slugify()`) :return: the unique slug """ - new_slug = slugify(title) - if new_slug in self.slug_pool: - num = 1 - while new_slug + '-' + str(num) in self.slug_pool: - num += 1 - new_slug = new_slug + '-' + str(num) - self.slug_pool.append(new_slug) + base = slugify(title) + try: + n = self.slug_pool[base] + except KeyError: + new_slug = base + self.slug_pool[base] = 0 + else: + new_slug = base + '-' + str(n) + self.slug_pool[base] += 1 + self.slug_pool[new_slug] = 1 return new_slug def add_slug_to_pool(self, slug): @@ -443,10 +446,12 @@ def add_slug_to_pool(self, slug): Add a slug to the slug pool to be taken into account when generate a unique slug :param slug: the slug to add """ - if slug not in self.slug_pool: - self.slug_pool.append(slug) + try: + self.slug_pool[slug] # test access + except KeyError: + self.slug_pool[slug] = 1 else: - raise Exception('slug {} already in the slug pool !'.format(slug)) + raise Exception('slug "{}" already in the slug pool !'.format(slug)) def get_path(self, relative=False): """ @@ -502,8 +507,10 @@ def fill_containers_from_json(json_sub, parent): for child in json_sub['children']: if child['object'] == 'container': new_container = Container(child['title'], child['slug']) - new_container.introduction = child['introduction'] - new_container.conclusion = child['conclusion'] + if 'introduction' in child: + new_container.introduction = child['introduction'] + if 'conclusion' in child: + new_container.conclusion = child['conclusion'] parent.add_container(new_container) if 'children' in child: fill_containers_from_json(child, new_container) @@ -533,6 +540,7 @@ class Meta: verbose_name_plural = 'Contenus' title = models.CharField('Titre', max_length=80) + slug = models.SlugField(max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -630,6 +638,30 @@ def in_public(self): """ return (self.sha_public is not None) and (self.sha_public.strip() != '') + def is_beta(self, sha): + """ + Is this version of the content the beta version ? + :param sha: version + :return: `True` if the tutorial is in beta, `False` otherwise + """ + return self.in_beta() and sha == self.sha_beta + + def is_validation(self, sha): + """ + Is this version of the content the validation version ? + :param sha: version + :return: `True` if the tutorial is in validation, `False` otherwise + """ + return self.in_validation() and sha == self.sha_validation + + def is_public(self, sha): + """ + Is this version of the content the published version ? + :param sha: version + :return: `True` if the tutorial is in public, `False` otherwise + """ + return self.in_public() and sha == self.sha_public + def is_article(self): """ :return: `True` if article, `False` otherwise @@ -666,16 +698,19 @@ def load_version(self, sha=None, public=False): versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: # fill metadata : - versioned.description = json['description'] + if 'description' in json: + versioned.description = json['description'] if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': versioned.type = json['type'] else: versioned.type = self.type if 'licence' in json: - versioned.licence = Licence.objects.filter(code=data['licence']).first() + versioned.licence = Licence.objects.filter(code=json['licence']).first() # TODO must default licence be enforced here ? - versioned.introduction = json['introduction'] - versioned.conclusion = json['conclusion'] + if 'introduction' in json: + versioned.introduction = json['introduction'] + if 'conclusion' in json: + versioned.conclusion = json['conclusion'] # then, fill container with children fill_containers_from_json(json, versioned) self.insert_data_in_versioned(versioned) @@ -703,9 +738,9 @@ def insert_data_in_versioned(self, versioned): setattr(versioned, fn, getattr(self, fn)) # general information - versioned.is_beta = self.in_beta() and self.sha_beta == versioned.current_version - versioned.is_validation = self.in_validation() and self.sha_validation == versioned.current_version - versioned.is_public = self.in_public() and self.sha_public == versioned.current_version + versioned.is_beta = self.is_beta(versioned.current_version) + versioned.is_validation = self.is_validation(versioned.current_version) + versioned.is_public = self.is_public(versioned.current_version) def save(self, *args, **kwargs): self.slug = slugify(self.title) diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 9c2c3b1323..396e44e65b 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -92,7 +92,6 @@ def test_ensure_unique_slug(self): new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) - print(versioned.get_json()) def tearDown(self): if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 9871095ea8..956c2d78a2 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -18,116 +18,119 @@ url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), # Current URLs - url(r'^recherche/(?P\d+)/$', - 'zds.tutorial.views.find_tuto'), + # url(r'^recherche/(?P\d+)/$', + # 'zds.tutorialv2.views.find_tuto'), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_chapter', - name="view-chapter-url"), + # url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_chapter', + # name="view-chapter-url"), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_part', - name="view-part-url"), + # url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_part', + # name="view-part-url"), - url(r'^tutorial/off/(?P\d+)/(?P.+)/$', - DisplayContent.as_view()), + url(r'^off/tutoriel/(?P.+)/$', + DisplayContent.as_view(), + name='view-tutorial-url'), - url(r'^article/off/(?P\d+)/(?P.+)/$', + url(r'^off/article/(?P.+)/$', DisplayArticle.as_view()), # View online - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_chapter_online', - name="view-chapter-url-online"), - - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_part_online', - name="view-part-url-online"), - - url(r'^tutoriel/(?P\d+)/(?P.+)/$', - DisplayOnlineContent.as_view()), - - url(r'^article/(?P\d+)/(?P.+)/$', - DisplayOnlineArticle.as_view()), + # url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_chapter_online', + # name="view-chapter-url-online"), + # + # url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_part_online', + # name="view-part-url-online"), + # + # url(r'^tutoriel/(?P\d+)/(?P.+)/$', + # DisplayOnlineContent.as_view()), + # + # url(r'^article/(?P\d+)/(?P.+)/$', + # DisplayOnlineArticle.as_view()), # Editing - url(r'^editer/tutoriel/$', - 'zds.tutorial.views.edit_tutorial'), - url(r'^modifier/tutoriel/$', - 'zds.tutorial.views.modify_tutorial'), - url(r'^modifier/partie/$', - 'zds.tutorial.views.modify_part'), - url(r'^editer/partie/$', - 'zds.tutorial.views.edit_part'), - url(r'^modifier/chapitre/$', - 'zds.tutorial.views.modify_chapter'), - url(r'^editer/chapitre/$', - 'zds.tutorial.views.edit_chapter'), - url(r'^modifier/extrait/$', - 'zds.tutorial.views.modify_extract'), - url(r'^editer/extrait/$', - 'zds.tutorial.views.edit_extract'), + # url(r'^editer/tutoriel/$', + # 'zds.tutorialv2.views.edit_tutorial'), + # url(r'^modifier/tutoriel/$', + # 'zds.tutorialv2.views.modify_tutorial'), + # url(r'^modifier/partie/$', + # 'zds.tutorialv2.views.modify_part'), + # url(r'^editer/partie/$', + # 'zds.tutorialv2.views.edit_part'), + # url(r'^modifier/chapitre/$', + # 'zds.tutorialv2.views.modify_chapter'), + # url(r'^editer/chapitre/$', + # 'zds.tutorialv2.views.edit_chapter'), + # url(r'^modifier/extrait/$', + # 'zds.tutorialv2.views.modify_extract'), + # url(r'^editer/extrait/$', + # 'zds.tutorialv2.views.edit_extract'), # Adding - url(r'^nouveau/tutoriel/$', - 'zds.tutorial.views.add_tutorial'), - url(r'^nouveau/partie/$', - 'zds.tutorial.views.add_part'), - url(r'^nouveau/chapitre/$', - 'zds.tutorial.views.add_chapter'), - url(r'^nouveau/extrait/$', - 'zds.tutorial.views.add_extract'), - - url(r'^$', TutorialList.as_view, name='index-tutorial'), - url(r'^importer/$', 'zds.tutorial.views.import_tuto'), - url(r'^import_local/$', - 'zds.tutorial.views.local_import'), - url(r'^telecharger/$', 'zds.tutorial.views.download'), - url(r'^telecharger/pdf/$', - 'zds.tutorial.views.download_pdf'), - url(r'^telecharger/html/$', - 'zds.tutorial.views.download_html'), - url(r'^telecharger/epub/$', - 'zds.tutorial.views.download_epub'), - url(r'^telecharger/md/$', - 'zds.tutorial.views.download_markdown'), - url(r'^historique/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.history'), - url(r'^comparaison/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.diff'), + # url(r'^nouveau/tutoriel/$', + # 'zds.tutorialv2.views.add_tutorial'), + # url(r'^nouveau/partie/$', + # 'zds.tutorialv2.views.add_part'), + # url(r'^nouveau/chapitre/$', + # 'zds.tutorialv2.views.add_chapter'), + # url(r'^nouveau/extrait/$', + # 'zds.tutorialv2.views.add_extract'), + + # url(r'^$', TutorialList.as_view, name='index-tutorial'), + # url(r'^importer/$', 'zds.tutorialv2.views.import_tuto'), + # url(r'^import_local/$', + # 'zds.tutorialv2.views.local_import'), + # url(r'^telecharger/$', 'zds.tutorialv2.views.download'), + # url(r'^telecharger/pdf/$', + # 'zds.tutorialv2.views.download_pdf'), + # url(r'^telecharger/html/$', + # 'zds.tutorialv2.views.download_html'), + # url(r'^telecharger/epub/$', + # 'zds.tutorialv2.views.download_epub'), + # url(r'^telecharger/md/$', + # 'zds.tutorialv2.views.download_markdown'), + url(r'^historique/(?P.+)/$', + 'zds.tutorialv2.views.history', + name='view-tutorial-history-url'), + # url(r'^comparaison/(?P.+)/$', + # DisplayDiff.as_view(), + # name='view-tutorial-diff-url'), # user actions - url(r'^suppression/(?P\d+)/$', - 'zds.tutorial.views.delete_tutorial'), + url(r'^suppression/(?P.+)/$', + 'zds.tutorialv2.views.delete_tutorial'), url(r'^validation/tutoriel/$', - 'zds.tutorial.views.ask_validation'), + 'zds.tutorialv2.views.ask_validation'), # Validation - url(r'^validation/$', - 'zds.tutorial.views.list_validation'), - url(r'^validation/reserver/(?P\d+)/$', - 'zds.tutorial.views.reservation'), - url(r'^validation/reject/$', - 'zds.tutorial.views.reject_tutorial'), - url(r'^validation/valid/$', - 'zds.tutorial.views.valid_tutorial'), - url(r'^validation/invalid/(?P\d+)/$', - 'zds.tutorial.views.invalid_tutorial'), - url(r'^validation/historique/(?P\d+)/$', - 'zds.tutorial.views.history_validation'), - url(r'^activation_js/$', - 'zds.tutorial.views.activ_js'), - # Reactions - url(r'^message/editer/$', - 'zds.tutorial.views.edit_note'), - url(r'^message/nouveau/$', 'zds.tutorial.views.answer'), - url(r'^message/like/$', 'zds.tutorial.views.like_note'), - url(r'^message/dislike/$', - 'zds.tutorial.views.dislike_note'), - - # Moderation - url(r'^resolution_alerte/$', - 'zds.tutorial.views.solve_alert'), + # url(r'^validation/$', + # 'zds.tutorialv2.views.list_validation'), + # url(r'^validation/reserver/(?P\d+)/$', + # 'zds.tutorialv2.views.reservation'), + # url(r'^validation/reject/$', + # 'zds.tutorialv2.views.reject_tutorial'), + # url(r'^validation/valid/$', + # 'zds.tutorialv2.views.valid_tutorial'), + # url(r'^validation/invalid/(?P\d+)/$', + # 'zds.tutorialv2.views.invalid_tutorial'), + url(r'^validation/historique/(?P.+)/$', + 'zds.tutorialv2.views.history_validation'), + # url(r'^activation_js/$', + # 'zds.tutorialv2.views.activ_js'), + # # Reactions + # url(r'^message/editer/$', + # 'zds.tutorialv2.views.edit_note'), + # url(r'^message/nouveau/$', 'zds.tutorialv2.views.answer'), + # url(r'^message/like/$', 'zds.tutorialv2.views.like_note'), + # url(r'^message/dislike/$', + # 'zds.tutorialv2.views.dislike_note'), + # + # # Moderation + # url(r'^resolution_alerte/$', + # 'zds.tutorialv2.views.solve_alert'), # Help url(r'^aides/$', diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index c1df7929cd..b512d0307e 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -92,6 +92,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super(ArticleList, self).get_context_data(**kwargs) context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! return context @@ -100,7 +101,7 @@ class TutorialList(ArticleList): context_object_name = 'tutorials' type = "TUTORIAL" - template_name = 'tutorial/index.html' + template_name = 'tutorialv2/index.html' def render_chapter_form(chapter): @@ -119,7 +120,7 @@ class TutorialWithHelp(TutorialList): """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta for more documentation, have a look to ZEP 03 specification (fr)""" context_object_name = 'tutorials' - template_name = 'tutorial/help.html' + template_name = 'tutorialv2/help.html' def get_queryset(self): """get only tutorial that need help and handle filtering if asked""" @@ -141,15 +142,19 @@ def get_context_data(self, **kwargs): context['helps'] = HelpWriting.objects.all() return context +# TODO ArticleWithHelp + class DisplayContent(DetailView): """Base class that can show any content in any state, by default it shows offline tutorials""" model = PublishableContent - template_name = 'tutorial/view.html' + template_name = 'tutorialv2/view.html' type = "TUTORIAL" - is_public = False + online = False + sha = None + # TODO compatibility should be performed into class `PublishableContent.load_version()` ! def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content dictionary["path"] = content.get_repo_path() @@ -181,7 +186,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): def get_forms(self, context, content): """get all the auxiliary forms about validation, js fiddle...""" - validation = Validation.objects.filter(tutorial__pk=content.pk)\ + validation = Validation.objects.filter(content__pk=content.pk)\ .order_by("-date_proposition")\ .first() form_js = ActivJsForm(initial={"js_support": content.js_support}) @@ -200,19 +205,20 @@ def get_forms(self, context, content): context["formValid"] = form_valid context["formReject"] = form_reject, - def get_object(self): - return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, slug=self.kwargs['content_slug']) def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" + # TODO: handling public version ! context = super(DisplayContent, self).get_context_data(**kwargs) - content = context[self.context_object_name] + content = context['object'] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't # exist, we take draft version of the content. - try: - sha = self.request.GET.get("version") + sha = self.request.GET["version"] except KeyError: if self.sha is not None: sha = self.sha @@ -220,40 +226,16 @@ def get_context_data(self, **kwargs): sha = content.sha_draft # check that if we ask for beta, we also ask for the sha version - is_beta = (sha == content.sha_beta and content.in_beta()) - # check that if we ask for public version, we also ask for the sha version - is_online = (sha == content.sha_public and content.in_public()) - # Only authors of the tutorial and staff can view tutorial in offline. + is_beta = content.is_beta(sha) - if self.request.user not in content.authors.all() and not is_beta and not is_online: + if self.request.user not in content.authors.all() and not is_beta: # if we are not author of this content or if we did not ask for beta # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Find the good manifest file - - repo = Repo(content.get_repo_path()) - - # Load the tutorial. - - mandata = content.load_json_for_public(sha) - content.load_dic(mandata, sha) - content.load_introduction_and_conclusion(mandata, sha, sha == content.sha_public) - children_tree = {} - - if 'chapter' in mandata: - # compatibility with old "Mini Tuto" - self.compatibility_chapter(content, repo, sha, mandata["chapter"]) - children_tree = mandata['chapter'] - elif 'parts' in mandata: - # compatibility with old "big tuto". - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - self.compatibility_parts(content, repo, sha, part, cpt_p) - cpt_p += 1 - children_tree = parts + # load versioned file + versioned_tutorial = content.load_version(sha) # check whether this tuto support js fiddle if content.js_support: @@ -261,8 +243,7 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = mandata # TODO : change to "content" - context["children"] = children_tree + context["tutorial"] = versioned_tutorial context["version"] = sha self.get_forms(context, content) diff --git a/zds/urls.py b/zds/urls.py index dd33fa9b80..0ffbb375b6 100644 --- a/zds/urls.py +++ b/zds/urls.py @@ -75,6 +75,7 @@ def location(self, article): urlpatterns = patterns('', url(r'^tutoriels/', include('zds.tutorial.urls')), url(r'^articles/', include('zds.article.urls')), + url(r'^contenu/', include('zds.tutorialv2.urls')), url(r'^forums/', include('zds.forum.urls')), url(r'^mp/', include('zds.mp.urls')), url(r'^membres/', include('zds.member.urls')), From e1c3cb707254ee353d38b8aad944ea7d1aeea837 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sun, 25 Jan 2015 09:02:01 +0100 Subject: [PATCH 018/887] Correction d'une erreur de manipulation sur `tutorial/view.py` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_repo_path()` → `get_path()` --- zds/tutorial/views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index ee492261fa..7b945bafb4 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -2338,9 +2338,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2731,7 +2731,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_repo_path()) + repo = Repo(part.tutorial.get_path()) index = repo.index # update the tutorial last edit date part.tutorial.update = datetime.now() @@ -2751,19 +2751,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = str(request.user.email) @@ -2836,10 +2836,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_repo_path(), + man_path = os.path.join(chapter.part.tutorial.get_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2906,14 +2906,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_repo_path(relative=True)]) + index.add([extract.get_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) From 692de4c8464582c202e31f7ac70ce502313d7412 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 29 Jan 2015 15:14:41 +0100 Subject: [PATCH 019/887] Implemente une partie des specifications + Reecriture d'une partie des modeles (entre autre sur la partie de gestion des slugs) + Creation de nouveaux tests et reecriture des anciens Permet de creer, supprimer et consulter un contenu + Creation des vues correspondantes + Creation de certains tests --- requirements.txt | 3 +- templates/tutorialv2/create/content.html | 30 ++ templates/tutorialv2/edit/content.html | 40 ++ .../includes/content_item.part.html | 31 ++ .../includes/tags_authors.part.html | 44 ++ templates/tutorialv2/index.html | 56 ++ templates/tutorialv2/view.html | 486 ------------------ templates/tutorialv2/view/content.html | 206 ++++++++ zds/settings.py | 6 +- zds/tutorialv2/forms.py | 14 +- zds/tutorialv2/models.py | 308 +++++++---- zds/tutorialv2/tests/tests_models.py | 41 +- zds/tutorialv2/tests/tests_views.py | 126 +++++ zds/tutorialv2/url/__init__.py | 1 + zds/tutorialv2/url/url_contents.py | 22 + zds/tutorialv2/urls.py | 12 +- zds/tutorialv2/views.py | 414 +++++++++++---- zds/urls.py | 2 +- 18 files changed, 1123 insertions(+), 719 deletions(-) create mode 100644 templates/tutorialv2/create/content.html create mode 100644 templates/tutorialv2/edit/content.html create mode 100644 templates/tutorialv2/includes/content_item.part.html create mode 100644 templates/tutorialv2/includes/tags_authors.part.html create mode 100644 templates/tutorialv2/index.html delete mode 100644 templates/tutorialv2/view.html create mode 100644 templates/tutorialv2/view/content.html create mode 100644 zds/tutorialv2/url/__init__.py create mode 100644 zds/tutorialv2/url/url_contents.py diff --git a/requirements.txt b/requirements.txt index 52e3f18d99..846b38f532 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,5 @@ django-filter==0.8 django-oauth-toolkit==0.7.2 drf-extensions==0.2.6 django-rest-swagger==0.2.8 -django-cors-headers==1.0.0 \ No newline at end of file +django-cors-headers==1.0.0 +django-uuslug==1.0.3 diff --git a/templates/tutorialv2/create/content.html b/templates/tutorialv2/create/content.html new file mode 100644 index 0000000000..0ab6c81cdb --- /dev/null +++ b/templates/tutorialv2/create/content.html @@ -0,0 +1,30 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Nouveau contenu" %} +{% endblock %} + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + +{% block breadcrumb %} +
  • {% trans "Nouveau contenu" %}
  • +{% endblock %} + + + +{% block headline %} +

    + {% trans "Nouveau contenu" %} +

    +{% endblock %} + + + +{% block content %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html new file mode 100644 index 0000000000..9d4a2cc26c --- /dev/null +++ b/templates/tutorialv2/edit/content.html @@ -0,0 +1,40 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load i18n %} + +{% block title %} + {% trans "Éditer le contenu" %} +{% endblock %} + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + +{% block breadcrumb %} +
  • {{ content.title }}
  • +
  • {% trans "Éditer le contenu" %}
  • +{% endblock %} + +{% block headline %} +

    + {% if content.image %} + + {% endif %} + {% trans "Éditer" %} : {{ content.title }} +

    +{% endblock %} + +{% block headline_sub %} + {{ content.description }} +{% endblock %} + + +{% block content %} + {% if new_version %} +

    + {% trans "Une nouvelle version a été postée avant que vous ne validiez" %}. +

    + {% endif %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/includes/content_item.part.html b/templates/tutorialv2/includes/content_item.part.html new file mode 100644 index 0000000000..f5b98891e1 --- /dev/null +++ b/templates/tutorialv2/includes/content_item.part.html @@ -0,0 +1,31 @@ +{% load thumbnail %} +{% load date %} +{% load i18n %} + + \ No newline at end of file diff --git a/templates/tutorialv2/includes/tags_authors.part.html b/templates/tutorialv2/includes/tags_authors.part.html new file mode 100644 index 0000000000..5fb924fd4e --- /dev/null +++ b/templates/tutorialv2/includes/tags_authors.part.html @@ -0,0 +1,44 @@ +{% load date %} +{% load i18n %} + + + +{% if content.subcategory.all|length > 0 %} + +{% endif %} + + + +{% if content.update %} + + {% trans "Dernière mise à jour" %} : + + +{% endif %} + +{% include "misc/zen_button.part.html" %} + +
    + {% trans "Auteur" %}{{ content.authors.all|pluralize }} : +
      + {% for member in content.authors.all %} +
    • + {% include "misc/member_item.part.html" with avatar=True author=True %} +
    • + {% endfor %} + + {% if add_author == True %} +
    • + + {% trans "Ajouter un auteur" %} + +
    • + {% endif %} +
    +
    diff --git a/templates/tutorialv2/index.html b/templates/tutorialv2/index.html new file mode 100644 index 0000000000..ce4a752428 --- /dev/null +++ b/templates/tutorialv2/index.html @@ -0,0 +1,56 @@ +{% extends "tutorialv2/base.html" %} +{% load date %} +{% load i18n %} + + +{% block title %} + {% trans "Mes contenus" %} +{% endblock %} + + + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + + + +{% block content_out %} +
    +

    + {% block headline %} + {% trans "Mes contenus" %} + {% endblock %} +

    + + {% if tutorials %} +

    {% trans "Mes tutoriels" %}

    +
    + {% for tutorial in tutorials %} + {% include "tutorialv2/includes/content_item.part.html" with content=tutorial %} + {% endfor %} +
    + {% endif %} + + {% if articles %} +

    {% trans "Mes articles" %}

    +
    + {% for article in articles %} + {% include "tutorialv2/includes/content_item.part.html" with content=article %} + {% endfor %} +
    + {% endif %} + + {% if not articles and not tutorials %} +

    {% trans "Vous n'avez encore créé aucun contenu" %}

    + {% endif %} +
    +{% endblock %} + + + +{% block sidebar_new %} + + {% trans "Nouveau contenu" %} + +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/view.html b/templates/tutorialv2/view.html deleted file mode 100644 index c53cf43273..0000000000 --- a/templates/tutorialv2/view.html +++ /dev/null @@ -1,486 +0,0 @@ -{% extends "tutorialv2/base.html" %} -{% load emarkdown %} -{% load repo_reader %} -{% load crispy_forms_tags %} -{% load thumbnail %} -{% load roman %} -{% load i18n %} - - -{% block title %} - {{ tutorial.title }} -{% endblock %} - - - -{% block breadcrumb %} -
  • {{ tutorial.title }}
  • -{% endblock %} - - - -{% block headline %} - {% if tutorial.licence %} -

    - {{ tutorial.licence }} -

    - {% endif %} - -

    - {% if tutorial.image %} - - {% endif %} - {{ tutorial.title }} -

    - - {% if tutorial.description %} -

    - {{ tutorial.description }} -

    - {% endif %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial add_author=True %} - {% else %} - {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial %} - {% endif %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if tutorial.in_validation %} - {% if validation.version == version %} - {% if validation.is_pending %} -

    - {% trans "Ce tutoriel est en attente d'un validateur" %} -

    - {% elif validation.is_pending_valid %} -

    - {% trans "Le tutoriel est en cours de validation par" %} - {% include "misc/member_item.part.html" with member=validation.validator %} -

    - {% endif %} - {% if validation.comment_authors %} -
    -

    - {% trans "Le message suivant a été laissé à destination des validateurs" %} : -

    - -
    - {{ validation.comment_authors|emarkdown }} -
    -
    - {% endif %} - {% else %} - {% if validation.is_pending %} -

    - - {% trans "Une autre version de ce tutoriel" %} - {% trans "est en attente d'un validateur" %} -

    - {% elif validation.is_pending_valid %} -

    - - {% trans "Une autre version de ce tutoriel" %} - {% trans "est en cours de validation par" %} - {% include "misc/member_item.part.html" with member=validation.validator %} -

    - {% endif %} - {% endif %} - {% endif %} - {% endif %} - - {% if tutorial.is_beta %} -
    -
    - {% blocktrans %} - Cette version du tutoriel est en "BÊTA" ! - {% endblocktrans %} -
    -
    - {% endif %} -{% endblock %} - - -{% block content %} - {% if tutorial.get_introduction and tutorial.get_introduction != "None" %} - {{ tutorial.get_introduction|emarkdown:is_js }} - {% elif not tutorial.is_beta %} -

    - {% trans "Il n'y a pas d'introduction" %}. -

    - {% endif %} - - {% if tutorial.is_mini %} - {# Small tutorial #} - - {% include "tutorial/includes/chapter.part.html" with authors=tutorial.authors.all %} - {% else %} - {# Large tutorial #} - -
    - - {% for part in parts %} -

    - - {{ part.position_in_tutorial|roman }} - {{ part.title }} - -

    - {% include "tutorial/includes/part.part.html" %} - {% empty %} -

    - {% trans "Il n'y a actuellement aucune partie dans ce tutoriel" %}. -

    - {% endfor %} - -
    - - {% endif %} - - {% if tutorial.get_conclusion and tutorial.get_conclusion != "None" %} - {{ tutorial.get_conclusion|emarkdown:is_js }} - {% elif not tutorial.is_beta %} -

    - {% trans "Il n'y a pas de conclusion" %}. -

    - {% endif %} -{% endblock %} - - - -{% block sidebar_new %} - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if tutorial.sha_draft = version %} - {% if not tutorial.is_mini %} - - {% trans "Ajouter une partie" %} - - {% else %} - - {% trans "Ajouter un extrait" %} - - {% endif %} - - - {% trans "Éditer" %} - - {% else %} - - {% trans "Version brouillon" %} - - {% endif %} - {% endif %} -{% endblock %} - - - -{% block sidebar_actions %} - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} -
  • - - {% trans "Ajouter un auteur" %} - - -
  • -
  • - - {% trans "Gérer les auteurs" %} - - -
  • - - {% if tutorial.sha_public %} -
  • - - {% blocktrans %} - Voir la version en ligne - {% endblocktrans %} - -
  • - {% endif %} - - {% if not tutorial.in_beta %} -
  • - - {% trans "Mettre cette version en bêta" %} - - -
  • - {% else %} - {% if not tutorial.is_beta %} -
  • - - {% blocktrans %} - Voir la version en bêta - {% endblocktrans %} - -
  • -
  • - - {% trans "Mettre à jour la bêta avec cette version" %} - - -
  • - {% else %} -
  • - - {% trans "Cette version est déjà en bêta" %} - -
  • - {% endif %} -
  • - - {% trans "Désactiver la bêta" %} - - -
  • - {% endif %} - -
  • - - {% trans "Historique des versions" %} - -
  • - - {% if not tutorial.in_validation %} -
  • - - {% trans "Demander la validation" %} - -
  • - {% else %} - {% if not tutorial.is_validation %} -
  • - - {% trans "Mettre à jour la version en validation" %} - -
  • - {% endif %} -
  • - {% trans "En attente de validation" %} -
  • - {% endif %} - - {% endif %} -{% endblock %} - - - -{% block sidebar_blocks %} - {% if perms.tutorial.change_tutorial %} - - {% endif %} - - {% include "tutorial/includes/summary.part.html" %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - - {% endif %} -{% endblock %} diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html new file mode 100644 index 0000000000..66daa60f08 --- /dev/null +++ b/templates/tutorialv2/view/content.html @@ -0,0 +1,206 @@ +{% extends "tutorialv2/base.html" %} +{% load emarkdown %} +{% load repo_reader %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load roman %} +{% load i18n %} + + +{% block title %} + {{ content.title }} +{% endblock %} + + +{% if user in content.authors.all or perms.content.change_content %} + {% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • + {% endblock %} +{% endif %} + + +{% block breadcrumb %} +
  • {{ content.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if content.licence %} +

    + {{ content.licence }} +

    + {% endif %} + +

    + {% if content.image %} + + {% endif %} + {{ content.title }} +

    + + {% if content.description %} +

    + {{ content.description }} +

    + {% endif %} + + {% if user in content.authors.all or perms.content.change_content %} + {% include 'tutorialv2/includes/tags_authors.part.html' with content=content add_author=True %} + {% else %} + {% include 'tutorialv2/includes/tags_authors.part.html' with content=content %} + {% endif %} + + {% if user in content.authors.all or perms.content.change_content %} + {% if content.in_validation %} + {% if validation.version == version %} + {% if validation.is_pending %} +

    + {% trans "Ce tutoriel est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + {% trans "Le tutoriel est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% if validation.comment_authors %} +
    +

    + {% trans "Le message suivant a été laissé à destination des validateurs" %} : +

    + +
    + {{ validation.comment_authors|emarkdown }} +
    +
    + {% endif %} + {% else %} + {% if validation.is_pending %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if content.is_beta %} +
    +
    + {% blocktrans %} + Cette version est en "BÊTA" ! + {% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + +{% block content %} + {% if content.get_introduction and content.get_introduction != "None" %} + {{ content.get_introduction|emarkdown:is_js }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + + +
    + + {% for child in content.childreen %} + {# include stuff #} + {% empty %} +

    + {% trans "Ce contenu est actuelement vide" %}. +

    + {% endfor %} + +
    + + {% if content.get_conclusion and content.get_conclusion != "None" %} + {{ content.get_conclusion|emarkdown:is_js }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in content.authors.all or perms.content.change_content %} + {% if content.sha_draft == version %} + + {% trans "Éditer" %} + + + {% else %} + + {% trans "Version brouillon" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in content.authors.all or perms.content.change_content %} + {# other action (valid, beta, ...) #} + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {% if perms.content.change_content %} + {# more actions ?!? #} + {% endif %} + + {# include "content/includes/summary.part.html" #} + + {% if user in content.authors.all or perms.content.change_content %} + + {% endif %} +{% endblock %} diff --git a/zds/settings.py b/zds/settings.py index 90f40d7651..2a5a440d45 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -453,10 +453,14 @@ 'default_license_pk': 7, 'home_number': 5, 'helps_per_page': 20, - 'max_tree_depth': 3, 'content_per_page': 50, 'feed_length': 5 }, + 'content': { + 'repo_private_path': os.path.join(SITE_ROOT, 'contents-private'), + 'repo_public_path': os.path.join(SITE_ROOT, 'contents-public'), + 'max_tree_depth': 3 + }, 'forum': { 'posts_per_page': 21, 'topics_per_page': 21, diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index e2001098e8..7bd5236838 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -10,14 +10,14 @@ from zds.utils.forms import CommonLayoutModalText, CommonLayoutEditor, CommonLayoutVersionEditor from zds.utils.models import SubCategory, Licence -from zds.tutorial.models import Tutorial, TYPE_CHOICES, HelpWriting +from zds.tutorialv2.models import PublishableContent, TYPE_CHOICES, HelpWriting from django.utils.translation import ugettext_lazy as _ class FormWithTitle(forms.Form): title = forms.CharField( label=_(u'Titre'), - max_length=Tutorial._meta.get_field('title').max_length, + max_length=PublishableContent._meta.get_field('title').max_length, widget=forms.TextInput( attrs={ 'required': 'required', @@ -39,11 +39,11 @@ def clean(self): return cleaned_data -class TutorialForm(FormWithTitle): +class ContentForm(FormWithTitle): description = forms.CharField( label=_(u'Description'), - max_length=Tutorial._meta.get_field('description').max_length, + max_length=PublishableContent._meta.get_field('description').max_length, required=False, ) @@ -123,7 +123,7 @@ class TutorialForm(FormWithTitle): ) def __init__(self, *args, **kwargs): - super(TutorialForm, self).__init__(*args, **kwargs) + super(ContentForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' @@ -414,7 +414,7 @@ class ImportArchiveForm(forms.Form): tutorial = forms.ModelChoiceField( label=_(u"Tutoriel vers lequel vous souhaitez importer votre archive"), - queryset=Tutorial.objects.none(), + queryset=PublishableContent.objects.none(), required=True ) @@ -423,7 +423,7 @@ def __init__(self, user, *args, **kwargs): self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' - self.fields['tutorial'].queryset = Tutorial.objects.filter(authors__in=[user]) + self.fields['tutorial'].queryset = PublishableContent.objects.filter(authors__in=[user]) self.helper.layout = Layout( Field('file'), diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cd6af0f5ae..63cf578acf 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -28,6 +28,8 @@ from zds.settings import ZDS_APP from zds.utils.models import HelpWriting +from uuslug import uuslug + TYPE_CHOICES = ( ('TUTORIAL', 'Tutoriel'), @@ -66,6 +68,7 @@ class Container: position_in_parent = 1 children = [] children_dict = {} + slug_pool = {} # TODO: thumbnails ? @@ -78,6 +81,8 @@ def __init__(self, title, slug='', parent=None, position_in_parent=1): self.children = [] # even if you want, do NOT remove this line self.children_dict = {} + self.slug_pool = {'introduction': 1, 'conclusion': 1} # forbidden slugs + def __unicode__(self): return u''.format(self.title) @@ -130,18 +135,50 @@ def top_container(self): current = current.parent return current + def get_unique_slug(self, title): + """ + Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a + "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. + :param title: title from which the slug is generated (with `slugify()`) + :return: the unique slug + """ + base = slugify(title) + try: + n = self.slug_pool[base] + except KeyError: + new_slug = base + self.slug_pool[base] = 0 + else: + new_slug = base + '-' + str(n) + self.slug_pool[base] += 1 + self.slug_pool[new_slug] = 1 + return new_slug + + def add_slug_to_pool(self, slug): + """ + Add a slug to the slug pool to be taken into account when generate a unique slug + :param slug: the slug to add + """ + try: + self.slug_pool[slug] # test access + except KeyError: + self.slug_pool[slug] = 1 + else: + raise Exception('slug "{}" already in the slug pool !'.format(slug)) + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. + Note: this function will also raise an Exception if article, because it cannot contain child container :param container: the new container :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_extracts(): - if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth'] and self.top_container().type != 'ARTICLE': if generate_slug: - container.slug = self.top_container().get_unique_slug(container.title) + container.slug = self.get_unique_slug(container.title) else: - self.top_container().add_slug_to_pool(container.slug) + self.add_slug_to_pool(container.slug) container.parent = self container.position_in_parent = self.get_last_child_position() + 1 self.children.append(container) @@ -149,8 +186,7 @@ def add_container(self, container, generate_slug=False): else: raise InvalidOperationError("Cannot add another level to this content") else: - raise InvalidOperationError("Can't add a container if this container contains extracts.") - # TODO: limitation if article ? + raise InvalidOperationError("Can't add a container if this container already contains extracts.") def add_extract(self, extract, generate_slug=False): """ @@ -160,18 +196,20 @@ def add_extract(self, extract, generate_slug=False): """ if not self.has_sub_containers(): if generate_slug: - extract.slug = self.top_container().get_unique_slug(extract.title) + extract.slug = self.get_unique_slug(extract.title) else: - self.top_container().add_slug_to_pool(extract.slug) + self.add_slug_to_pool(extract.slug) extract.container = self extract.position_in_parent = self.get_last_child_position() + 1 self.children.append(extract) self.children_dict[extract.slug] = extract + else: + raise InvalidOperationError("Can't add an extract if this container already contains containers.") def update_children(self): """ Update the path for introduction and conclusion for the container and all its children. If the children is an - extract, update the path to the text instead. This function is useful when `self.pk` or `self.title` has + extract, update the path to the text instead. This function is useful when `self.slug` has changed. Note : this function does not account for a different arrangement of the files. """ @@ -214,18 +252,6 @@ def get_introduction(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.introduction) - def get_introduction_online(self): - """ - Get introduction content of the public version - :return: the introduction - """ - path = self.top_container().get_prod_path() + self.introduction + '.html' - if os.path.exists(path): - intro = open(path) - intro_content = intro.read() - intro.close() - return intro_content.decode('utf-8') - def get_conclusion(self): """ :return: the conclusion from the file in `self.conclusion` @@ -234,17 +260,48 @@ def get_conclusion(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.conclusion) - def get_conclusion_online(self): + def repo_update(self, title, introduction, conclusion, commit_message=''): """ - Get conclusion content of the public version - :return: the conclusion + Update the container information and commit them into the repository + :param title: the new title + :param introduction: the new introduction text + :param conclusion: the new conclusion text + :param commit_message: commit message that will be used instead of the default one + :return : commit sha """ - path = self.top_container().get_prod_path() + self.conclusion + '.html' - if os.path.exists(path): - conclusion = open(path) - conclusion_content = conclusion.read() - conclusion.close() - return conclusion_content.decode('utf-8') + + # update title + if title != self.title: + self.title = title + if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed + self.slug = self.top_container().get_unique_slug(title) + self.update_children() + # TODO : and move() !!! + + # update introduction and conclusion + if self.introduction is None: + self.introduction = self.get_path(relative=True) + 'introduction.md' + if self.conclusion is None: + self.conclusion = self.get_path(relative=True) + 'conclusion.md' + + path = self.top_container().get_path() + f = open(os.path.join(path, self.introduction), "w") + f.write(introduction.encode('utf-8')) + f.close() + f = open(os.path.join(path, self.conclusion), "w") + f.write(conclusion.encode('utf-8')) + f.close() + + self.top_container().dump_json() + + repo = self.top_container().repository + repo.index.add(['manifest.json', self.introduction, self.conclusion]) + + if commit_message == '': + commit_message = u'Mise à jour de « ' + self.title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha # TODO: # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) @@ -311,14 +368,6 @@ def get_path(self, relative=False): """ return os.path.join(self.container.get_path(relative=relative), self.slug) + '.md' - def get_prod_path(self): - """ - Get the physical path to the public version of a specific version of the extract. - :return: physical path - """ - return os.path.join(self.container.get_prod_path(), self.slug) + '.md.html' - # TODO: should this function exists ? (there will be no public version of a text, all in parent container) - def get_text(self): """ :return: versioned text @@ -360,6 +409,7 @@ class VersionedContent(Container): slug_pool = {} # Metadata from DB : + pk = 0 sha_draft = None sha_beta = None sha_public = None @@ -390,8 +440,6 @@ def __init__(self, current_version, _type, title, slug): self.type = _type self.repository = Repo(self.get_path()) - self.slug_pool = {'introduction': 1, 'conclusion': 1, slug: 1} # forbidden slugs - def __unicode__(self): return self.title @@ -399,7 +447,7 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('view-tutorial-url', args=[self.slug]) + return reverse('content:view', args=[self.pk, self.slug]) def get_absolute_url_online(self): """ @@ -422,37 +470,6 @@ def get_edit_url(self): """ return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) - def get_unique_slug(self, title): - """ - Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a - "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. - :param title: title from which the slug is generated (with `slugify()`) - :return: the unique slug - """ - base = slugify(title) - try: - n = self.slug_pool[base] - except KeyError: - new_slug = base - self.slug_pool[base] = 0 - else: - new_slug = base + '-' + str(n) - self.slug_pool[base] += 1 - self.slug_pool[new_slug] = 1 - return new_slug - - def add_slug_to_pool(self, slug): - """ - Add a slug to the slug pool to be taken into account when generate a unique slug - :param slug: the slug to add - """ - try: - self.slug_pool[slug] # test access - except KeyError: - self.slug_pool[slug] = 1 - else: - raise Exception('slug "{}" already in the slug pool !'.format(slug)) - def get_path(self, relative=False): """ Get the physical path to the draft version of the Content. @@ -462,15 +479,14 @@ def get_path(self, relative=False): if relative: return '' else: - # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) def get_prod_path(self): """ Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.slug) + return os.path.join(settings.ZDS_APP['contents']['repo_public_path'], self.slug) def get_json(self): """ @@ -494,6 +510,28 @@ def dump_json(self, path=None): json_data.write(self.get_json().encode('utf-8')) json_data.close() + def repo_update_top_container(self, title, slug, introduction, conclusion, commit_message=''): + """ + Update the top container information and commit them into the repository. + Note that this is slightly different from the `repo_update()` function, because slug is generated using DB + :param title: the new title + :param slug: the new slug, according to title (choose using DB!!) + :param introduction: the new introduction text + :param conclusion: the new conclusion text + :param commit_message: commit message that will be used instead of the default one + :return : commit sha + """ + + if slug != self.slug: + # move repository + old_path = self.get_path() + self.slug = slug + new_path = self.get_path() + shutil.move(old_path, new_path) + self.repository = Repo(new_path) + + return self.repo_update(title, introduction, conclusion, commit_message) + def fill_containers_from_json(json_sub, parent): """ @@ -501,27 +539,101 @@ def fill_containers_from_json(json_sub, parent): :param json_sub: dictionary from "manifest.json" :param parent: the container to fill """ - # TODO should be static function of `VersionedContent` - # TODO should implement fallbacks + # TODO should be static function of `VersionedContent` ?!? if 'children' in json_sub: for child in json_sub['children']: if child['object'] == 'container': - new_container = Container(child['title'], child['slug']) + slug = '' + try: + slug = child['slug'] + except KeyError: + pass + new_container = Container(child['title'], slug) if 'introduction' in child: new_container.introduction = child['introduction'] if 'conclusion' in child: new_container.conclusion = child['conclusion'] - parent.add_container(new_container) + parent.add_container(new_container, generate_slug=(slug != '')) if 'children' in child: fill_containers_from_json(child, new_container) elif child['object'] == 'extract': - new_extract = Extract(child['title'], child['slug']) + slug = '' + try: + slug = child['slug'] + except KeyError: + pass + new_extract = Extract(child['title'], slug) new_extract.text = child['text'] - parent.add_extract(new_extract) + parent.add_extract(new_extract, generate_slug=(slug != '')) else: raise Exception('Unknown object type'+child['object']) +def init_new_repo(db_object, introduction_text, conclusion_text, commit_message=''): + """ + Create a new repository in `settings.ZDS_APP['contents']['private_repo']` to store the files for a new content. + Note that `db_object.sha_draft` will be set to the good value + :param db_object: `PublishableContent` (WARNING: should have a valid `slug`, so previously saved) + :param introduction_text: introduction from form + :param conclusion_text: conclusion from form + :param commit_message : set a commit message instead of the default one + :return: `VersionedContent` object + """ + # TODO: should be a static function of an object (I don't know which one yet) + + # create directory + path = db_object.get_repo_path() + print(path) + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + introduction = 'introduction.md' + conclusion = 'conclusion.md' + versioned_content = VersionedContent(None, + db_object.type, + db_object.title, + db_object.slug) + + # fill some information that are missing : + versioned_content.licence = db_object.licence + versioned_content.description = db_object.description + versioned_content.introduction = introduction + versioned_content.conclusion = conclusion + + # init repo: + Repo.init(path, bare=False) + repo = Repo(path) + + # fill intro/conclusion: + f = open(os.path.join(path, introduction), "w") + f.write(introduction_text.encode('utf-8')) + f.close() + f = open(os.path.join(path, conclusion), "w") + f.write(conclusion_text.encode('utf-8')) + f.close() + + versioned_content.dump_json() + + # commit change: + if commit_message == '': + commit_message = u'Création du contenu' + repo.index.add(['manifest.json', introduction, conclusion]) + cm = repo.index.commit(commit_message) + + # update sha: + db_object.sha_draft = cm.hexsha + db_object.sha_beta = None + db_object.sha_public = None + db_object.sha_validation = None + + db_object.save() + + versioned_content.current_version = cm.hexsha + versioned_content.repository = repo + + return versioned_content + + class PublishableContent(models.Model): """ A tutorial whatever its size or an article. @@ -540,7 +652,7 @@ class Meta: verbose_name_plural = 'Contenus' title = models.CharField('Titre', max_length=80) - slug = models.SlugField(max_length=80) + slug = models.CharField('Slug', max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -598,6 +710,13 @@ class Meta: def __unicode__(self): return self.title + def save(self, *args, **kwargs): + """ + Rewrite the `save()` function to handle slug uniqueness + """ + self.slug = uuslug(self.title, instance=self, max_length=80) + super(PublishableContent, self).save(*args, **kwargs) + def get_repo_path(self, relative=False): """ Get the path to the tutorial repository @@ -608,7 +727,7 @@ def get_repo_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) def in_beta(self): """ @@ -697,20 +816,27 @@ def load_version(self, sha=None, public=False): # create and fill the container versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: + # fill metadata : if 'description' in json: versioned.description = json['description'] - if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': - versioned.type = json['type'] + + if 'type' in json: + if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': + versioned.type = json['type'] else: versioned.type = self.type + if 'licence' in json: versioned.licence = Licence.objects.filter(code=json['licence']).first() - # TODO must default licence be enforced here ? + else: + versioned.licence = Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + if 'introduction' in json: versioned.introduction = json['introduction'] if 'conclusion' in json: versioned.conclusion = json['conclusion'] + # then, fill container with children fill_containers_from_json(json, versioned) self.insert_data_in_versioned(versioned) @@ -728,6 +854,7 @@ def insert_data_in_versioned(self, versioned): """ fns = [ + 'pk', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public', 'authors', 'subcategory', 'image', 'creation_date', 'pubdate', 'update_date', 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' @@ -742,12 +869,6 @@ def insert_data_in_versioned(self, versioned): versioned.is_validation = self.is_validation(versioned.current_version) versioned.is_public = self.is_public(versioned.current_version) - def save(self, *args, **kwargs): - self.slug = slugify(self.title) - # TODO ensure unique slug here !! - - super(PublishableContent, self).save(*args, **kwargs) - def get_note_count(self): """ :return : umber of notes in the tutorial. @@ -859,18 +980,17 @@ def have_epub(self): """ return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) - def delete_entity_and_tree(self): + def repo_delete(self): """ Delete the entities and their filesystem counterparts """ shutil.rmtree(self.get_repo_path(), False) - Validation.objects.filter(tutorial=self).delete() + Validation.objects.filter(content=self).delete() if self.gallery is not None: self.gallery.delete() if self.in_public(): shutil.rmtree(self.get_prod_path()) - self.delete() class ContentReaction(Comment): diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 396e44e65b..fe13d94dc2 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -1,7 +1,5 @@ # coding: utf-8 -# NOTE : this file is only there for tests purpose, it will be deleted in final version - import os import shutil @@ -10,16 +8,15 @@ from django.test.utils import override_settings from zds.settings import SITE_ROOT -from zds.member.factories import ProfileFactory +from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory from zds.gallery.factories import GalleryFactory # from zds.tutorialv2.models import Container, Extract, VersionedContent overrided_zds_app = settings.ZDS_APP -overrided_zds_app['tutorial']['repo_path'] = os.path.join(SITE_ROOT, 'tutoriels-private-test') -overrided_zds_app['tutorial']['repo__public_path'] = os.path.join(SITE_ROOT, 'tutoriels-public-test') -overrided_zds_app['article']['repo_path'] = os.path.join(SITE_ROOT, 'article-data-test') +overrided_zds_app['content']['repo_private_path'] = os.path.join(SITE_ROOT, 'contents-private-test') +overrided_zds_app['content']['repo_public_path'] = os.path.join(SITE_ROOT, 'contents-public-test') @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @@ -34,6 +31,8 @@ def setUp(self): self.licence = LicenceFactory() self.user_author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.tuto = PublishableContentFactory(type='TUTORIAL') self.tuto.authors.add(self.user_author) self.tuto.gallery = GalleryFactory() @@ -68,13 +67,7 @@ def test_ensure_unique_slug(self): versioned = self.tuto.load_version() # forbidden slugs : - slug_to_test = ['introduction', # forbidden slug - 'conclusion', # forbidden slug - self.tuto.slug, # forbidden slug - # slug normally already in the slug pool : - self.part1.slug, - self.chapter1.slug, - self.extract1.slug] + slug_to_test = ['introduction', 'conclusion'] for slug in slug_to_test: new_slug = versioned.get_unique_slug(slug) @@ -83,22 +76,22 @@ def test_ensure_unique_slug(self): # then test with "real" containers and extracts : new_chapter_1 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) - # now, slug "aa" is forbidden ! new_chapter_2 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) self.assertNotEqual(new_chapter_1.slug, new_chapter_2.slug) + new_extract_1 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) - self.assertNotEqual(new_extract_1.slug, new_chapter_2.slug) - self.assertNotEqual(new_extract_1.slug, new_chapter_1.slug) + self.assertEqual(new_extract_1.slug, new_chapter_1.slug) # different level can have the same slug ! + new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) - self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) - self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) + self.assertEqual(new_extract_2.slug, new_extract_1.slug) # not the same parent, so allowed + + new_extract_3 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) + self.assertNotEqual(new_extract_3.slug, new_extract_1.slug) # same parent, forbidden def tearDown(self): - if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): - shutil.rmtree(settings.ZDS_APP['tutorial']['repo_path']) - if os.path.isdir(settings.ZDS_APP['tutorial']['repo_public_path']): - shutil.rmtree(settings.ZDS_APP['tutorial']['repo_public_path']) - if os.path.isdir(settings.ZDS_APP['article']['repo_path']): - shutil.rmtree(settings.ZDS_APP['article']['repo_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_public_path']) if os.path.isdir(settings.MEDIA_ROOT): shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index e69de29bb2..178b9d0368 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -0,0 +1,126 @@ +# coding: utf-8 + +import os +import shutil + +from django.conf import settings +from django.test import TestCase +from django.test.utils import override_settings +from django.core.urlresolvers import reverse + +from zds.settings import SITE_ROOT +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory +from zds.tutorialv2.models import PublishableContent +from zds.gallery.factories import GalleryFactory + +overrided_zds_app = settings.ZDS_APP +overrided_zds_app['content']['repo_private_path'] = os.path.join(SITE_ROOT, 'contents-private-test') +overrided_zds_app['content']['repo_public_path'] = os.path.join(SITE_ROOT, 'contents-public-test') + + +@override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) +@override_settings(ZDS_APP=overrided_zds_app) +class ContentTests(TestCase): + + def setUp(self): + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + self.mas = ProfileFactory().user + settings.ZDS_APP['member']['bot_account'] = self.mas.username + + self.licence = LicenceFactory() + + self.user_author = ProfileFactory().user + self.staff = StaffProfileFactory().user + + self.tuto = PublishableContentFactory(type='TUTORIAL') + self.tuto.authors.add(self.user_author) + self.tuto.gallery = GalleryFactory() + self.tuto.licence = self.licence + self.tuto.save() + + self.tuto_draft = self.tuto.load_version() + self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto) + + self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + + def test_ensure_access(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access for user + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + self.client.logout() + + # check access for public (get 302, login) + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 302) + + # login with staff + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access for staff (get 200) + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + def test_deletion(self): + """Ensure deletion behavior""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # create a new tutorial + tuto = PublishableContentFactory(type='TUTORIAL') + tuto.authors.add(self.user_author) + tuto.gallery = GalleryFactory() + tuto.licence = self.licence + tuto.save() + + versioned = tuto.load_version() + path = versioned.get_path() + + # delete it + result = self.client.get( + reverse('content:delete', args=[tuto.pk, tuto.slug]), + follow=True) + self.assertEqual(result.status_code, 405) # get method is not allowed for deleting + + result = self.client.post( + reverse('content:delete', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 302) + + self.assertFalse(os.path.isfile(path)) # deletion get right ;) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_public_path']) + if os.path.isdir(settings.MEDIA_ROOT): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/url/__init__.py b/zds/tutorialv2/url/__init__.py new file mode 100644 index 0000000000..a170a28c86 --- /dev/null +++ b/zds/tutorialv2/url/__init__.py @@ -0,0 +1 @@ +__author__ = 'pbeaujea' diff --git a/zds/tutorialv2/url/url_contents.py b/zds/tutorialv2/url/url_contents.py new file mode 100644 index 0000000000..58718bb534 --- /dev/null +++ b/zds/tutorialv2/url/url_contents.py @@ -0,0 +1,22 @@ +# coding: utf-8 + +from django.conf.urls import patterns, url + +from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent + +urlpatterns = patterns('', + url(r'^$', ListContent.as_view(), name='index'), + + # view: + url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(), name='view'), + + # create: + url(r'^nouveau/$', CreateContent.as_view(), name='create'), + + # edit: + url(r'^editer/(?P\d+)/(?P.+)/$', EditContent.as_view(), name='edit'), + + # delete: + url(r'^supprimer/(?P\d+)/(?P.+)/$', DeleteContent.as_view(), name='delete'), + + ) diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 956c2d78a2..f53f428b03 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -1,12 +1,12 @@ # coding: utf-8 -from django.conf.urls import patterns, url - -from . import views -from . import feeds -from .views import * +from django.conf.urls import patterns, include, url urlpatterns = patterns('', + url(r'^contenus/', include('zds.tutorialv2.url.url_contents', namespace='content')) +) + +"""urlpatterns = patterns('', # viewing articles url(r'^articles/$', ArticleList.as_view(), name="index-article"), url(r'^articles/flux/rss/$', feeds.LastArticlesFeedRSS(), name='article-feed-rss'), @@ -136,4 +136,4 @@ url(r'^aides/$', TutorialWithHelp.as_view()), ) - +""" diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index b512d0307e..4a9ef3cd02 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -25,6 +25,7 @@ from PIL import Image as ImagePIL from django.conf import settings from django.contrib import messages +from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied @@ -40,9 +41,9 @@ from git import Repo, Actor from lxml import etree -from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ +from forms import ContentForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation, ContentReaction # , ContentRead +from models import PublishableContent, Container, Extract, Validation, ContentReaction, init_new_repo from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now @@ -60,129 +61,137 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView # , UpdateView +from django.views.generic import ListView, DetailView, FormView, DeleteView # until we completely get rid of these, import them : from zds.tutorial.models import Tutorial, Chapter, Part +from zds.tutorial.forms import TutorialForm -class ArticleList(ListView): +class ListContent(ListView): """ - Displays the list of published articles. + Displays the list of offline contents (written by user) """ - context_object_name = 'articles' - paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type = "ARTICLE" - template_name = 'article/index.html' - tag = None + context_object_name = 'contents' + template_name = 'tutorialv2/index.html' + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(ListContent, self).dispatch(*args, **kwargs) def get_queryset(self): """ - Filter the content to obtain the list of only articles. If tag parameter is provided, only articles - which have this category will be listed. + Filter the content to obtain the list of content written by current user :return: list of articles """ - if self.request.GET.get('tag') is not None: - self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) - query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ - .exclude(sha_public='') - if self.tag is not None: - query_set = query_set.filter(subcategory__in=[self.tag]) - return query_set.order_by('-pubdate') + query_set = PublishableContent.objects.all().filter(authors__in=[self.request.user]) + return query_set def get_context_data(self, **kwargs): - context = super(ArticleList, self).get_context_data(**kwargs) - context['tag'] = self.tag - # TODO in database, the information concern the draft, so we have to make stuff here ! + """Separate articles and tutorials""" + context = super(ListContent, self).get_context_data(**kwargs) + context['articles'] = [] + context['tutorials'] = [] + for content in self.get_queryset(): + versioned = content.load_version() + if content.type == 'ARTICLE': + context['articles'].append(versioned) + else: + context['tutorials'].append(versioned) return context -class TutorialList(ArticleList): - """Displays the list of published tutorials.""" +class CreateContent(FormView): + template_name = 'tutorialv2/create/content.html' + model = PublishableContent + form_class = ContentForm + content = None - context_object_name = 'tutorials' - type = "TUTORIAL" - template_name = 'tutorialv2/index.html' + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(CreateContent, self).dispatch(*args, **kwargs) + def form_valid(self, form): + # create the object: + self.content = PublishableContent() + self.content.title = form.cleaned_data['title'] + self.content.description = form.cleaned_data["description"] + self.content.type = form.cleaned_data["type"] + self.content.licence = form.cleaned_data["licence"] -def render_chapter_form(chapter): - if chapter.part: - return ChapterForm({"title": chapter.title, - "introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) - else: + self.content.creation_date = datetime.now() - return \ - EmbdedChapterForm({"introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) + # Creating the gallery + gal = Gallery() + gal.title = form.cleaned_data["title"] + gal.slug = slugify(form.cleaned_data["title"]) + gal.pubdate = datetime.now() + gal.save() + # Attach user to gallery + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = self.request.user + userg.save() + self.content.gallery = gal -class TutorialWithHelp(TutorialList): - """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta - for more documentation, have a look to ZEP 03 specification (fr)""" - context_object_name = 'tutorials' - template_name = 'tutorialv2/help.html' + # create image: + if "image" in self.request.FILES: + img = Image() + img.physical = self.request.FILES["image"] + img.gallery = gal + img.title = self.request.FILES["image"] + img.slug = slugify(self.request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + self.content.image = img - def get_queryset(self): - """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects\ - .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - try: - type_filter = self.request.GET.get('type') - query_set = query_set.filter(helps_title__in=[type_filter]) - except KeyError: - # if no filter, no need to change - pass - return query_set + self.content.save() - def get_context_data(self, **kwargs): - """Add all HelpWriting objects registered to the context so that the template can use it""" - context = super(TutorialWithHelp, self).get_context_data(**kwargs) - context['helps'] = HelpWriting.objects.all() - return context + # We need to save the tutorial before changing its author list since it's a many-to-many relationship + self.content.authors.add(self.request.user) -# TODO ArticleWithHelp + # Add subcategories on tutorial + for subcat in form.cleaned_data["subcategory"]: + self.content.subcategory.add(subcat) + + # Add helps if needed + for helpwriting in form.cleaned_data["helps"]: + self.content.helps.add(helpwriting) + + self.content.save() + + # create a new repo : + init_new_repo(self.content, + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) + + return super(CreateContent, self).form_valid(form) + + def get_success_url(self): + if self.content: + return reverse('content:view', args=[self.content.pk, self.content.slug]) + else: + return reverse('content:index') class DisplayContent(DetailView): - """Base class that can show any content in any state, by default it shows offline tutorials""" + """Base class that can show any content in any state""" model = PublishableContent - template_name = 'tutorialv2/view.html' - type = "TUTORIAL" + template_name = 'tutorialv2/view/content.html' online = False sha = None - # TODO compatibility should be performed into class `PublishableContent.load_version()` ! - def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): - dictionary["tutorial"] = content - dictionary["path"] = content.get_repo_path() - dictionary["slug"] = slugify(dictionary["title"]) - dictionary["position_in_tutorial"] = cpt_p - - cpt_c = 1 - for chapter in dictionary["chapters"]: - chapter["part"] = dictionary - chapter["slug"] = slugify(chapter["title"]) - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - self.compatibility_chapter(content, repo, sha, chapter) - cpt_c += 1 - - def compatibility_chapter(self, content, repo, sha, dictionary): - """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_repo_path() - dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") - dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md") - cpt = 1 - for ext in dictionary["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = content.get_repo_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt += 1 + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(DisplayContent, self).dispatch(*args, **kwargs) def get_forms(self, context, content): """get all the auxiliary forms about validation, js fiddle...""" @@ -206,11 +215,11 @@ def get_forms(self, context, content): context["formReject"] = form_reject, def get_object(self, queryset=None): - return get_object_or_404(PublishableContent, slug=self.kwargs['content_slug']) + # TODO : check slug ? + return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" - # TODO: handling public version ! context = super(DisplayContent, self).get_context_data(**kwargs) content = context['object'] @@ -234,6 +243,10 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied + # check if slug is good: + if self.kwargs['slug'] != content.slug: + raise Http404 + # load versioned file versioned_tutorial = content.load_version(sha) @@ -243,13 +256,208 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = versioned_tutorial + context["content"] = versioned_tutorial context["version"] = sha self.get_forms(context, content) return context +class EditContent(FormView): + template_name = 'tutorialv2/edit/content.html' + model = PublishableContent + form_class = ContentForm + content = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(EditContent, self).dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + # TODO: check slug ? + return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + versioned = context['content'] + initial = super(EditContent, self).get_initial() + + initial['title'] = versioned.title + initial['description'] = versioned.description + initial['type'] = versioned.type + initial['introduction'] = versioned.get_introduction() + initial['conclusion'] = versioned.get_conclusion() + initial['licence'] = versioned.licence + initial['subcategory'] = self.content.subcategory.all() + initial['helps'] = self.content.helps.all() + + return initial + + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditContent, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + + return context + + def form_valid(self, form): + # TODO: tutorial <-> article + context = self.get_context_data() + versioned = context['content'] + + # first, update DB (in order to get a new slug if needed) + self.content.title = form.cleaned_data['title'] + self.content.description = form.cleaned_data["description"] + self.content.licence = form.cleaned_data["licence"] + + self.content.update_date = datetime.now() + + # update gallery and image: + gal = Gallery.objects.filter(pk=self.content.gallery.pk) + gal.update(title=self.content.title) + gal.update(slug=slugify(self.content.title)) + gal.update(update=datetime.now()) + + if "image" in self.request.FILES: + img = Image() + img.physical = self.request.FILES["image"] + img.gallery = self.content.gallery + img.title = self.request.FILES["image"] + img.slug = slugify(self.request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + self.content.image = img + + self.content.save() + + # now, update the versioned information + versioned.description = form.cleaned_data['description'] + versioned.licence = form.cleaned_data['licence'] + + sha = versioned.repo_update_top_container(form.cleaned_data['title'], + self.content.slug, + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) + + # update relationships : + self.content.sha_draft = sha + + self.content.subcategory.clear() + for subcat in form.cleaned_data["subcategory"]: + self.content.subcategory.add(subcat) + + self.content.helps.clear() + for help in form.cleaned_data["helps"]: + self.content.helps.add(help) + + self.content.save() + + return super(EditContent, self).form_valid(form) + + def get_success_url(self): + return reverse('content:view', args=[self.content.pk, self.content.slug]) + + +class DeleteContent(DeleteView): + model = PublishableContent + template_name = None + http_method_names = [u'delete', u'post'] + object = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(DeleteContent, self).dispatch(*args, **kwargs) + + def get_queryset(self): + # TODO: check slug ? + qs = super(DeleteContent, self).get_queryset() + return qs.filter(pk=self.kwargs['pk']) + + def delete(self, request, *args, **kwargs): + """rewrite delete() function to ensure repository deletion""" + self.object = self.get_object() + self.object.repo_delete() + + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('content:index') + + +class ArticleList(ListView): + """ + Displays the list of published articles. + """ + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type = "ARTICLE" + template_name = 'article/index.html' + tag = None + + def get_queryset(self): + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') + + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! + return context + + +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" + + context_object_name = 'tutorials' + type = "TUTORIAL" + template_name = 'tutorialv2/index.html' + + +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorialv2/help.html' + + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set + + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context + +# TODO ArticleWithHelp + + class DisplayDiff(DetailView): """Display the difference between two version of a content. Reference is always HEAD and compared version is a GET query parameter named sha @@ -284,10 +492,6 @@ def get_context_data(self, **kwargs): return context -class DisplayArticle(DisplayContent): - type = "ARTICLE" - - class DisplayOnlineContent(DisplayContent): """Display online tutorial""" type = "TUTORIAL" @@ -399,6 +603,18 @@ class DisplayOnlineArticle(DisplayOnlineContent): type = "ARTICLE" +def render_chapter_form(chapter): + if chapter.part: + return ChapterForm({"title": chapter.title, + "introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + else: + + return \ + EmbdedChapterForm({"introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + + # Staff actions. diff --git a/zds/urls.py b/zds/urls.py index 0ffbb375b6..c061eecd17 100644 --- a/zds/urls.py +++ b/zds/urls.py @@ -75,7 +75,7 @@ def location(self, article): urlpatterns = patterns('', url(r'^tutoriels/', include('zds.tutorial.urls')), url(r'^articles/', include('zds.article.urls')), - url(r'^contenu/', include('zds.tutorialv2.urls')), + url(r'^', include('zds.tutorialv2.urls')), url(r'^forums/', include('zds.forum.urls')), url(r'^mp/', include('zds.mp.urls')), url(r'^membres/', include('zds.member.urls')), From 0ef4fe72141f0cf8e2fe61fdb9afb09271b7ce76 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 10:47:17 +0100 Subject: [PATCH 020/887] Ajout d'un fichier de migration oublie --- ...auto__add_field_publishablecontent_slug.py | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py diff --git a/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py b/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py new file mode 100644 index 0000000000..3d836c0669 --- /dev/null +++ b/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'PublishableContent.slug' + db.add_column(u'tutorialv2_publishablecontent', 'slug', + self.gf('django.db.models.fields.CharField')(default='', max_length=80), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PublishableContent.slug' + db.delete_column(u'tutorialv2_publishablecontent', 'slug') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'helps': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['utils.HelpWriting']", 'db_index': 'True', 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.helpwriting': { + 'Meta': {'object_name': 'HelpWriting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '20'}), + 'tablelabel': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file From 4c1ffa299298be279d18daacfc0b3b5b29ecfa41 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 11:11:31 +0100 Subject: [PATCH 021/887] Force le slug a etre correct --- zds/tutorialv2/models.py | 3 ++- zds/tutorialv2/views.py | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 63cf578acf..7f383f9daf 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -830,7 +830,8 @@ def load_version(self, sha=None, public=False): if 'licence' in json: versioned.licence = Licence.objects.filter(code=json['licence']).first() else: - versioned.licence = Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + versioned.licence = \ + Licence.objects.filter(pk=settings.ZDS_APP['tutorial']['default_license_pk']).first() if 'introduction' in json: versioned.introduction = json['introduction'] diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4a9ef3cd02..8a44cdd1a9 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -111,7 +111,6 @@ class CreateContent(FormView): @method_decorator(login_required) @method_decorator(can_write_and_read_now) def dispatch(self, *args, **kwargs): - """rewrite this method to ensure decoration""" return super(CreateContent, self).dispatch(*args, **kwargs) def form_valid(self, form): @@ -190,7 +189,6 @@ class DisplayContent(DetailView): @method_decorator(login_required) def dispatch(self, *args, **kwargs): - """rewrite this method to ensure decoration""" return super(DisplayContent, self).dispatch(*args, **kwargs) def get_forms(self, context, content): @@ -212,11 +210,13 @@ def get_forms(self, context, content): context["formAskValidation"] = form_ask_validation context["formJs"] = form_js context["formValid"] = form_valid - context["formReject"] = form_reject, + context["formReject"] = form_reject def get_object(self, queryset=None): - # TODO : check slug ? - return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" @@ -243,10 +243,6 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # check if slug is good: - if self.kwargs['slug'] != content.slug: - raise Http404 - # load versioned file versioned_tutorial = content.load_version(sha) @@ -276,8 +272,10 @@ def dispatch(self, *args, **kwargs): return super(EditContent, self).dispatch(*args, **kwargs) def get_object(self, queryset=None): - # TODO: check slug ? - return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_initial(self): """rewrite function to pre-populate form""" @@ -374,10 +372,11 @@ def dispatch(self, *args, **kwargs): """rewrite this method to ensure decoration""" return super(DeleteContent, self).dispatch(*args, **kwargs) - def get_queryset(self): - # TODO: check slug ? - qs = super(DeleteContent, self).get_queryset() - return qs.filter(pk=self.kwargs['pk']) + def get_object(self, queryset=None): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def delete(self, request, *args, **kwargs): """rewrite delete() function to ensure repository deletion""" From 3acccec7167bbfbcc811b357f78ea2f896844728 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 21:56:06 +0100 Subject: [PATCH 022/887] Ajoute de features Possibilite d'ajouter, d'editer et de supprimer des conteneurs et extraits !! --- templates/tutorialv2/base.html | 8 +- templates/tutorialv2/create/container.html | 38 + templates/tutorialv2/create/content.html | 4 - templates/tutorialv2/create/extract.html | 43 + templates/tutorialv2/edit/container.html | 39 + templates/tutorialv2/edit/content.html | 4 - templates/tutorialv2/edit/extract.html | 45 + templates/tutorialv2/includes/child.part.html | 42 + .../tutorialv2/includes/delete.part.html | 16 + .../includes/tags_authors.part.html | 6 +- templates/tutorialv2/view/container.html | 202 + templates/tutorialv2/view/content.html | 23 +- zds/tutorialv2/forms.py | 120 +- zds/tutorialv2/models.py | 327 +- zds/tutorialv2/url/url_contents.py | 59 +- zds/tutorialv2/views.py | 3296 ++++------------- 16 files changed, 1594 insertions(+), 2678 deletions(-) create mode 100644 templates/tutorialv2/create/container.html create mode 100644 templates/tutorialv2/create/extract.html create mode 100644 templates/tutorialv2/edit/container.html create mode 100644 templates/tutorialv2/edit/extract.html create mode 100644 templates/tutorialv2/includes/child.part.html create mode 100644 templates/tutorialv2/includes/delete.part.html create mode 100644 templates/tutorialv2/view/container.html diff --git a/templates/tutorialv2/base.html b/templates/tutorialv2/base.html index 73ef8519d1..2c8ba30155 100644 --- a/templates/tutorialv2/base.html +++ b/templates/tutorialv2/base.html @@ -4,20 +4,20 @@ {% block title_base %} - • {% trans "Tutoriels" %} + • {% trans "Mes contenus" %} {% endblock %} {% block mobile_title %} - {% trans "Tutoriels" %} + {% trans "Mes contenus" %} {% endblock %} {% block breadcrumb_base %} - {% if user in tutorial.authors.all %} -
  • {% trans "Mes tutoriels" %}
  • + {% if user in content.authors.all %} +
  • {% trans "Mes contenus" %}
  • {% else %}
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Nouveau conteneur" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/create/content.html b/templates/tutorialv2/create/content.html index 0ab6c81cdb..5643d67e23 100644 --- a/templates/tutorialv2/create/content.html +++ b/templates/tutorialv2/create/content.html @@ -7,10 +7,6 @@ {% trans "Nouveau contenu" %} {% endblock %} -{% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • -{% endblock %} - {% block breadcrumb %}
  • {% trans "Nouveau contenu" %}
  • {% endblock %} diff --git a/templates/tutorialv2/create/extract.html b/templates/tutorialv2/create/extract.html new file mode 100644 index 0000000000..5c267b9f01 --- /dev/null +++ b/templates/tutorialv2/create/extract.html @@ -0,0 +1,43 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Nouvel extrait" %} +{% endblock %} + + + +{% block headline %} +

    + {% trans "Nouvel extrait" %} +

    +{% endblock %} + + + +{% block breadcrumb %} + + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Nouvel extrait" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} + + {% if form.text.value %} + {% include "misc/previsualization.part.html" with text=form.text.value %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/container.html b/templates/tutorialv2/edit/container.html new file mode 100644 index 0000000000..d9e771fb3f --- /dev/null +++ b/templates/tutorialv2/edit/container.html @@ -0,0 +1,39 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Editer un conteneur" %} +{% endblock %} + +{% block breadcrumb %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Editer le conteneur" %}
  • +{% endblock %} + +{% block headline %} +

    + {% trans "Éditer" %} : {{ container.title }} +

    +{% endblock %} + + + +{% block content %} + {% if new_version %} +

    + {% trans "Une nouvelle version a été postée avant que vous ne validiez" %}. +

    + {% endif %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html index 9d4a2cc26c..2f05ddcdae 100644 --- a/templates/tutorialv2/edit/content.html +++ b/templates/tutorialv2/edit/content.html @@ -7,10 +7,6 @@ {% trans "Éditer le contenu" %} {% endblock %} -{% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • -{% endblock %} - {% block breadcrumb %}
  • {{ content.title }}
  • {% trans "Éditer le contenu" %}
  • diff --git a/templates/tutorialv2/edit/extract.html b/templates/tutorialv2/edit/extract.html new file mode 100644 index 0000000000..65c5a37773 --- /dev/null +++ b/templates/tutorialv2/edit/extract.html @@ -0,0 +1,45 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Éditer l'extrait" %} +{% endblock %} + + + +{% block headline %} +

    + {% trans "Éditer l'extrait" %} +

    +{% endblock %} + + + +{% block breadcrumb %} + + {% with container=extract.container %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + {% endwith %} + +
  • {% trans "Éditer l'extrait" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} + + {% if form.text.value %} + {% include "misc/previsualization.part.html" with text=form.text.value %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html new file mode 100644 index 0000000000..683fea4a72 --- /dev/null +++ b/templates/tutorialv2/includes/child.part.html @@ -0,0 +1,42 @@ +{% load emarkdown %} +{% load i18n %} + + +

    + + {{ child.title }} + +

    + +{% if user in content.authors.all or perms.tutorial.change_tutorial %} +
    + + {% trans "Éditer" %} + + + {% include "tutorialv2/includes/delete.part.html" with object=child additional_classes="ico-after cross btn btn-grey" %} +
    +{% endif %} + +{% if child.text %} + {# child is an extract #} + + {{ child.get_text|emarkdown }} + +{% else %} + {# child is a container #} + + {% if child.children %} +
      + {% for subchild in child.children %} +
    1. + {{ subchild.title }} +
    2. + {% endfor %} +
    + {% else %} +

    + {% trans "Ce conteneur est actuelement vide" %}. +

    + {% endif %} +{% endif %} diff --git a/templates/tutorialv2/includes/delete.part.html b/templates/tutorialv2/includes/delete.part.html new file mode 100644 index 0000000000..f42b4caf18 --- /dev/null +++ b/templates/tutorialv2/includes/delete.part.html @@ -0,0 +1,16 @@ +{% load i18n %} + +{# note : ico-after cross btn btn-grey #} + +{% trans "Supprimer" %} + \ No newline at end of file diff --git a/templates/tutorialv2/includes/tags_authors.part.html b/templates/tutorialv2/includes/tags_authors.part.html index 5fb924fd4e..319f275022 100644 --- a/templates/tutorialv2/includes/tags_authors.part.html +++ b/templates/tutorialv2/includes/tags_authors.part.html @@ -13,11 +13,11 @@ -{% if content.update %} +{% if content.update_date %} {% trans "Dernière mise à jour" %} : - {% endif %} diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html new file mode 100644 index 0000000000..13cb0e5ca7 --- /dev/null +++ b/templates/tutorialv2/view/container.html @@ -0,0 +1,202 @@ +{% extends "tutorialv2/base.html" %} +{% load set %} +{% load thumbnail %} +{% load emarkdown %} +{% load i18n %} + + +{% block title %} + {{ container.title }} - {{ content.title }} +{% endblock %} + + + +{% block breadcrumb %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if content.licence %} +

    + {{ content.licence }} +

    + {% endif %} + +

    + {{ container.title }} +

    + + {% include 'tutorialv2/includes/tags_authors.part.html' %} + + {% if content.is_beta %} +
    +
    + {% blocktrans %}Cette version du tutoriel est en BÊTA !{% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + + +{% block content %} + {% if container.get_introduction and container.get_introduction != "None" %} + {{ container.get_introduction|emarkdown }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + + +
    + + {% for child in container.children %} + {% include "tutorialv2/includes/child.part.html" with child=child %} + {% empty %} +

    + {% trans "Ce conteneur est actuelement vide" %}. +

    + {% endfor %} + +
    + + {% if container.get_conclusion and container.get_conclusion != "None" %} + {{ container.get_conclusion|emarkdown }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in content.authors.all or perms.tutorial.change_tutorial %} + + + {% trans "Éditer" %} + + + + {% if container.can_add_container %} + + {% trans "Ajouter un conteneur" %} + + {% endif %} + + {% if container.can_add_extract %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if chapter.part %} +
  • + + {% blocktrans %} + Déplacer le chapitre + {% endblocktrans %} + + +
  • + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {# include "tutorial/includes/summary.part.html" with tutorial=tutorial chapter_current=chapter #} + + {% if user in content.authors.all or perms.content.change_content %} + + {% endif %} +{% endblock %} diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 66daa60f08..20ed95446c 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -12,13 +12,6 @@ {% endblock %} -{% if user in content.authors.all or perms.content.change_content %} - {% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • - {% endblock %} -{% endif %} - - {% block breadcrumb %}
  • {{ content.title }}
  • {% endblock %} @@ -119,8 +112,8 @@


    - {% for child in content.childreen %} - {# include stuff #} + {% for child in content.children %} + {% include "tutorialv2/includes/child.part.html" with child=child %} {% empty %}

    {% trans "Ce contenu est actuelement vide" %}. @@ -147,6 +140,18 @@

    {% trans "Éditer" %} + {% if content.can_add_container %} + + {% trans "Ajouter un conteneur" %} + + {% endif %} + + {% if content.can_add_extract %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + {% else %} {% trans "Version brouillon" %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 7bd5236838..beeb1ffdd9 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -4,7 +4,7 @@ from crispy_forms.bootstrap import StrictButton from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Layout, Fieldset, Submit, Field, \ +from crispy_forms.layout import HTML, Layout, Submit, Field, \ ButtonHolder, Hidden from django.core.urlresolvers import reverse @@ -157,7 +157,7 @@ def __init__(self, *args, **kwargs): disabled=True) -class PartForm(FormWithTitle): +class ContainerForm(FormWithTitle): introduction = forms.CharField( label=_(u"Introduction"), @@ -191,7 +191,7 @@ class PartForm(FormWithTitle): ) def __init__(self, *args, **kwargs): - super(PartForm, self).__init__(*args, **kwargs) + super(ContainerForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' @@ -206,122 +206,8 @@ def __init__(self, *args, **kwargs): StrictButton( _(u'Valider'), type='submit'), - StrictButton( - _(u'Ajouter et continuer'), - type='submit', - name='submit_continue'), - ) - ) - - -class ChapterForm(FormWithTitle): - - image = forms.ImageField( - label=_(u'Selectionnez le logo du tutoriel ' - u'(max. {0} Ko)').format(str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), - required=False - ) - - introduction = forms.CharField( - label=_(u'Introduction'), - required=False, - widget=forms.Textarea( - attrs={ - 'placeholder': _(u'Votre message au format Markdown.') - } - ) - ) - - conclusion = forms.CharField( - label=_(u'Conclusion'), - required=False, - widget=forms.Textarea( - attrs={ - 'placeholder': _(u'Votre message au format Markdown.') - } - ) - ) - - msg_commit = forms.CharField( - label=_(u"Message de suivi"), - max_length=80, - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': _(u'Un résumé de vos ajouts et modifications') - } - ) - ) - - def __init__(self, *args, **kwargs): - super(ChapterForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_class = 'content-wrapper' - self.helper.form_method = 'post' - - self.helper.layout = Layout( - Field('title'), - Field('image'), - Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor'), - Field('msg_commit'), - Hidden('last_hash', '{{ last_hash }}'), - ButtonHolder( - StrictButton( - _(u'Valider'), - type='submit'), - StrictButton( - _(u'Ajouter et continuer'), - type='submit', - name='submit_continue'), - )) - - -class EmbdedChapterForm(forms.Form): - introduction = forms.CharField( - required=False, - widget=forms.Textarea - ) - - image = forms.ImageField( - label=_(u'Sélectionnez une image'), - required=False) - - conclusion = forms.CharField( - required=False, - widget=forms.Textarea - ) - - msg_commit = forms.CharField( - label=_(u'Message de suivi'), - max_length=80, - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': _(u'Un résumé de vos ajouts et modifications') - } - ) - ) - - def __init__(self, *args, **kwargs): - self.helper = FormHelper() - self.helper.form_class = 'content-wrapper' - self.helper.form_method = 'post' - - self.helper.layout = Layout( - Fieldset( - _(u'Contenu'), - Field('image'), - Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor'), - Field('msg_commit'), - Hidden('last_hash', '{{ last_hash }}'), - ), - ButtonHolder( - Submit('submit', _(u'Valider')) ) ) - super(EmbdedChapterForm, self).__init__(*args, **kwargs) class ExtractForm(FormWithTitle): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 7f383f9daf..21e03de321 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -19,6 +19,7 @@ from django.db import models from datetime import datetime from git.repo import Repo +from django.core.exceptions import PermissionDenied from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user @@ -166,6 +167,25 @@ def add_slug_to_pool(self, slug): else: raise Exception('slug "{}" already in the slug pool !'.format(slug)) + def can_add_container(self): + """ + :return: True if this container accept child container, false otherwise + """ + if not self.has_extracts(): + if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth']-1: + if self.top_container().type != 'ARTICLE': + return True + return False + + def can_add_extract(self): + """ + :return: True if this container accept child extract, false otherwise + """ + if not self.has_sub_containers(): + if self.get_tree_depth() <= ZDS_APP['content']['max_tree_depth']: + return True + return False + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. @@ -173,20 +193,17 @@ def add_container(self, container, generate_slug=False): :param container: the new container :param generate_slug: if `True`, ask the top container an unique slug for this object """ - if not self.has_extracts(): - if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth'] and self.top_container().type != 'ARTICLE': - if generate_slug: - container.slug = self.get_unique_slug(container.title) - else: - self.add_slug_to_pool(container.slug) - container.parent = self - container.position_in_parent = self.get_last_child_position() + 1 - self.children.append(container) - self.children_dict[container.slug] = container + if self.can_add_container(): + if generate_slug: + container.slug = self.get_unique_slug(container.title) else: - raise InvalidOperationError("Cannot add another level to this content") + self.add_slug_to_pool(container.slug) + container.parent = self + container.position_in_parent = self.get_last_child_position() + 1 + self.children.append(container) + self.children_dict[container.slug] = container else: - raise InvalidOperationError("Can't add a container if this container already contains extracts.") + raise InvalidOperationError("Cannot add another level to this container") def add_extract(self, extract, generate_slug=False): """ @@ -194,7 +211,7 @@ def add_extract(self, extract, generate_slug=False): :param extract: the new extract :param generate_slug: if `True`, ask the top container an unique slug for this object """ - if not self.has_sub_containers(): + if self.can_add_extract(): if generate_slug: extract.slug = self.get_unique_slug(extract.title) else: @@ -213,6 +230,7 @@ def update_children(self): changed. Note : this function does not account for a different arrangement of the files. """ + # TODO : path comparison instead of pure rewritring ? self.introduction = os.path.join(self.get_path(relative=True), "introduction.md") self.conclusion = os.path.join(self.get_path(relative=True), "conclusion.md") for child in self.children: @@ -244,6 +262,42 @@ def get_prod_path(self): base = self.parent.get_prod_path() return os.path.join(base, self.slug) + def get_absolute_url(self): + """ + :return: url to access the container + """ + return self.top_container().get_absolute_url() + self.get_path(relative=True) + '/' + + def get_edit_url(self): + """ + :return: url to edit the container + """ + slugs = [self.slug] + parent = self.parent + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.top_container().pk] + args.extend(slugs) + + return reverse('content:edit-container', args=args) + + def get_delete_url(self): + """ + :return: url to edit the container + """ + slugs = [self.slug] + parent = self.parent + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.top_container().pk] + args.extend(slugs) + + return reverse('content:delete', args=args) + def get_introduction(self): """ :return: the introduction from the file in `self.introduction` @@ -270,19 +324,26 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): :return : commit sha """ + repo = self.top_container().repository + # update title if title != self.title: self.title = title if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed + old_path = self.get_path(relative=True) self.slug = self.top_container().get_unique_slug(title) + new_path = self.get_path(relative=True) + repo.index.move([old_path, new_path]) + + # update manifest self.update_children() - # TODO : and move() !!! # update introduction and conclusion + rel_path = self.get_path(relative=True) if self.introduction is None: - self.introduction = self.get_path(relative=True) + 'introduction.md' + self.introduction = os.path.join(rel_path, 'introduction.md') if self.conclusion is None: - self.conclusion = self.get_path(relative=True) + 'conclusion.md' + self.conclusion = os.path.join(rel_path, 'conclusion.md') path = self.top_container().get_path() f = open(os.path.join(path, self.introduction), "w") @@ -293,8 +354,6 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): f.close() self.top_container().dump_json() - - repo = self.top_container().repository repo.index.add(['manifest.json', self.introduction, self.conclusion]) if commit_message == '': @@ -303,9 +362,110 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): return cm.hexsha - # TODO: - # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) - # - the `maj_repo_*()` stuffs should probably be into the model ? + def repo_add_container(self, title, introduction, conclusion, commit_message=''): + """ + :param title: title of the new container + :param introduction: text of its introduction + :param conclusion: text of its conclusion + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + subcontainer = Container(title) + + # can a subcontainer be added ? + try: + self.add_container(subcontainer, generate_slug=True) + except Exception: + raise PermissionDenied + + # create directory + repo = self.top_container().repository + path = self.top_container().get_path() + rel_path = subcontainer.get_path(relative=True) + os.makedirs(os.path.join(path, rel_path), mode=0o777) + + repo.index.add([rel_path]) + + # create introduction and conclusion + subcontainer.introduction = os.path.join(rel_path, 'introduction.md') + subcontainer.conclusion = os.path.join(rel_path, 'conclusion.md') + + f = open(os.path.join(path, subcontainer.introduction), "w") + f.write(introduction.encode('utf-8')) + f.close() + f = open(os.path.join(path, subcontainer.conclusion), "w") + f.write(conclusion.encode('utf-8')) + f.close() + + # commit + self.top_container().dump_json() + repo.index.add(['manifest.json', subcontainer.introduction, subcontainer.conclusion]) + + if commit_message == '': + commit_message = u'Création du conteneur « ' + title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_add_extract(self, title, text, commit_message=''): + """ + :param title: title of the new extract + :param text: text of the new extract + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + extract = Extract(title) + + # can an extract be added ? + try: + self.add_extract(extract, generate_slug=True) + except Exception: + raise PermissionDenied + + # create text + repo = self.top_container().repository + path = self.top_container().get_path() + + extract.text = extract.get_path(relative=True) + f = open(os.path.join(path, extract.text), "w") + f.write(text.encode('utf-8')) + f.close() + + # commit + self.top_container().dump_json() + repo.index.add(['manifest.json', extract.text]) + + if commit_message == '': + commit_message = u'Création de l\'extrait « ' + title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_delete(self, commit_message=''): + """ + :param commit_message: commit message used instead of default one if provided + :return: commit sha + """ + path = self.get_path(relative=True) + repo = self.top_container().repository + repo.index.remove([path], r=True) + shutil.rmtree(self.get_path()) # looks like removing from git is not enough !! + + # now, remove from manifest + # work only if slug is correct + top = self.top_container() + top.children_dict.pop(self.slug) + top.children.pop(top.children.index(self)) + + # commit + top.dump_json() + repo.index.add(['manifest.json']) + + if commit_message == '': + commit_message = u'Suppression du conteneur « {} »'.format(self.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha class Extract: @@ -318,14 +478,14 @@ class Extract: title = '' slug = '' container = None - position_in_container = 1 + position_in_parent = 1 text = None - def __init__(self, title, slug='', container=None, position_in_container=1): + def __init__(self, title, slug='', container=None, position_in_parent=1): self.title = title self.slug = slug self.container = container - self.position_in_container = position_in_container + self.position_in_parent = position_in_parent def __unicode__(self): return u''.format(self.title) @@ -336,8 +496,8 @@ def get_absolute_url(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) def get_absolute_url_online(self): @@ -346,8 +506,8 @@ def get_absolute_url_online(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_online(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) def get_absolute_url_beta(self): @@ -356,10 +516,40 @@ def get_absolute_url_beta(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_beta(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) + def get_edit_url(self): + """ + :return: url to edit the extract + """ + slugs = [self.slug] + parent = self.container + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.container.top_container().pk] + args.extend(slugs) + + return reverse('content:edit-extract', args=args) + + def get_delete_url(self): + """ + :return: url to delete the extract + """ + slugs = [self.slug] + parent = self.container + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.container.top_container().pk] + args.extend(slugs) + + return reverse('content:delete', args=args) + def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. @@ -377,16 +567,69 @@ def get_text(self): self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) - def get_text_online(self): + def repo_update(self, title, text, commit_message=''): + """ + :param title: new title of the extract + :param text: new text of the extract + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + + repo = self.container.top_container().repository + + if title != self.title: + # get a new slug + old_path = self.get_path(relative=True) + self.title = title + self.slug = self.container.get_unique_slug(title) + new_path = self.get_path(relative=True) + # move file + repo.index.move([old_path, new_path]) + + # edit text + path = self.container.top_container().get_path() + + self.text = self.get_path(relative=True) + f = open(os.path.join(path, self.text), "w") + f.write(text.encode('utf-8')) + f.close() + + # commit + self.container.top_container().dump_json() + repo.index.add(['manifest.json', self.text]) + + if commit_message == '': + commit_message = u'Modification de l\'extrait « {} », situé dans le conteneur « {} »'\ + .format(self.title, self.container.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_delete(self, commit_message=''): """ - :return: public text of the extract + :param commit_message: commit message used instead of default one if provided + :return: commit sha """ - path = self.container.top_container().get_prod_path() + self.text + '.html' - if os.path.exists(path): - txt = open(path) - txt_content = txt.read() - txt.close() - return txt_content.decode('utf-8') + path = self.get_path(relative=True) + repo = self.container.top_container().repository + repo.index.remove([path]) + os.remove(self.get_path()) # looks like removing from git is not enough + + # now, remove from manifest + # work only if slug is correct!! + top = self.container + top.children_dict.pop(self.slug, None) + top.children.pop(top.children.index(self)) + + # commit + top.top_container().dump_json() + repo.index.add(['manifest.json']) + + if commit_message == '': + commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha class VersionedContent(Container): @@ -464,12 +707,6 @@ def get_absolute_url_beta(self): else: return self.get_absolute_url() - def get_edit_url(self): - """ - :return: the url to edit the tutorial - """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) - def get_path(self, relative=False): """ Get the physical path to the draft version of the Content. @@ -486,7 +723,7 @@ def get_prod_path(self): Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP['contents']['repo_public_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_public_path'], self.slug) def get_json(self): """ diff --git a/zds/tutorialv2/url/url_contents.py b/zds/tutorialv2/url/url_contents.py index 58718bb534..e22c50492d 100644 --- a/zds/tutorialv2/url/url_contents.py +++ b/zds/tutorialv2/url/url_contents.py @@ -2,21 +2,78 @@ from django.conf.urls import patterns, url -from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent +from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent,\ + CreateContainer, DisplayContainer, EditContainer, CreateExtract, EditExtract, DeleteContainerOrExtract urlpatterns = patterns('', url(r'^$', ListContent.as_view(), name='index'), # view: + url(r'^(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + DisplayContainer.as_view(), + name='view-container'), + url(r'^(?P\d+)/(?P.+)/(?P.+)/$', + DisplayContainer.as_view(), + name='view-container'), + url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(), name='view'), # create: url(r'^nouveau/$', CreateContent.as_view(), name='create'), + url(r'^nouveau-conteneur/(?P\d+)/(?P.+)/(?P.+)/$', + CreateContainer.as_view(), + name='create-container'), + url(r'^nouveau-conteneur/(?P\d+)/(?P.+)/$', + CreateContainer.as_view(), + name='create-container'), + + + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + # edit: + url(r'^editer-conteneur/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + EditContainer.as_view(), + name='edit-container'), + url(r'^editer-conteneur/(?P\d+)/(?P.+)/(?P.+)/$', + EditContainer.as_view(), + name='edit-container'), + + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer/(?P\d+)/(?P.+)/$', EditContent.as_view(), name='edit'), # delete: + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/$', DeleteContent.as_view(), name='delete'), ) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8a44cdd1a9..814f39ecdb 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -4,7 +4,6 @@ from datetime import datetime from operator import attrgetter from urllib import urlretrieve -from django.contrib.humanize.templatetags.humanize import naturaltime from urlparse import urlparse, parse_qs try: import ujson as json_reader @@ -38,33 +37,26 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.encoding import smart_str from django.views.decorators.http import require_POST -from git import Repo, Actor -from lxml import etree +from git import Repo -from forms import ContentForm, PartForm, ChapterForm, EmbdedChapterForm, \ - ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm +from forms import ContentForm, ContainerForm, \ + ExtractForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm from models import PublishableContent, Container, Extract, Validation, ContentReaction, init_new_repo from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now -from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip -from zds.forum.models import Forum, Topic from zds.utils import slugify from zds.utils.models import Alert -from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ +from zds.utils.models import Category, CommentLike, CommentDislike, \ SubCategory, HelpWriting from zds.utils.mps import send_mp -from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic from zds.utils.paginator import paginator_range from zds.utils.templatetags.emarkdown import emarkdown -from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive -from zds.utils.misc import compute_hash, content_has_changed +from zds.utils.tutorials import get_blob, export_tutorial_to_md from django.utils.translation import ugettext as _ from django.views.generic import ListView, DetailView, FormView, DeleteView -# until we completely get rid of these, import them : from zds.tutorial.models import Tutorial, Chapter, Part -from zds.tutorial.forms import TutorialForm class ListContent(ListView): @@ -173,10 +165,7 @@ def form_valid(self, form): return super(CreateContent, self).form_valid(form) def get_success_url(self): - if self.content: - return reverse('content:view', args=[self.content.pk, self.content.slug]) - else: - return reverse('content:index') + return reverse('content:view', args=[self.content.pk, self.content.slug]) class DisplayContent(DetailView): @@ -389,2144 +378,1023 @@ def get_success_url(self): return reverse('content:index') -class ArticleList(ListView): - """ - Displays the list of published articles. - """ - context_object_name = 'articles' - paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type = "ARTICLE" - template_name = 'article/index.html' - tag = None +class CreateContainer(FormView): + template_name = 'tutorialv2/create/container.html' + form_class = ContainerForm + content = None - def get_queryset(self): - """ - Filter the content to obtain the list of only articles. If tag parameter is provided, only articles - which have this category will be listed. - :return: list of articles - """ - if self.request.GET.get('tag') is not None: - self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) - query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ - .exclude(sha_public='') - if self.tag is not None: - query_set = query_set.filter(subcategory__in=[self.tag]) - return query_set.order_by('-pubdate') + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(CreateContainer, self).dispatch(*args, **kwargs) - def get_context_data(self, **kwargs): - context = super(ArticleList, self).get_context_data(**kwargs) - context['tag'] = self.tag - # TODO in database, the information concern the draft, so we have to make stuff here ! - return context + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(CreateContainer, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() -class TutorialList(ArticleList): - """Displays the list of published tutorials.""" + # get the container: + if 'container_slug' in self.kwargs: + try: + container = context['content'].children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + context['container'] = container + else: + context['container'] = context['content'] + return context - context_object_name = 'tutorials' - type = "TUTORIAL" - template_name = 'tutorialv2/index.html' + def form_valid(self, form): + context = self.get_context_data() + parent = context['container'] + sha = parent.repo_add_container(form.cleaned_data['title'], + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) -class TutorialWithHelp(TutorialList): - """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta - for more documentation, have a look to ZEP 03 specification (fr)""" - context_object_name = 'tutorials' - template_name = 'tutorialv2/help.html' + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - def get_queryset(self): - """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects\ - .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - try: - type_filter = self.request.GET.get('type') - query_set = query_set.filter(helps_title__in=[type_filter]) - except KeyError: - # if no filter, no need to change - pass - return query_set + self.success_url = parent.get_absolute_url() - def get_context_data(self, **kwargs): - """Add all HelpWriting objects registered to the context so that the template can use it""" - context = super(TutorialWithHelp, self).get_context_data(**kwargs) - context['helps'] = HelpWriting.objects.all() - return context + return super(CreateContainer, self).form_valid(form) -# TODO ArticleWithHelp +class DisplayContainer(DetailView): + """Base class that can show any content in any state""" -class DisplayDiff(DetailView): - """Display the difference between two version of a content. - Reference is always HEAD and compared version is a GET query parameter named sha - this class has no reason to be adapted to any content type""" model = PublishableContent - template_name = "tutorial/diff.html" - context_object_name = "tutorial" + template_name = 'tutorialv2/view/container.html' + online = False + sha = None + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(DisplayContainer, self).dispatch(*args, **kwargs) def get_object(self, queryset=None): - return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_context_data(self, **kwargs): + """Show the given tutorial if exists.""" - context = super(DisplayDiff, self).get_context_data(**kwargs) + context = super(DisplayContainer, self).get_context_data(**kwargs) + content = context['object'] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the content. try: - sha = self.request.GET.get("sha") + sha = self.request.GET["version"] except KeyError: - sha = self.get_object().sha_draft + if self.sha is not None: + sha = self.sha + else: + sha = content.sha_draft - if self.request.user not in context[self.context_object_name].authors.all(): + # check that if we ask for beta, we also ask for the sha version + is_beta = content.is_beta(sha) + + if self.request.user not in content.authors.all() and not is_beta: + # if we are not author of this content or if we did not ask for beta + # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # open git repo and find diff between displayed version and head - repo = Repo(context[self.context_object_name].get_repo_path()) - current_version_commit = repo.commit(sha) - diff_with_head = current_version_commit.diff("HEAD~1") - context["path_add"] = diff_with_head.iter_change_type("A") - context["path_ren"] = diff_with_head.iter_change_type("R") - context["path_del"] = diff_with_head.iter_change_type("D") - context["path_maj"] = diff_with_head.iter_change_type("M") - return context - - -class DisplayOnlineContent(DisplayContent): - """Display online tutorial""" - type = "TUTORIAL" - template_name = "tutorial/view_online.html" - - def get_forms(self, context, content): - - # Build form to send a note for the current tutorial. - context['form'] = NoteForm(content, self.request.user) - - def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): - dictionary["tutorial"] = content - dictionary["path"] = content.get_repo_path() - dictionary["slug"] = slugify(dictionary["title"]) - dictionary["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in dictionary["chapters"]: - chapter["part"] = dictionary - chapter["slug"] = slugify(chapter["title"]) - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - self.compatibility_chapter(content, repo, sha, chapter) - cpt_c += 1 + # load versioned file + context['content'] = content.load_version(sha) + container = context['content'] - def compatibility_chapter(self, content, repo, sha, dictionary): - """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_prod_path() - dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") - dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") - # load extracts - cpt = 1 - for ext in dictionary["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = content.get_prod_path() - text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") - ext["txt"] = text.read() - cpt += 1 + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - def get_context_data(self, **kwargs): - content = self.get_object() - # If the tutorial isn't online, we raise 404 error. - if not content.in_public(): + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: raise Http404 - self.sha = content.sha_public - context = super(DisplayOnlineContent, self).get_context_data(**kwargs) - - context["tutorial"]["update"] = content.update - context["tutorial"]["get_note_count"] = content.get_note_count() - if self.request.user.is_authenticated(): - # If the user is authenticated, he may want to tell the world how cool the content is - # We check if he can post a not or not with - # antispam filter. - context['tutorial']['antispam'] = content.antispam() + context['container'] = container - # If the user has never read this before, we mark this tutorial read. - if never_read(content): - mark_read(content) + return context - # Find all notes of the tutorial. - notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() +class EditContainer(FormView): + template_name = 'tutorialv2/edit/container.html' + form_class = ContainerForm + content = None - # Retrieve pk of the last note. If there aren't notes for the tutorial, we - # initialize this last note at 0. + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(EditContainer, self).dispatch(*args, **kwargs) - last_note_pk = 0 - if content.last_note: - last_note_pk = content.last_note.pk + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj - # Handle pagination + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditContainer, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] - paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) - try: - page_nbr = int(self.request.GET.get("page")) - except KeyError: - page_nbr = 1 - except ValueError: - raise Http404 + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - try: - notes = paginator.page(page_nbr) - except PageNotAnInteger: - notes = paginator.page(1) - except EmptyPage: + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: raise Http404 - res = [] - if page_nbr != 1: + context['container'] = container - # Show the last note of the previous page + return context - last_page = paginator.page(page_nbr - 1).object_list - last_note = last_page[len(last_page) - 1] - res.append(last_note) - for note in notes: - res.append(note) + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + container = context['container'] + initial = super(EditContainer, self).get_initial() - context['notes'] = res - context['last_note_pk'] = last_note_pk - context['pages'] = paginator_range(page_nbr, paginator.num_pages) - context['nb'] = page_nbr + initial['title'] = container.title + initial['introduction'] = container.get_introduction() + initial['conclusion'] = container.get_conclusion() + return initial -class DisplayOnlineArticle(DisplayOnlineContent): - type = "ARTICLE" + def form_valid(self, form): + context = self.get_context_data() + container = context['container'] + sha = container.repo_update(form.cleaned_data['title'], + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) -def render_chapter_form(chapter): - if chapter.part: - return ChapterForm({"title": chapter.title, - "introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) - else: + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - return \ - EmbdedChapterForm({"introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) + self.success_url = container.get_absolute_url() + return super(EditContainer, self).form_valid(form) -# Staff actions. +class CreateExtract(FormView): + template_name = 'tutorialv2/create/extract.html' + form_class = ExtractForm + content = None -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -def list_validation(request): - """Display tutorials list in validation.""" - - # Retrieve type of the validation. Default value is all validations. - - try: - type = request.GET["type"] - except KeyError: - type = None - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - - # Orphan validation. There aren't validator attached to the validations. - - if type == "orphan": - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=True, - status="PENDING").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=True, - status="PENDING", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - elif type == "reserved": - - # Reserved validation. There are a validator attached to the - # validations. - - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=False, - status="PENDING_V").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=False, - status="PENDING_V", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - else: - - # Default, we display all validations. - - if subcategory is None: - validations = Validation.objects.filter( - Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() - else: - validations = Validation.objects.filter(Q(status="PENDING") - | Q(status="PENDING_V" - )).filter(tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition")\ - .all() - return render(request, "tutorial/validation/index.html", - {"validations": validations}) - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -@require_POST -def reservation(request, validation_pk): - """Display tutorials list in validation.""" - - validation = get_object_or_404(Validation, pk=validation_pk) - if validation.validator: - validation.validator = None - validation.date_reserve = None - validation.status = "PENDING" - validation.save() - messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) - return redirect(reverse("zds.tutorial.views.list_validation")) - else: - validation.validator = request.user - validation.date_reserve = datetime.now() - validation.status = "PENDING_V" - validation.save() - messages.info(request, - _(u"Le tutoriel a bien été \ - réservé par {0}.").format(request.user.username)) - return redirect( - validation.content.get_absolute_url() + - "?version=" + validation.version - ) - - -@login_required -def history(request, tutorial_pk, tutorial_slug): - """History of the tutorial.""" - - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - - repo = Repo(tutorial.get_repo_path()) - logs = repo.head.reference.log() - logs = sorted(logs, key=attrgetter("time"), reverse=True) - return render(request, "tutorial/tutorial/history.html", - {"tutorial": tutorial, "logs": logs}) - - -@login_required -@permission_required("tutorial.change_tutorial", raise_exception=True) -def history_validation(request, tutorial_pk): - """History of the validation of a tutorial.""" - - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - if subcategory is None: - validations = \ - Validation.objects.filter(tutorial__pk=tutorial_pk) \ - .order_by("date_proposition" - ).all() - else: - validations = Validation.objects.filter(tutorial__pk=tutorial_pk, - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition" - ).all() - return render(request, "tutorial/validation/history.html", - {"validations": validations, "tutorial": tutorial}) - - -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def reject_tutorial(request): - """Staff reject tutorial of an author.""" - - # Retrieve current tutorial; - - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") - - if request.user == validation.validator: - validation.comment_validator = request.POST["text"] - validation.status = "REJECT" - validation.date_validation = datetime.now() - validation.save() - - # Remove sha_validation because we rejected this version of the tutorial. - - tutorial.sha_validation = None - tutorial.pubdate = None - tutorial.save() - messages.info(request, _(u"Le tutoriel a bien été refusé.")) - comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) - # send feedback - msg = ( - _(u'Désolé, le zeste **{0}** n\'a malheureusement ' - u'pas passé l’étape de validation. Mais ne désespère pas, ' - u'certaines corrections peuvent surement être faite pour ' - u'l’améliorer et repasser la validation plus tard. ' - u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' - u'N\'hésite pas a lui envoyer un petit message pour discuter ' - u'de la décision ou demander plus de détail si tout cela te ' - u'semble injuste ou manque de clarté.') - .format(tutorial.title, - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), - comment_reject)) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Refus de Validation : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le refuser.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - - -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def valid_tutorial(request): - """Staff valid tutorial of an author.""" - - # Retrieve current tutorial; - - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") - - if request.user == validation.validator: - (output, err) = mep(tutorial, tutorial.sha_validation) - messages.info(request, output) - messages.error(request, err) - validation.comment_validator = request.POST["text"] - validation.status = "ACCEPT" - validation.date_validation = datetime.now() - validation.save() - - # Update sha_public with the sha of validation. We don't update sha_draft. - # So, the user can continue to edit his tutorial in offline. - - if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': - tutorial.pubdate = datetime.now() - tutorial.sha_public = validation.version - tutorial.source = request.POST["source"] - tutorial.sha_validation = None - tutorial.save() - messages.success(request, _(u"Le tutoriel a bien été validé.")) - - # send feedback - - msg = ( - _(u'Félicitations ! Le zeste [{0}]({1}) ' - u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' - u'peuvent venir l\'éplucher et réagir a son sujet. ' - u'Je te conseille de rester a leur écoute afin ' - u'd\'apporter des corrections/compléments.' - u'Un Tutoriel vivant et a jour est bien plus lu ' - u'qu\'un sujet abandonné !') - .format(tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Publication : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le valider.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(CreateExtract, self).dispatch(*args, **kwargs) + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj -@can_write_and_read_now -@login_required -@permission_required("tutorial.change_tutorial", raise_exception=True) -@require_POST -def invalid_tutorial(request, tutorial_pk): - """Staff invalid tutorial of an author.""" + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(CreateExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] - # Retrieve current tutorial + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - un_mep(tutorial) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_public).latest("date_proposition") - validation.status = "PENDING" - validation.date_validation = None - validation.save() + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - # Only update sha_validation because contributors can contribute on - # rereading version. + context['container'] = container - tutorial.sha_public = None - tutorial.sha_validation = validation.version - tutorial.pubdate = None - tutorial.save() - messages.success(request, _(u"Le tutoriel a bien été dépublié.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) + return context + def form_valid(self, form): + context = self.get_context_data() + parent = context['container'] -# User actions on tutorial. + sha = parent.repo_add_extract(form.cleaned_data['title'], + form.cleaned_data['text'], + form.cleaned_data['msg_commit']) -@can_write_and_read_now -@login_required -@require_POST -def ask_validation(request): - """User ask validation for his tutorial.""" + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - # Retrieve current tutorial; + self.success_url = parent.get_absolute_url() - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + return super(CreateExtract, self).form_valid(form) - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied +class EditExtract(FormView): + template_name = 'tutorialv2/edit/extract.html' + form_class = ExtractForm + content = None - old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, - status__in=['PENDING_V']).first() - if old_validation is not None: - old_validator = old_validation.validator - else: - old_validator = None - # delete old pending validation - Validation.objects.filter(tutorial__pk=tutorial_pk, - status__in=['PENDING', 'PENDING_V'])\ - .delete() - # We create and save validation object of the tutorial. + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(EditExtract, self).dispatch(*args, **kwargs) - validation = Validation() - validation.content = tutorial - validation.date_proposition = datetime.now() - validation.comment_authors = request.POST["text"] - validation.version = request.POST["version"] - if old_validator is not None: - validation.validator = old_validator - validation.date_reserve - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - msg = \ - (_(u'Bonjour {0},' - u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' - u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' - u'consulter l\'historique des versions' - u'\n\n> Merci').format(old_validator.username, tutorial.title)) - send_mp( - bot, - [old_validator], - _(u"Mise à jour de tuto : {0}").format(tutorial.title), - _(u"En validation"), - msg, - False, - ) - validation.save() - validation.content.source = request.POST["source"] - validation.content.sha_validation = request.POST["version"] - validation.content.save() - messages.success(request, - _(u"Votre demande de validation a été envoyée à l'équipe.")) - return redirect(tutorial.get_absolute_url()) + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] -@can_write_and_read_now -@login_required -@require_POST -def delete_tutorial(request, tutorial_pk): - """User would like delete his tutorial.""" + # get the extract: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - # Retrieve current tutorial + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: + raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + extract = None + if 'extract_slug' in self.kwargs: + try: + extract = container.children_dict[self.kwargs['extract_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(extract, Extract): + raise Http404 - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: + context['extract'] = extract - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied + return context + + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + extract = context['extract'] + initial = super(EditExtract, self).get_initial() - # when author is alone we can delete definitively tutorial + initial['title'] = extract.title + initial['text'] = extract.get_text() - if tutorial.authors.count() == 1: + return initial - # user can access to gallery + def form_valid(self, form): + context = self.get_context_data() + extract = context['extract'] - try: - ug = UserGallery.objects.filter(user=request.user, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - - # Delete the tutorial on the repo and on the database. - - old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - maj_repo_tuto(request, old_slug_path=old_slug, tuto=tutorial, - action="del") - messages.success(request, - _(u'Le tutoriel {0} a bien ' - u'été supprimé.').format(tutorial.title)) - tutorial.delete() - else: - tutorial.authors.remove(request.user) + sha = extract.repo_update(form.cleaned_data['title'], + form.cleaned_data['text'], + form.cleaned_data['msg_commit']) + + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - # user can access to gallery + self.success_url = extract.get_absolute_url() - try: - ug = UserGallery.objects.filter( - user=request.user, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - tutorial.save() - messages.success(request, - _(u'Vous ne faites plus partie des rédacteurs de ce ' - u'tutoriel.')) - return redirect(reverse("zds.tutorial.views.index")) + return super(EditExtract, self).form_valid(form) -@can_write_and_read_now -@require_POST -def modify_tutorial(request): - tutorial_pk = request.POST["tutorial"] - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # User actions - - if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): - if "add_author" in request.POST: - redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ - tutorial.pk, - tutorial.slug, - ]) - author_username = request.POST["author"] - author = None - try: - author = User.objects.get(username=author_username) - except User.DoesNotExist: - return redirect(redirect_url) - tutorial.authors.add(author) - tutorial.save() - - # share gallery - - ug = UserGallery() - ug.user = author - ug.gallery = tutorial.gallery - ug.mode = "W" - ug.save() - messages.success(request, - _(u'L\'auteur {0} a bien été ajouté à la rédaction ' - u'du tutoriel.').format(author.username)) - - # send msg to new author - - msg = ( - _(u'Bonjour **{0}**,\n\n' - u'Tu as été ajouté comme auteur du tutoriel [{1}]({2}).\n' - u'Tu peux retrouver ce tutoriel en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' - u'"Tutoriels" sur la page de ton profil.\n\n' - u'Tu peux maintenant commencer à rédiger !').format( - author.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url(), - settings.ZDS_APP['site']['url'] + reverse("zds.member.views.tutorials")) - ) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - [author], - _(u"Ajout en tant qu'auteur : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - - return redirect(redirect_url) - elif "remove_author" in request.POST: - redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ - tutorial.pk, - tutorial.slug, - ]) - - # Avoid orphan tutorials - - if tutorial.authors.all().count() <= 1: - raise Http404 - author_pk = request.POST["author"] - author = get_object_or_404(User, pk=author_pk) - tutorial.authors.remove(author) +class DeleteContainerOrExtract(DeleteView): + model = PublishableContent + template_name = None + http_method_names = [u'delete', u'post'] + object = None + content = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(DeleteContainerOrExtract, self).dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj - # user can access to gallery + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(DeleteContainerOrExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] + # get the extract: + if 'parent_container_slug' in self.kwargs: try: - ug = UserGallery.objects.filter(user=author, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - tutorial.save() - messages.success(request, - _(u"L'auteur {0} a bien été retiré du tutoriel.") - .format(author.username)) - - # send msg to removed author - - msg = ( - _(u'Bonjour **{0}**,\n\n' - u'Tu as été supprimé des auteurs du tutoriel [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' - u'pourra plus y accéder.\n').format( - author.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url()) - ) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - [author], - _(u"Suppression des auteurs : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - - return redirect(redirect_url) - elif "activ_beta" in request.POST: - if "version" in request.POST: - tutorial.sha_beta = request.POST['version'] - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id'])\ - .first() - msg = \ - (_(u'Bonjour à tous,\n\n' - u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' - u'J\'aimerais obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' - u'sur la forme, afin de proposer en validation un texte de qualité.' - u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' - u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' - u'\n\nMerci d\'avance pour votre aide').format( - naturaltime(tutorial.create_at), - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) - if topic is None: - forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) - - create_topic(author=request.user, - forum=forum, - title=_(u"[beta][tutoriel]{0}").format(tutorial.title), - subtitle=u"{}".format(tutorial.description), - text=msg, - key=tutorial.pk - ) - tp = Topic.objects.get(key=tutorial.pk) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - private_mp = \ - (_(u'Bonjour {},\n\n' - u'Vous venez de mettre votre tutoriel **{}** en beta. La communauté ' - u'pourra le consulter afin de vous faire des retours ' - u'constructifs avant sa soumission en validation.\n\n' - u'Un sujet dédié pour la beta de votre tutoriel a été ' - u'crée dans le forum et est accessible [ici]({})').format( - request.user.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tp.get_absolute_url())) - send_mp( - bot, - [request.user], - _(u"Tutoriel en beta : {0}").format(tutorial.title), - "", - private_mp, - False, - ) - else: - msg_up = \ - (_(u'Bonjour,\n\n' - u'La beta du tutoriel est de nouveau active.' - u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' - u'\n\nMerci pour vos relectures').format(tutorial.title, - settings.ZDS_APP['site']['url'] - + tutorial.get_absolute_url_beta())) - unlock_topic(topic, msg) - send_post(topic, msg_up) - - messages.success(request, _(u"La BETA sur ce tutoriel est bien activée.")) + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 else: - messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être activée.")) - return redirect(tutorial.get_absolute_url_beta()) - elif "update_beta" in request.POST: - if "version" in request.POST: - tutorial.sha_beta = request.POST['version'] - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, - forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() - msg = \ - (_(u'Bonjour à tous,\n\n' - u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' - u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' - u'sur la forme, afin de proposer en validation un texte de qualité.' - u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' - u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' - u'\n\nMerci d\'avance pour votre aide').format( - naturaltime(tutorial.create_at), - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) - if topic is None: - forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) - - create_topic(author=request.user, - forum=forum, - title=u"[beta][tutoriel]{0}".format(tutorial.title), - subtitle=u"{}".format(tutorial.description), - text=msg, - key=tutorial.pk - ) - else: - msg_up = \ - (_(u'Bonjour, !\n\n' - u'La beta du tutoriel a été mise à jour.' - u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' - u'\n\nMerci pour vos relectures').format(tutorial.title, - settings.ZDS_APP['site']['url'] - + tutorial.get_absolute_url_beta())) - unlock_topic(topic, msg) - send_post(topic, msg_up) - messages.success(request, _(u"La BETA sur ce tutoriel a bien été mise à jour.")) + if not isinstance(container, Container): + raise Http404 + + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 else: - messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être mise à jour.")) - return redirect(tutorial.get_absolute_url_beta()) - elif "desactiv_beta" in request.POST: - tutorial.sha_beta = None - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() - if topic is not None: - msg = \ - (_(u'Désactivation de la beta du tutoriel **{}**' - u'\n\nPour plus d\'informations envoyez moi un message privé.').format(tutorial.title)) - lock_topic(topic) - send_post(topic, msg) - messages.info(request, _(u"La BETA sur ce tutoriel a bien été désactivée.")) + if not isinstance(container, Container): + raise Http404 - return redirect(tutorial.get_absolute_url()) + to_delete = None + if 'object_slug' in self.kwargs: + try: + to_delete = container.children_dict[self.kwargs['object_slug']] + except KeyError: + raise Http404 - # No action performed, raise 403 + context['to_delete'] = to_delete - raise PermissionDenied + return context + def delete(self, request, *args, **kwargs): + """delete any object, either Extract or Container""" -@can_write_and_read_now -@login_required -def add_tutorial(request): - """'Adds a tutorial.""" + context = self.get_context_data() + to_delete = context['to_delete'] - if request.method == "POST": - form = TutorialForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - - # Creating a tutorial - - tutorial = PublishableContent() - tutorial.title = data["title"] - tutorial.description = data["description"] - tutorial.type = data["type"] - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - tutorial.images = "images" - if "licence" in data and data["licence"] != "": - lc = Licence.objects.filter(pk=data["licence"]).all()[0] - tutorial.licence = lc - else: - tutorial.licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) + sha = to_delete.repo_delete() - # add create date + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - tutorial.create_at = datetime.now() - tutorial.pubdate = datetime.now() + success_url = '' + if isinstance(to_delete, Extract): + success_url = to_delete.container.get_absolute_url() + else: + success_url = to_delete.parent.get_absolute_url() - # Creating the gallery - - gal = Gallery() - gal.title = data["title"] - gal.slug = slugify(data["title"]) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = gal - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - tutorial.image = img - tutorial.save() - - # Add subcategories on tutorial - - for subcat in form.cleaned_data["subcategory"]: - tutorial.subcategory.add(subcat) - - # Add helps if needed - for helpwriting in form.cleaned_data["helps"]: - tutorial.helps.add(helpwriting) - - # We need to save the tutorial before changing its author list - # since it's a many-to-many relationship - - tutorial.authors.add(request.user) - - # If it's a small tutorial, create its corresponding chapter - - if tutorial.type == "MINI": - chapter = Container() - chapter.tutorial = tutorial - chapter.save() - tutorial.save() - maj_repo_tuto( - request, - new_slug_path=tutorial.get_repo_path(), - tuto=tutorial, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - return redirect(tutorial.get_absolute_url()) - else: - form = TutorialForm( - initial={ - 'licence': Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) - } - ) - return render(request, "tutorial/tutorial/new.html", {"form": form}) + return redirect(success_url) -@can_write_and_read_now -@login_required -def edit_tutorial(request): - """Edit a tutorial.""" +class ArticleList(ListView): + """ + Displays the list of published articles. + """ + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type = "ARTICLE" + template_name = 'article/index.html' + tag = None - # Retrieve current tutorial; + def get_queryset(self): + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') - try: - tutorial_pk = request.GET["tutoriel"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! + return context - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - introduction = os.path.join(tutorial.get_repo_path(), "introduction.md") - conclusion = os.path.join(tutorial.get_repo_path(), "conclusion.md") - if request.method == "POST": - form = TutorialForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = TutorialForm(initial={ - "title": tutorial.title, - "type": tutorial.type, - "licence": tutorial.licence, - "description": tutorial.description, - "subcategory": tutorial.subcategory.all(), - "introduction": tutorial.get_introduction(), - "conclusion": tutorial.get_conclusion(), - "helps": tutorial.helps.all(), - }) - return render(request, "tutorial/tutorial/edit.html", - { - "tutorial": tutorial, "form": form, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True - }) - old_slug = tutorial.get_repo_path() - tutorial.title = data["title"] - tutorial.description = data["description"] - if "licence" in data and data["licence"] != "": - lc = Licence.objects.filter(pk=data["licence"]).all()[0] - tutorial.licence = lc - else: - tutorial.licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) - - # add MAJ date - - tutorial.update = datetime.now() - - # MAJ gallery - - gal = Gallery.objects.filter(pk=tutorial.gallery.pk) - gal.update(title=data["title"]) - gal.update(slug=slugify(data["title"])) - gal.update(update=datetime.now()) - - # MAJ image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = tutorial.gallery - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - tutorial.image = img - tutorial.save() - tutorial.update_children() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - - maj_repo_tuto( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - tuto=tutorial, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - - tutorial.subcategory.clear() - for subcat in form.cleaned_data["subcategory"]: - tutorial.subcategory.add(subcat) - - tutorial.helps.clear() - for help in form.cleaned_data["helps"]: - tutorial.helps.add(help) - - tutorial.save() - return redirect(tutorial.get_absolute_url()) - else: - json = tutorial.load_json_for_public(tutorial.sha_draft) - if "licence" in json: - licence = json['licence'] - else: - licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) - form = TutorialForm(initial={ - "title": json["title"], - "type": json["type"], - "licence": licence, - "description": json["description"], - "subcategory": tutorial.subcategory.all(), - "introduction": tutorial.get_introduction(), - "conclusion": tutorial.get_conclusion(), - "helps": tutorial.helps.all(), - }) - return render(request, "tutorial/tutorial/edit.html", - {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])}) +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" -# Parts. + context_object_name = 'tutorials' + type = "TUTORIAL" + template_name = 'tutorialv2/index.html' -@login_required -def view_part( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, -): - """Display a part.""" +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorialv2/help.html' - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - try: - sha = request.GET["version"] - except KeyError: - sha = tutorial.sha_draft + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context - # Only authors of the tutorial and staff can view tutorial in offline. +# TODO ArticleWithHelp - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - final_part = None +class DisplayDiff(DetailView): + """Display the difference between two version of a content. + Reference is always HEAD and compared version is a GET query parameter named sha + this class has no reason to be adapted to any content type""" + model = PublishableContent + template_name = "tutorial/diff.html" + context_object_name = "tutorial" + + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) - # find the good manifest file + def get_context_data(self, **kwargs): - repo = Repo(tutorial.get_repo_path()) - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha=sha) - - parts = mandata["parts"] - find = False - cpt_p = 1 - for part in parts: - if part_pk == str(part["pk"]): - find = True - part["tutorial"] = tutorial - part["path"] = tutorial.get_repo_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) - part["conclu"] = get_blob(repo.commit(sha).tree, part["conclusion"]) - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_repo_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_repo_path() - cpt_e += 1 - cpt_c += 1 - final_part = part - break - cpt_p += 1 - - # if part can't find - if not find: - raise Http404 + context = super(DisplayDiff, self).get_context_data(**kwargs) - if tutorial.js_support: - is_js = "js" - else: - is_js = "" + try: + sha = self.request.GET.get("sha") + except KeyError: + sha = self.get_object().sha_draft + + if self.request.user not in context[self.context_object_name].authors.all(): + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + # open git repo and find diff between displayed version and head + repo = Repo(context[self.context_object_name].get_repo_path()) + current_version_commit = repo.commit(sha) + diff_with_head = current_version_commit.diff("HEAD~1") + context["path_add"] = diff_with_head.iter_change_type("A") + context["path_ren"] = diff_with_head.iter_change_type("R") + context["path_del"] = diff_with_head.iter_change_type("D") + context["path_maj"] = diff_with_head.iter_change_type("M") + return context - return render(request, "tutorial/part/view.html", - {"tutorial": mandata, - "part": final_part, - "version": sha, - "is_js": is_js}) +class DisplayOnlineContent(DisplayContent): + """Display online tutorial""" + type = "TUTORIAL" + template_name = "tutorial/view_online.html" -def view_part_online( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, -): - """Display a part.""" + def get_forms(self, context, content): - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if not tutorial.in_public(): - raise Http404 + # Build form to send a note for the current tutorial. + context['form'] = NoteForm(content, self.request.user) + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_repo_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p - # find the good manifest file - - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - mandata["update"] = tutorial.update - - mandata["get_parts"] = mandata["parts"] - parts = mandata["parts"] - cpt_p = 1 - final_part = None - find = False - for part in parts: - part["tutorial"] = mandata - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - if part_pk == str(part["pk"]): - find = True - intro = open(os.path.join(tutorial.get_prod_path(), - part["introduction"] + ".html"), "r") - part["intro"] = intro.read() - intro.close() - conclu = open(os.path.join(tutorial.get_prod_path(), - part["conclusion"] + ".html"), "r") - part["conclu"] = conclu.read() - conclu.close() - final_part = part cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c chapter["position_in_tutorial"] = cpt_c * cpt_p - if part_slug == slugify(part["title"]): - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_prod_path() - cpt_e += 1 + self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - part["get_chapters"] = part["chapters"] - cpt_p += 1 - - # if part can't find - if not find: - raise Http404 - - return render(request, "tutorial/part/view_online.html", {"part": final_part}) + def compatibility_chapter(self, content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_prod_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") + # load extracts + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_prod_path() + text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") + ext["txt"] = text.read() + cpt += 1 -@can_write_and_read_now -@login_required -def add_part(request): - """Add a new part.""" - - try: - tutorial_pk = request.GET["tutoriel"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + def get_context_data(self, **kwargs): + content = self.get_object() + # If the tutorial isn't online, we raise 404 error. + if not content.in_public(): + raise Http404 + self.sha = content.sha_public + context = super(DisplayOnlineContent, self).get_context_data(**kwargs) - # Make sure it's a big tutorial, just in case + context["tutorial"]["update"] = content.update + context["tutorial"]["get_note_count"] = content.get_note_count() - if not tutorial.type == "BIG": - raise Http404 + if self.request.user.is_authenticated(): + # If the user is authenticated, he may want to tell the world how cool the content is + # We check if he can post a not or not with + # antispam filter. + context['tutorial']['antispam'] = content.antispam() - # Make sure the user belongs to the author list + # If the user has never read this before, we mark this tutorial read. + if never_read(content): + mark_read(content) - if request.user not in tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = PartForm(request.POST) - if form.is_valid(): - data = form.data - part = Container() - part.tutorial = tutorial - part.title = data["title"] - part.position_in_tutorial = tutorial.get_parts().count() + 1 - part.save() - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part.save() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - - maj_repo_part( - request, - new_slug_path=new_slug, - part=part, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - if "submit_continue" in request.POST: - form = PartForm() - messages.success(request, - _(u'La partie « {0} » a été ajoutée ' - u'avec succès.').format(part.title)) - else: - return redirect(part.get_absolute_url()) - else: - form = PartForm() - return render(request, "tutorial/part/new.html", {"tutorial": tutorial, - "form": form}) + # Find all notes of the tutorial. + notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() -@can_write_and_read_now -@login_required -def modify_part(request): - """Modifiy the given part.""" + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. - if not request.method == "POST": - raise Http404 - part_pk = request.POST["part"] - part = get_object_or_404(Container, pk=part_pk) + last_note_pk = 0 + if content.last_note: + last_note_pk = content.last_note.pk - # Make sure the user is allowed to do that + # Handle pagination - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if "move" in request.POST: + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) try: - new_pos = int(request.POST["move_target"]) + page_nbr = int(self.request.GET.get("page")) + except KeyError: + page_nbr = 1 except ValueError: - # Invalid conversion, maybe the user played with the move button - return redirect(part.tutorial.get_absolute_url()) - - move(part, new_pos, "position_in_tutorial", "tutorial", "get_parts") - part.save() - - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) - - maj_repo_tuto(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - tuto=part.tutorial, - action="maj", - msg=_(u"Déplacement de la partie {} ").format(part.title)) - elif "delete" in request.POST: - # Delete all chapters belonging to the part - - Container.objects.all().filter(part=part).delete() - - # Move other parts - - old_pos = part.position_in_tutorial - for tut_p in part.tutorial.get_parts(): - if old_pos <= tut_p.position_in_tutorial: - tut_p.position_in_tutorial = tut_p.position_in_tutorial - 1 - tut_p.save() - old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - maj_repo_part(request, old_slug_path=old_slug, part=part, action="del") - - new_slug_tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) - # Actually delete the part - part.delete() - - maj_repo_tuto(request, - old_slug_path=new_slug_tuto_path, - new_slug_path=new_slug_tuto_path, - tuto=part.tutorial, - action="maj", - msg=_(u"Suppression de la partie {} ").format(part.title)) - return redirect(part.tutorial.get_absolute_url()) + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + res = [] + if page_nbr != 1: -@can_write_and_read_now -@login_required -def edit_part(request): - """Edit the given part.""" + # Show the last note of the previous page - try: - part_pk = int(request.GET["partie"]) - except KeyError: - raise Http404 - except ValueError: - raise Http404 + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) - part = get_object_or_404(Container, pk=part_pk) - introduction = os.path.join(part.get_path(), "introduction.md") - conclusion = os.path.join(part.get_path(), "conclusion.md") - # Make sure the user is allowed to do that + context['notes'] = res + context['last_note_pk'] = last_note_pk + context['pages'] = paginator_range(page_nbr, paginator.num_pages) + context['nb'] = page_nbr - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = PartForm(request.POST) - if form.is_valid(): - data = form.data - # avoid collision - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = PartForm({"title": part.title, - "introduction": part.get_introduction(), - "conclusion": part.get_conclusion()}) - return render(request, "tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True, - "form": form - }) - # Update title and his slug. - - part.title = data["title"] - old_slug = part.get_path() - part.save() - - # Update path for introduction and conclusion. - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part.save() - part.update_children() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - - maj_repo_part( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - part=part, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(part.get_absolute_url()) - else: - form = PartForm({"title": part.title, - "introduction": part.get_introduction(), - "conclusion": part.get_conclusion()}) - return render(request, "tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "form": form - }) + +class DisplayOnlineArticle(DisplayOnlineContent): + type = "ARTICLE" -# Containers. +# Staff actions. +@permission_required("tutorial.change_tutorial", raise_exception=True) @login_required -def view_chapter( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, - chapter_pk, - chapter_slug, -): - """View chapter.""" +def list_validation(request): + """Display tutorials list in validation.""" - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + # Retrieve type of the validation. Default value is all validations. try: - sha = request.GET["version"] + type = request.GET["type"] except KeyError: - sha = tutorial.sha_draft - - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() - - # Only authors of the tutorial and staff can view tutorial in offline. + type = None - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied + # Get subcategory to filter validations. - # find the good manifest file + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None - repo = Repo(tutorial.get_repo_path()) - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha=sha) - - parts = mandata["parts"] - cpt_p = 1 - final_chapter = None - chapter_tab = [] - final_position = 0 - find = False - for part in parts: - cpt_c = 1 - part["slug"] = slugify(part["title"]) - part["get_absolute_url"] = reverse( - "zds.tutorial.views.view_part", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"]]) - part["tutorial"] = tutorial - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_repo_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url"] = part["get_absolute_url"] \ - + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) - if chapter_pk == str(chapter["pk"]): - find = True - chapter["intro"] = get_blob(repo.commit(sha).tree, - chapter["introduction"]) - chapter["conclu"] = get_blob(repo.commit(sha).tree, - chapter["conclusion"]) - - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_repo_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt_e += 1 - chapter_tab.append(chapter) - if chapter_pk == str(chapter["pk"]): - final_chapter = chapter - final_position = len(chapter_tab) - 1 - cpt_c += 1 - cpt_p += 1 + # Orphan validation. There aren't validator attached to the validations. - # if chapter can't find - if not find: - raise Http404 + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": - prev_chapter = (chapter_tab[final_position - 1] if final_position - > 0 else None) - next_chapter = (chapter_tab[final_position + 1] if final_position + 1 - < len(chapter_tab) else None) + # Reserved validation. There are a validator attached to the + # validations. - if tutorial.js_support: - is_js = "js" + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() else: - is_js = "" - - return render(request, "tutorial/chapter/view.html", { - "tutorial": mandata, - "chapter": final_chapter, - "prev": prev_chapter, - "next": next_chapter, - "version": sha, - "is_js": is_js - }) - - -def view_chapter_online( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, - chapter_pk, - chapter_slug, -): - """View chapter.""" - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if not tutorial.in_public(): - raise Http404 + # Default, we display all validations. - # find the good manifest file + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - mandata["update"] = tutorial.update - mandata['get_parts'] = mandata["parts"] - parts = mandata["parts"] - cpt_p = 1 - final_chapter = None - chapter_tab = [] - final_position = 0 +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" - find = False - for part in parts: - cpt_c = 1 - part["slug"] = slugify(part["title"]) - part["get_absolute_url_online"] = reverse( - "zds.tutorial.views.view_part_online", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"]]) - part["tutorial"] = mandata - part["position_in_tutorial"] = cpt_p - part["get_chapters"] = part["chapters"] - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_prod_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url_online"] = part[ - "get_absolute_url_online"] + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) - if chapter_pk == str(chapter["pk"]): - find = True - intro = open( - os.path.join( - tutorial.get_prod_path(), - chapter["introduction"] + - ".html"), - "r") - chapter["intro"] = intro.read() - intro.close() - conclu = open( - os.path.join( - tutorial.get_prod_path(), - chapter["conclusion"] + - ".html"), - "r") - chapter["conclu"] = conclu.read() - conclu.close() - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - text = open(os.path.join(tutorial.get_prod_path(), - ext["text"] + ".html"), "r") - ext["txt"] = text.read() - text.close() - cpt_e += 1 - else: - intro = None - conclu = None - chapter_tab.append(chapter) - if chapter_pk == str(chapter["pk"]): - final_chapter = chapter - final_position = len(chapter_tab) - 1 - cpt_c += 1 - cpt_p += 1 + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.content.get_absolute_url() + + "?version=" + validation.version + ) - # if chapter can't find - if not find: - raise Http404 - prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None) - next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) +@login_required +def history(request, tutorial_pk, tutorial_slug): + """History of the tutorial.""" + + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied - return render(request, "tutorial/chapter/view_online.html", { - "chapter": final_chapter, - "parts": parts, - "prev": prev_chapter, - "next": next_chapter, - }) + repo = Repo(tutorial.get_repo_path()) + logs = repo.head.reference.log() + logs = sorted(logs, key=attrgetter("time"), reverse=True) + return render(request, "tutorial/tutorial/history.html", + {"tutorial": tutorial, "logs": logs}) -@can_write_and_read_now @login_required -def add_chapter(request): - """Add a new chapter to given part.""" +@permission_required("tutorial.change_tutorial", raise_exception=True) +def history_validation(request, tutorial_pk): + """History of the validation of a tutorial.""" - try: - part_pk = request.GET["partie"] - except KeyError: - raise Http404 - part = get_object_or_404(Container, pk=part_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # Make sure the user is allowed to do that + # Get subcategory to filter validations. - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = ChapterForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - chapter = Container() - chapter.title = data["title"] - chapter.part = part - chapter.position_in_part = part.get_chapters().count() + 1 - chapter.update_position_in_tutorial() - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = part.tutorial.gallery - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - chapter.image = img - - chapter.save() - if chapter.tutorial: - chapter_path = os.path.join( - os.path.join( - settings.ZDS_APP['tutorial']['repo_path'], - chapter.tutorial.get_phy_slug()), - chapter.get_phy_slug()) - chapter.introduction = os.path.join(chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join(chapter.get_phy_slug(), - "conclusion.md") - else: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - chapter.introduction = os.path.join( - chapter.part.get_phy_slug(), - chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug(), "conclusion.md") - chapter.save() - maj_repo_chapter( - request, - new_slug_path=chapter_path, - chapter=chapter, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - if "submit_continue" in request.POST: - form = ChapterForm() - messages.success(request, - _(u'Le chapitre « {0} » a été ajouté ' - u'avec succès.').format(chapter.title)) - else: - return redirect(chapter.get_absolute_url()) + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + if subcategory is None: + validations = \ + Validation.objects.filter(tutorial__pk=tutorial_pk) \ + .order_by("date_proposition" + ).all() else: - form = ChapterForm() - - return render(request, "tutorial/chapter/new.html", {"part": part, - "form": form}) + validations = Validation.objects.filter(tutorial__pk=tutorial_pk, + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition" + ).all() + return render(request, "tutorial/validation/history.html", + {"validations": validations, "tutorial": tutorial}) @can_write_and_read_now @login_required -def modify_chapter(request): - """Modify the given chapter.""" +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def reject_tutorial(request): + """Staff reject tutorial of an author.""" + + # Retrieve current tutorial; - if not request.method == "POST": - raise Http404 - data = request.POST try: - chapter_pk = request.POST["chapter"] + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - chapter = get_object_or_404(Container, pk=chapter_pk) - - # Make sure the user is allowed to do that - - if request.user not in chapter.get_tutorial().authors.all() and \ - not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if "move" in data: - try: - new_pos = int(request.POST["move_target"]) - except ValueError: - - # User misplayed with the move button - - return redirect(chapter.get_absolute_url()) - move(chapter, new_pos, "position_in_part", "part", "get_chapters") - chapter.update_position_in_tutorial() - chapter.save() - - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug()) - - maj_repo_part(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - part=chapter.part, - action="maj", - msg=_(u"Déplacement du chapitre {}").format(chapter.title)) - messages.info(request, _(u"Le chapitre a bien été déplacé.")) - elif "delete" in data: - old_pos = chapter.position_in_part - old_tut_pos = chapter.position_in_tutorial - - if chapter.part: - parent = chapter.part - else: - parent = chapter.tutorial - - # Move other chapters first - - for tut_c in chapter.part.get_chapters(): - if old_pos <= tut_c.position_in_part: - tut_c.position_in_part = tut_c.position_in_part - 1 - tut_c.save() - maj_repo_chapter(request, chapter=chapter, - old_slug_path=chapter.get_path(), action="del") - - # Then delete the chapter - new_slug_path_part = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug()) - chapter.delete() - - # Update all the position_in_tutorial fields for the next chapters - - for tut_c in \ - Container.objects.filter(position_in_tutorial__gt=old_tut_pos): - tut_c.update_position_in_tutorial() - tut_c.save() + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") - maj_repo_part(request, - old_slug_path=new_slug_path_part, - new_slug_path=new_slug_path_part, - part=chapter.part, - action="maj", - msg=_(u"Suppression du chapitre {}").format(chapter.title)) - messages.info(request, _(u"Le chapitre a bien été supprimé.")) + if request.user == validation.validator: + validation.comment_validator = request.POST["text"] + validation.status = "REJECT" + validation.date_validation = datetime.now() + validation.save() - return redirect(parent.get_absolute_url()) + # Remove sha_validation because we rejected this version of the tutorial. - return redirect(chapter.get_absolute_url()) + tutorial.sha_validation = None + tutorial.pubdate = None + tutorial.save() + messages.info(request, _(u"Le tutoriel a bien été refusé.")) + comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) + # send feedback + msg = ( + _(u'Désolé, le zeste **{0}** n\'a malheureusement ' + u'pas passé l’étape de validation. Mais ne désespère pas, ' + u'certaines corrections peuvent surement être faite pour ' + u'l’améliorer et repasser la validation plus tard. ' + u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' + u'N\'hésite pas a lui envoyer un petit message pour discuter ' + u'de la décision ou demander plus de détail si tout cela te ' + u'semble injuste ou manque de clarté.') + .format(tutorial.title, + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), + comment_reject)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Refus de Validation : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le refuser.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) @can_write_and_read_now @login_required -def edit_chapter(request): - """Edit the given chapter.""" +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def valid_tutorial(request): + """Staff valid tutorial of an author.""" + + # Retrieve current tutorial; try: - chapter_pk = int(request.GET["chapitre"]) + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - except ValueError: - raise Http404 + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + (output, err) = mep(tutorial, tutorial.sha_validation) + messages.info(request, output) + messages.error(request, err) + validation.comment_validator = request.POST["text"] + validation.status = "ACCEPT" + validation.date_validation = datetime.now() + validation.save() - chapter = get_object_or_404(Container, pk=chapter_pk) - big = chapter.part - small = chapter.tutorial + # Update sha_public with the sha of validation. We don't update sha_draft. + # So, the user can continue to edit his tutorial in offline. - # Make sure the user is allowed to do that + if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': + tutorial.pubdate = datetime.now() + tutorial.sha_public = validation.version + tutorial.source = request.POST["source"] + tutorial.sha_validation = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été validé.")) - if (big and request.user not in chapter.part.tutorial.authors.all() - or small and request.user not in chapter.tutorial.authors.all())\ - and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - introduction = os.path.join(chapter.get_path(), "introduction.md") - conclusion = os.path.join(chapter.get_path(), "conclusion.md") - if request.method == "POST": + # send feedback - if chapter.part: - form = ChapterForm(request.POST, request.FILES) - gal = chapter.part.tutorial.gallery - else: - form = EmbdedChapterForm(request.POST, request.FILES) - gal = chapter.tutorial.gallery - if form.is_valid(): - data = form.data - # avoid collision - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = render_chapter_form(chapter) - return render(request, "tutorial/part/edit.html", - { - "chapter": chapter, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True, - "form": form - }) - chapter.title = data["title"] - - old_slug = chapter.get_path() - chapter.save() - chapter.update_children() - - if chapter.part: - if chapter.tutorial: - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.tutorial.get_phy_slug(), - chapter.get_phy_slug()) - else: - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = gal - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - chapter.image = img - maj_repo_chapter( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - chapter=chapter, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(chapter.get_absolute_url()) + msg = ( + _(u'Félicitations ! Le zeste [{0}]({1}) ' + u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' + u'peuvent venir l\'éplucher et réagir a son sujet. ' + u'Je te conseille de rester a leur écoute afin ' + u'd\'apporter des corrections/compléments.' + u'Un Tutoriel vivant et a jour est bien plus lu ' + u'qu\'un sujet abandonné !') + .format(tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Publication : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) else: - form = render_chapter_form(chapter) - return render(request, "tutorial/chapter/edit.html", {"chapter": chapter, - "last_hash": compute_hash([introduction, conclusion]), - "form": form}) + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le valider.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) +@can_write_and_read_now @login_required -def add_extract(request): - """Add extract.""" - - try: - chapter_pk = int(request.GET["chapitre"]) - except KeyError: - raise Http404 - except ValueError: - raise Http404 - - chapter = get_object_or_404(Container, pk=chapter_pk) - part = chapter.part - - # If part exist, we check if the user is in authors of the tutorial of the - # part or If part doesn't exist, we check if the user is in authors of the - # tutorial of the chapter. - - if part and request.user not in chapter.part.tutorial.authors.all() \ - or not part and request.user not in chapter.tutorial.authors.all(): - - # If the user isn't an author or a staff, we raise an exception. - - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - data = request.POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +@require_POST +def invalid_tutorial(request, tutorial_pk): + """Staff invalid tutorial of an author.""" - # Using the « preview button » + # Retrieve current tutorial - if "preview" in data: - form = ExtractForm(initial={"title": data["title"], - "text": data["text"], - 'msg_commit': data['msg_commit']}) - return render(request, "tutorial/extract/new.html", - {"chapter": chapter, "form": form}) - else: + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + un_mep(tutorial) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_public).latest("date_proposition") + validation.status = "PENDING" + validation.date_validation = None + validation.save() - # Save extract. + # Only update sha_validation because contributors can contribute on + # rereading version. - form = ExtractForm(request.POST) - if form.is_valid(): - data = form.data - extract = Extract() - extract.chapter = chapter - extract.position_in_chapter = chapter.get_extract_count() + 1 - extract.title = data["title"] - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract(request, new_slug_path=extract.get_path(), - extract=extract, text=data["text"], - action="add", - msg=request.POST.get('msg_commit', None)) - return redirect(extract.get_absolute_url()) - else: - form = ExtractForm() + tutorial.sha_public = None + tutorial.sha_validation = validation.version + tutorial.pubdate = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été dépublié.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) - return render(request, "tutorial/extract/new.html", {"chapter": chapter, - "form": form}) +# User actions on tutorial. @can_write_and_read_now @login_required -def edit_extract(request): - """Edit extract.""" +@require_POST +def ask_validation(request): + """User ask validation for his tutorial.""" + + # Retrieve current tutorial; + try: - extract_pk = request.GET["extrait"] + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - extract = get_object_or_404(Extract, pk=extract_pk) - part = extract.chapter.part - - # If part exist, we check if the user is in authors of the tutorial of the - # part or If part doesn't exist, we check if the user is in authors of the - # tutorial of the chapter. - - if part and request.user \ - not in extract.chapter.part.tutorial.authors.all() or not part \ - and request.user not in extract.chapter.tutorial.authors.all(): + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # If the user isn't an author or a staff, we raise an exception. + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - if request.method == "POST": - data = request.POST - # Using the « preview button » - - if "preview" in data: - form = ExtractForm(initial={ - "title": data["title"], - "text": data["text"], - 'msg_commit': data['msg_commit'] - }) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, "form": form, - "last_hash": compute_hash([extract.get_path()]) - }) - else: - if content_has_changed([extract.get_path()], data["last_hash"]): - form = ExtractForm(initial={ - "title": extract.title, - "text": extract.get_text(), - 'msg_commit': data['msg_commit']}) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "new_version": True, - "form": form - }) - # Edit extract. - - form = ExtractForm(request.POST) - if form.is_valid(): - data = form.data - old_slug = extract.get_path() - extract.title = data["title"] - extract.text = extract.get_path(relative=True) - - # Use path retrieve before and use it to create the new slug. - extract.save() - new_slug = extract.get_path() - - maj_repo_extract( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - extract=extract, - text=data["text"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(extract.get_absolute_url()) + old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator else: - form = ExtractForm({"title": extract.title, - "text": extract.get_text()}) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "form": form - }) - - -@can_write_and_read_now -def modify_extract(request): - if not request.method == "POST": - raise Http404 - data = request.POST - try: - extract_pk = request.POST["extract"] - except KeyError: - raise Http404 - extract = get_object_or_404(Extract, pk=extract_pk) - chapter = extract.chapter - if "delete" in data: - pos_current_extract = extract.position_in_chapter - for extract_c in extract.chapter.get_extracts(): - if pos_current_extract <= extract_c.position_in_chapter: - extract_c.position_in_chapter = extract_c.position_in_chapter \ - - 1 - extract_c.save() - - # Use path retrieve before and use it to create the new slug. - - old_slug = extract.get_path() - - if extract.chapter.tutorial: - new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug()) - else: - new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - - maj_repo_extract(request, old_slug_path=old_slug, extract=extract, - action="del") - - maj_repo_chapter(request, - old_slug_path=new_slug_path_chapter, - new_slug_path=new_slug_path_chapter, - chapter=chapter, - action="maj", - msg=_(u"Suppression de l'extrait {}").format(extract.title)) - return redirect(chapter.get_absolute_url()) - elif "move" in data: - try: - new_pos = int(request.POST["move_target"]) - except ValueError: - # Error, the user misplayed with the move button - return redirect(extract.get_absolute_url()) - - move(extract, new_pos, "position_in_chapter", "chapter", "get_extracts") - extract.save() - - if extract.chapter.tutorial: - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug()) - else: - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) + old_validator = None + # delete old pending validation + Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING', 'PENDING_V'])\ + .delete() + # We create and save validation object of the tutorial. - maj_repo_chapter(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - chapter=chapter, - action="maj", - msg=_(u"Déplacement de l'extrait {}").format(extract.title)) - return redirect(extract.get_absolute_url()) - raise Http404 + validation = Validation() + validation.content = tutorial + validation.date_proposition = datetime.now() + validation.comment_authors = request.POST["text"] + validation.version = request.POST["version"] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' + u'consulter l\'historique des versions' + u'\n\n> Merci').format(old_validator.username, tutorial.title)) + send_mp( + bot, + [old_validator], + _(u"Mise à jour de tuto : {0}").format(tutorial.title), + _(u"En validation"), + msg, + False, + ) + validation.save() + validation.content.source = request.POST["source"] + validation.content.sha_validation = request.POST["version"] + validation.content.save() + messages.success(request, + _(u"Votre demande de validation a été envoyée à l'équipe.")) + return redirect(tutorial.get_absolute_url()) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) def find_tuto(request, pk_user): try: type = request.GET["type"] @@ -2600,560 +1468,6 @@ def replace_real_url(md_text, dict): return md_text -def import_content( - request, - tuto, - images, - logo, -): - tutorial = PublishableContent() - - # add create date - - tutorial.create_at = datetime.now() - tree = etree.parse(tuto) - racine_big = tree.xpath("/bigtuto") - racine_mini = tree.xpath("/minituto") - if len(racine_big) > 0: - - # it's a big tuto - - tutorial.type = "BIG" - tutorial_title = tree.xpath("/bigtuto/titre")[0] - tutorial_intro = tree.xpath("/bigtuto/introduction")[0] - tutorial_conclu = tree.xpath("/bigtuto/conclusion")[0] - tutorial.title = tutorial_title.text.strip() - tutorial.description = tutorial_title.text.strip() - tutorial.images = "images" - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - - # Creating the gallery - - gal = Gallery() - gal.title = tutorial_title.text - gal.slug = slugify(tutorial_title.text) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - tutorial.save() - tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - mapping = upload_images(images, tutorial) - maj_repo_tuto( - request, - new_slug_path=tuto_path, - tuto=tutorial, - introduction=replace_real_url(tutorial_intro.text, mapping), - conclusion=replace_real_url(tutorial_conclu.text, mapping), - action="add", - ) - tutorial.authors.add(request.user) - part_count = 1 - for partie in tree.xpath("/bigtuto/parties/partie"): - part_title = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/titre")[0] - part_intro = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/introduction")[0] - part_conclu = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/conclusion")[0] - part = Container() - part.title = part_title.text.strip() - part.position_in_tutorial = part_count - part.tutorial = tutorial - part.save() - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - part.save() - maj_repo_part( - request, - None, - part_path, - part, - replace_real_url(part_intro.text, mapping), - replace_real_url(part_conclu.text, mapping), - action="add", - ) - chapter_count = 1 - for chapitre in tree.xpath("/bigtuto/parties/partie[" - + str(part_count) - + "]/chapitres/chapitre"): - chapter_title = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/titre")[0] - chapter_intro = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/introduction")[0] - chapter_conclu = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/conclusion")[0] - chapter = Container() - chapter.title = chapter_title.text.strip() - chapter.position_in_part = chapter_count - chapter.position_in_tutorial = part_count * chapter_count - chapter.part = part - chapter.save() - chapter.introduction = os.path.join( - part.get_phy_slug(), - chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join( - part.get_phy_slug(), - chapter.get_phy_slug(), - "conclusion.md") - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - chapter.save() - maj_repo_chapter( - request, - new_slug_path=chapter_path, - chapter=chapter, - introduction=replace_real_url(chapter_intro.text, - mapping), - conclusion=replace_real_url(chapter_conclu.text, mapping), - action="add", - ) - extract_count = 1 - for souspartie in tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/chapitres/chapitre[" - + str(chapter_count) + "]/sousparties/souspartie"): - extract_title = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/sousparties/souspartie[" + - str(extract_count) + - "]/titre")[0] - extract_text = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/sousparties/souspartie[" + - str(extract_count) + - "]/texte")[0] - extract = Extract() - extract.title = extract_title.text.strip() - extract.position_in_chapter = extract_count - extract.chapter = chapter - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract( - request, - new_slug_path=extract.get_path(), - extract=extract, - text=replace_real_url( - extract_text.text, - mapping), - action="add") - extract_count += 1 - chapter_count += 1 - part_count += 1 - elif len(racine_mini) > 0: - - # it's a mini tuto - - tutorial.type = "MINI" - tutorial_title = tree.xpath("/minituto/titre")[0] - tutorial_intro = tree.xpath("/minituto/introduction")[0] - tutorial_conclu = tree.xpath("/minituto/conclusion")[0] - tutorial.title = tutorial_title.text.strip() - tutorial.description = tutorial_title.text.strip() - tutorial.images = "images" - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - - # Creating the gallery - - gal = Gallery() - gal.title = tutorial_title.text - gal.slug = slugify(tutorial_title.text) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - tutorial.save() - tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - mapping = upload_images(images, tutorial) - maj_repo_tuto( - request, - new_slug_path=tuto_path, - tuto=tutorial, - introduction=replace_real_url(tutorial_intro.text, mapping), - conclusion=replace_real_url(tutorial_conclu.text, mapping), - action="add", - ) - tutorial.authors.add(request.user) - chapter = Container() - chapter.tutorial = tutorial - chapter.save() - extract_count = 1 - for souspartie in tree.xpath("/minituto/sousparties/souspartie"): - extract_title = tree.xpath("/minituto/sousparties/souspartie[" - + str(extract_count) + "]/titre")[0] - extract_text = tree.xpath("/minituto/sousparties/souspartie[" - + str(extract_count) + "]/texte")[0] - extract = Extract() - extract.title = extract_title.text.strip() - extract.position_in_chapter = extract_count - extract.chapter = chapter - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract(request, new_slug_path=extract.get_path(), - extract=extract, - text=replace_real_url(extract_text.text, - mapping), action="add") - extract_count += 1 - - -@can_write_and_read_now -@login_required -@require_POST -def local_import(request): - import_content(request, request.POST["tuto"], request.POST["images"], - request.POST["logo"]) - return redirect(reverse("zds.member.views.tutorials")) - - -@can_write_and_read_now -@login_required -def import_tuto(request): - if request.method == "POST": - # for import tuto - if "import-tuto" in request.POST: - form = ImportForm(request.POST, request.FILES) - if form.is_valid(): - import_content(request, request.FILES["file"], request.FILES["images"], "") - return redirect(reverse("zds.member.views.tutorials")) - else: - form_archive = ImportArchiveForm(user=request.user) - - elif "import-archive" in request.POST: - form_archive = ImportArchiveForm(request.user, request.POST, request.FILES) - if form_archive.is_valid(): - (check, reason) = import_archive(request) - if not check: - messages.error(request, reason) - else: - messages.success(request, reason) - return redirect(reverse("zds.member.views.tutorials")) - else: - form = ImportForm() - - else: - form = ImportForm() - form_archive = ImportArchiveForm(user=request.user) - - profile = get_object_or_404(Profile, user=request.user) - oldtutos = [] - if profile.sdz_tutorial: - olds = profile.sdz_tutorial.strip().split(":") - else: - olds = [] - for old in olds: - oldtutos.append(get_info_old_tuto(old)) - return render( - request, - "tutorial/tutorial/import.html", - {"form": form, "form_archive": form_archive, "old_tutos": oldtutos} - ) - - -# Handling repo -def maj_repo_tuto( - request, - old_slug_path=None, - new_slug_path=None, - tuto=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - if action == "del": - shutil.rmtree(old_slug_path) - else: - if action == "maj": - if old_slug_path != new_slug_path: - shutil.move(old_slug_path, new_slug_path) - repo = Repo(new_slug_path) - msg = _(u"Modification du tutoriel : «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - repo = Repo.init(new_slug_path, bare=False) - msg = _(u"Création du tutoriel «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg)).strip() - repo = Repo(new_slug_path) - index = repo.index - man_path = os.path.join(new_slug_path, "manifest.json") - tuto.dump_json(path=man_path) - index.add(["manifest.json"]) - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - index.add(["introduction.md"]) - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - index.add(["conclusion.md"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - tuto.sha_draft = com.hexsha - tuto.save() - - -def maj_repo_part( - request, - old_slug_path=None, - new_slug_path=None, - part=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - repo = Repo(part.tutorial.get_repo_path()) - index = repo.index - if action == "del": - shutil.rmtree(old_slug_path) - msg = _(u"Suppresion de la partie : «{}»").format(part.title) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - - msg = _(u"Modification de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") - part.tutorial.dump_json(path=man_path) - index.add(["manifest.json"]) - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" - )]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['litteral_name']) - com_part = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - part.tutorial.sha_draft = com_part.hexsha - part.tutorial.save() - part.save() - - -def maj_repo_chapter( - request, - old_slug_path=None, - new_slug_path=None, - chapter=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - if chapter.tutorial: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.tutorial.get_phy_slug())) - ph = None - else: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug())) - ph = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug()) - index = repo.index - if action == "del": - shutil.rmtree(old_slug_path) - msg = _(u"Suppresion du chapitre : «{}»").format(chapter.title) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - if chapter.tutorial: - msg = _(u"Modification du tutoriel «{}» " - u"{} {}").format(chapter.tutorial.title, get_sep(msg), get_text_is_empty(msg)).strip() - else: - msg = _(u"Modification du chapitre «{}» " - u"{} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg)).strip() - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - msg = _(u"Création du chapitre «{}» {} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - if ph is not None: - index.add([ph]) - - # update manifest - - if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") - chapter.tutorial.dump_json(path=man_path) - else: - man_path = os.path.join(chapter.part.tutorial.get_repo_path(), - "manifest.json") - chapter.part.tutorial.dump_json(path=man_path) - index.add(["manifest.json"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com_ch = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - if chapter.tutorial: - chapter.tutorial.sha_draft = com_ch.hexsha - chapter.tutorial.save() - else: - chapter.part.tutorial.sha_draft = com_ch.hexsha - chapter.part.tutorial.save() - chapter.save() - - -def maj_repo_extract( - request, - old_slug_path=None, - new_slug_path=None, - extract=None, - text=None, - action=None, - msg=None, -): - - if extract.chapter.tutorial: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug())) - else: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.part.tutorial.get_phy_slug())) - index = repo.index - - chap = extract.chapter - - if action == "del": - msg = _(u"Suppression de l'extrait : «{}»").format(extract.title) - extract.delete() - if old_slug_path: - os.remove(old_slug_path) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - msg = _(u"Mise à jour de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - elif action == "add": - msg = _(u"Création de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - ext = open(new_slug_path, "w") - ext.write(smart_str(text).strip()) - ext.close() - index.add([extract.get_repo_path(relative=True)]) - - # update manifest - if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") - chap.tutorial.dump_json(path=man_path) - else: - man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") - chap.part.tutorial.dump_json(path=man_path) - - index.add(["manifest.json"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com_ex = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - if chap.tutorial: - chap.tutorial.sha_draft = com_ex.hexsha - chap.tutorial.save() - else: - chap.part.tutorial.sha_draft = com_ex.hexsha - chap.part.tutorial.save() - - def insert_into_zip(zip_file, git_tree): """recursively add files from a git_tree to a zip archive""" for blob in git_tree.blobs: # first, add files : From 5b6259e1d0cc372b20932215ee686bf757d73bdc Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 1 Feb 2015 10:59:53 +0100 Subject: [PATCH 023/887] =?UTF-8?q?am=C3=A9liore=20les=20perfs=20sde=20l'a?= =?UTF-8?q?ffichage=20+=20quelques=20commentaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 814f39ecdb..65089d153a 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -202,7 +202,12 @@ def get_forms(self, context, content): context["formReject"] = form_reject def get_object(self, queryset=None): - obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + query_set = PublishableContent.objects\ + .select_related("licence")\ + .prefetch_related("authors")\ + .filter(pk=self.kwargs["pk"]) + + obj = query_set.first() if obj.slug != self.kwargs['slug']: raise Http404 return obj @@ -657,6 +662,7 @@ def dispatch(self, *args, **kwargs): return super(EditExtract, self).dispatch(*args, **kwargs) def get_object(self): + """get the PublishableContent object that the user asked. We double check pk and slug""" obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) if obj.slug != self.kwargs['slug']: raise Http404 @@ -668,7 +674,7 @@ def get_context_data(self, **kwargs): context['content'] = self.content.load_version() container = context['content'] - # get the extract: + # if the extract is at a depth of 3 we get the first parent container if 'parent_container_slug' in self.kwargs: try: container = container.children_dict[self.kwargs['parent_container_slug']] @@ -678,6 +684,7 @@ def get_context_data(self, **kwargs): if not isinstance(container, Container): raise Http404 + # if extract is at depth 2 or 3 we get its direct parent container if 'container_slug' in self.kwargs: try: container = container.children_dict[self.kwargs['container_slug']] @@ -830,6 +837,7 @@ def get_queryset(self): .exclude(sha_public='') if self.tag is not None: query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') def get_context_data(self, **kwargs): From 91fb6bc3f914cfa732375ce60cea9a2f906eb6b8 Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 1 Feb 2015 11:29:47 +0100 Subject: [PATCH 024/887] =?UTF-8?q?derni=C3=A8re=20optimisation=20de=20req?= =?UTF-8?q?u=C3=AAte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/content.html | 14 +++++++------- zds/tutorialv2/views.py | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 20ed95446c..34e7c36b0e 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -38,13 +38,13 @@

    {% endif %} - {% if user in content.authors.all or perms.content.change_content %} + {% if user.user in content.authors.all %} {% include 'tutorialv2/includes/tags_authors.part.html' with content=content add_author=True %} {% else %} {% include 'tutorialv2/includes/tags_authors.part.html' with content=content %} {% endif %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% if content.in_validation %} {% if validation.version == version %} {% if validation.is_pending %} @@ -134,7 +134,7 @@

    {% block sidebar_new %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% if content.sha_draft == version %} {% trans "Éditer" %} @@ -163,7 +163,7 @@

    {% block sidebar_actions %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {# other action (valid, beta, ...) #} {% endif %} {% endblock %} @@ -171,13 +171,13 @@

    {% block sidebar_blocks %} - {% if perms.content.change_content %} - {# more actions ?!? #} + {% if can_edit and not user in content.authors.all %} + {# more actions only for validators?!? #} {% endif %} {# include "content/includes/summary.part.html" #} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% endif %} diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index 5353ca0590..d6593a96a0 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -132,70 +132,45 @@

    {% block sidebar_actions %} {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if chapter.part %} -
  • - - {% blocktrans %} - Déplacer le chapitre - {% endblocktrans %} - - +
  • - {% if chapter.position_in_part > 1 %} - - {% endif %} - - {% if chapter.position_in_part < chapter.part.chapters|length %} - - {% endif %} - - - {% for chapter_mv in chapter.part.chapters %} - {% if chapter != chapter_mv and chapter_mv.position_in_part|add:-1 != chapter.position_in_part %} - - {% endif %} - {% endfor %} - - - {% for chapter_mv in chapter.part.chapters %} - {% if chapter != chapter_mv and chapter_mv.position_in_part|add:1 != chapter.position_in_part %} - - {% endif %} - {% endfor %} - - - - - {% csrf_token %} - - - - - {% endif %} {% endif %} {% endblock %} From 85be0d168bb9aa0abf1983abb16a726ba07df90f Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 15 Mar 2015 22:22:51 +0100 Subject: [PATCH 074/887] =?UTF-8?q?debut=20d=C3=A9placement=20inter-partie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 27 ++++++++++++++++++++++++- zds/tutorialv2/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++ zds/tutorialv2/views.py | 6 ++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 4be3ea49e2..59748a7d8a 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -524,7 +524,7 @@ def move_child_down(self, child_slug): def move_child_after(self, child_slug, refer_slug): """ - Change the child's ordering by moving down the child whose slug equals child_slug. + Change the child's ordering by moving the child to be below the reference child. This method **does not** automaticaly update the repo :param child_slug: the child's slug :param refer_slug: the referent child's slug. @@ -536,6 +536,7 @@ def move_child_after(self, child_slug, refer_slug): raise ValueError(refer_slug + " does not exist") child_pos = self.children.index(self.children_dict[child_slug]) refer_pos = self.children.index(self.children_dict[refer_slug]) + # if we want our child to get down (reference is an lower child) if child_pos < refer_pos: for i in range(child_pos, refer_pos): @@ -545,6 +546,30 @@ def move_child_after(self, child_slug, refer_slug): for i in range(child_pos, refer_pos + 1, - 1): self.move_child_up(child_slug) + def move_child_before(self, child_slug, refer_slug): + """ + Change the child's ordering by moving the child to be just above the reference child. . + This method **does not** automaticaly update the repo + :param child_slug: the child's slug + :param refer_slug: the referent child's slug. + :raise ValueError: if one slug does not refer to an existing child + """ + if child_slug not in self.children_dict: + raise ValueError(child_slug + " does not exist") + if refer_slug not in self.children_dict: + raise ValueError(refer_slug + " does not exist") + child_pos = self.children.index(self.children_dict[child_slug]) + refer_pos = self.children.index(self.children_dict[refer_slug]) + + # if we want our child to get down (reference is an lower child) + if child_pos < refer_pos: + for i in range(child_pos, refer_pos - 1): + self.move_child_down(child_slug) + elif child_pos > refer_pos: + # if we want our child to get up (reference is an upper child) + for i in range(child_pos, refer_pos, - 1): + self.move_child_up(child_slug) + class Extract: """ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index d9990bbbf3..0f618b43ca 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -6,6 +6,15 @@ from zds.utils import get_current_user +class TooDeepContainerError(ValueError): + """ + Exception used to represent the fact you can't add a container to a level greater than two + """ + + def __init__(self, *args, **kwargs): + super(TooDeepContainerError, self).__init__(*args, **kwargs) + + def search_container_or_404(base_content, kwargs_array): """ :param base_content: the base Publishable content we will use to retrieve the container @@ -122,3 +131,37 @@ def mark_read(content): content=content, user=get_current_user()) a.save() + + +def try_adopt_new_child(adoptive_parent_full_path, child, root): + """ + Try the adoptive parent to take the responsability of the child + :param parent_full_path: + :param child_slug: + :param root: + :raise Http404: if adoptive_parent_full_path is not found on root hierarchy + :raise TypeError: if the adoptive parent is not allowed to adopt the child due to its type + :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent + :return: + """ + splitted = adoptive_parent_full_path.split('/') + if len(splitted) == 1: + container = root + elif len(splitted) == 2: + container = search_container_or_404(root, {'parent_container_slug': splitted[1]}) + elif len(splitted) == 3: + container = search_container_or_404(root, + {'parent_container_slug': splitted[1], + 'container_slug': splitted[2]}) + else: + raise Http404 + if isinstance(child, Extract): + if not container.can_add_extract(): + raise TypeError + # Todo : handle file deplacement + if isinstance(child, Container): + if not container.can_add_container(): + raise TypeError + if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: + raise TooDeepContainerError + # Todo: handle dir deplacement diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8bc8c87755..8aca85012b 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1024,6 +1024,12 @@ def form_valid(self, form): parent.move_child_up(child_slug) if form.data['moving_method'] == MoveElementForm.MOVE_DOWN: parent.move_child_down(child_slug) + if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: + target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] + parent.move_child_after(child_slug, target) + if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: + target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] + parent.move_child_before(child_slug, target) versioned.dump_json() parent.repo_update(parent.title, From 86217e143b6f8ec3e880216b4de51b3849a3c017 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 17 Mar 2015 16:53:17 +0100 Subject: [PATCH 075/887] adoption d'un enfant --- zds/tutorialv2/models.py | 16 ++++++++++------ zds/tutorialv2/utils.py | 7 +++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 59748a7d8a..cca2ed1c73 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -460,9 +460,10 @@ def repo_add_extract(self, title, text, commit_message=''): return cm.hexsha - def repo_delete(self, commit_message=''): + def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided + :param do_commit: tells if we have to commit the change now or let the outter program do it :return: commit sha """ path = self.get_path(relative=True) @@ -482,9 +483,10 @@ def repo_delete(self, commit_message=''): if commit_message == '': commit_message = u'Suppression du conteneur « {} »'.format(self.title) - cm = repo.index.commit(commit_message, **get_commit_author()) + if do_commit: + cm = repo.index.commit(commit_message, **get_commit_author()) - self.top_container().sha_draft = cm.hexsha + self.top_container().sha_draft = cm.hexsha return cm.hexsha @@ -710,9 +712,10 @@ def repo_update(self, title, text, commit_message=''): return cm.hexsha - def repo_delete(self, commit_message=''): + def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided + :param do_commit: tells if we have to commit the change now or let the outter program do it :return: commit sha """ path = self.get_path(relative=True) @@ -732,9 +735,10 @@ def repo_delete(self, commit_message=''): if commit_message == '': commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) - cm = repo.index.commit(commit_message, **get_commit_author()) + if do_commit: + cm = repo.index.commit(commit_message, **get_commit_author()) - self.container.top_container().sha_draft = cm.hexsha + self.container.top_container().sha_draft = cm.hexsha return cm.hexsha diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 0f618b43ca..52fc96cc20 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -158,10 +158,13 @@ def try_adopt_new_child(adoptive_parent_full_path, child, root): if isinstance(child, Extract): if not container.can_add_extract(): raise TypeError - # Todo : handle file deplacement + child.repo_delete('', False) + container.add_extract(child, generate_slug=False) if isinstance(child, Container): if not container.can_add_container(): raise TypeError if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - # Todo: handle dir deplacement + child.repo_delete('', False) + container.add_container(child) + From 225aac686c1bfae56cc8be67b27ce59d5fc3517f Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Fri, 20 Mar 2015 14:06:31 +0100 Subject: [PATCH 076/887] =?UTF-8?q?d=C3=A9placement=20'avant=20XXX'=20impl?= =?UTF-8?q?=C3=A9ment=C3=A9=20et=20test=C3=A9=20quand=20ils=20sont=20fr?= =?UTF-8?q?=C3=A8res?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 10 ++++++++++ zds/tutorialv2/tests/tests_views.py | 29 +++++++++++++++++++++++++++++ zds/tutorialv2/utils.py | 21 +++++++++------------ zds/tutorialv2/views.py | 15 +++++++++++++-- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cca2ed1c73..3e5d9f08fb 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -130,6 +130,16 @@ def get_tree_depth(self): depth += 1 return depth + def has_child_with_path(self, child_path): + """ + Check that the given path represent the full path + of a child of this container. + :param child_path: the full path (/maincontainer/subc1/subc2/childslug) we want to check + """ + if self.get_path(True) not in child_path: + return False + return child_path.replace(self.get_path(True), "").replace("/", "") in self.children_dict + def top_container(self): """ :return: Top container (for which parent is `None`) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index cf4ebf7e95..09e01cf154 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -769,6 +769,35 @@ def test_move_up_extract(self): follow=False) self.assertEqual(result.status_code, 403) + def test_move_before(self): + # test 1 : move extract after a sibling + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.extract2 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + self.extract3 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + old_sha = tuto.sha_draft + # test moving smoothly + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.extract3.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] + self.assertEqual(self.extract2.slug, extract.slug) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 52fc96cc20..bb9c644201 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -19,10 +19,17 @@ def search_container_or_404(base_content, kwargs_array): """ :param base_content: the base Publishable content we will use to retrieve the container :param kwargs_array: an array that may contain `parent_container_slug` and `container_slug` keys + or the string representation :return: the Container object we were searching for :raise Http404 if no suitable container is found """ container = None + if isinstance(kwargs_array, str): + dic = {} + dic["parent_container_slug"] = kwargs_array.split("/")[0] + if len(kwargs_array.split("/")) == 2: + dic["container_slug"] = kwargs_array.split("/")[1] + kwargs_array = dic if 'parent_container_slug' in kwargs_array: try: @@ -133,7 +140,7 @@ def mark_read(content): a.save() -def try_adopt_new_child(adoptive_parent_full_path, child, root): +def try_adopt_new_child(adoptive_parent, child): """ Try the adoptive parent to take the responsability of the child :param parent_full_path: @@ -144,17 +151,7 @@ def try_adopt_new_child(adoptive_parent_full_path, child, root): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - splitted = adoptive_parent_full_path.split('/') - if len(splitted) == 1: - container = root - elif len(splitted) == 2: - container = search_container_or_404(root, {'parent_container_slug': splitted[1]}) - elif len(splitted) == 3: - container = search_container_or_404(root, - {'parent_container_slug': splitted[1], - 'container_slug': splitted[2]}) - else: - raise Http404 + container = adoptive_parent if isinstance(child, Extract): if not container.can_add_extract(): raise TypeError diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8aca85012b..15ab388a65 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1026,10 +1026,21 @@ def form_valid(self, form): parent.move_child_down(child_slug) if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] - parent.move_child_after(child_slug, target) + if not parent.has_child_with_path(target): + target_parent = search_container_or_404(versionned, target.split("/")[1:-2]) + child = target_parent.children_dict[target.split("/")[-1]] + try_adopt_new_child(target_parent, parent[child_slug]) + target_parent.move_child_after(child_slug, target) if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] - parent.move_child_before(child_slug, target) + if not parent.has_child_with_path(target): + + target_parent = search_container_or_404(versioned, target.split("/")[1:-2]) + if target.split("/")[-1] not in target_parent: + raise Http404 + child = target_parent.children_dict[target.split("/")[-1]] + try_adopt_new_child(target_parent, parent[child_slug]) + parent.move_child_before(child_slug, target.split("/")[-1]) versioned.dump_json() parent.repo_update(parent.title, From 653a5cdfcad406abdf6dae372970f509ac45b9ec Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Mon, 23 Mar 2015 14:15:23 +0100 Subject: [PATCH 077/887] =?UTF-8?q?d=C3=A9placement=20dvers=20un=20nouveau?= =?UTF-8?q?=20parent=20test=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 5 +++-- zds/tutorialv2/tests/tests_views.py | 24 ++++++++++++++++++++++++ zds/tutorialv2/utils.py | 4 ++-- zds/tutorialv2/views.py | 11 +++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 3e5d9f08fb..efa1ae00b8 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -726,7 +726,7 @@ def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided :param do_commit: tells if we have to commit the change now or let the outter program do it - :return: commit sha + :return: commit sha, None if no commit is done """ path = self.get_path(relative=True) repo = self.container.top_container().repository @@ -750,7 +750,8 @@ def repo_delete(self, commit_message='', do_commit=True): self.container.top_container().sha_draft = cm.hexsha - return cm.hexsha + return cm.hexsha + return None class VersionedContent(Container): diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 09e01cf154..d8f46346f8 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -797,6 +797,30 @@ def test_move_before(self): versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] self.assertEqual(self.extract2.slug, extract.slug) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for extract (smoothly) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.extract4 = ExtractFactory(container=self.chapter2, db_object=self.tuto) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.extract4.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index bb9c644201..8301ad233c 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -24,10 +24,10 @@ def search_container_or_404(base_content, kwargs_array): :raise Http404 if no suitable container is found """ container = None - if isinstance(kwargs_array, str): + if isinstance(kwargs_array, basestring): dic = {} dic["parent_container_slug"] = kwargs_array.split("/")[0] - if len(kwargs_array.split("/")) == 2: + if len(kwargs_array.split("/")) >= 2: dic["container_slug"] = kwargs_array.split("/")[1] kwargs_array = dic diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 15ab388a65..4b37bfb229 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,6 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm +from zds.tutorialv2.utils import try_adopt_new_child from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -1034,12 +1035,14 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): - - target_parent = search_container_or_404(versioned, target.split("/")[1:-2]) - if target.split("/")[-1] not in target_parent: + + target_parent = search_container_or_404(versioned, target) + print target_parent.get_path() + if target.split("/")[-1] not in target_parent.children_dict: raise Http404 child = target_parent.children_dict[target.split("/")[-1]] - try_adopt_new_child(target_parent, parent[child_slug]) + try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) versioned.dump_json() From ed2b60660014a69e13287b5e404bf3d96d260ada Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 24 Mar 2015 14:30:12 +0100 Subject: [PATCH 078/887] =?UTF-8?q?ajout=20de=20tests=20dans=20le=20d?= =?UTF-8?q?=C3=A9placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 18 ++++- zds/tutorialv2/tests/tests_views.py | 107 +++++++++++++++++++++++++++- zds/tutorialv2/utils.py | 4 +- zds/tutorialv2/views.py | 36 ++++++---- 4 files changed, 145 insertions(+), 20 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index efa1ae00b8..66061bb1d8 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -116,6 +116,7 @@ def get_last_child_position(self): def get_tree_depth(self): """ + Represent the depth where this container is found Tree depth is no more than 2, because there is 3 levels for Containers : - PublishableContent (0), - Part (1), @@ -130,6 +131,20 @@ def get_tree_depth(self): depth += 1 return depth + def get_tree_level(self): + """ + Represent the level in the tree of this container, i.e the depth of its deepest child + :return: tree level + """ + current = self + if len(self.children) == 0: + return 1 + elif isinstance(self.children[0], Extract): + return 2 + else: + return 1 + max([i.get_tree_level() for i in self.children]) + + def has_child_with_path(self, child_path): """ Check that the given path represent the full path @@ -498,7 +513,8 @@ def repo_delete(self, commit_message='', do_commit=True): self.top_container().sha_draft = cm.hexsha - return cm.hexsha + return cm.hexsha + return None def move_child_up(self, child_slug): """ diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index d8f46346f8..da7ba39a01 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -769,9 +769,9 @@ def test_move_up_extract(self): follow=False) self.assertEqual(result.status_code, 403) - def test_move_before(self): + def test_move_extract_before(self): # test 1 : move extract after a sibling - # login with author + # login with author self.assertEqual( self.client.login( username=self.user_author.username, @@ -813,6 +813,7 @@ def test_move_before(self): 'pk': tuto.pk }, follow=True) + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -821,7 +822,107 @@ def test_move_before(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + # test try to move to a container that can't get extract + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.chapter1.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move near an extract that does not exist + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(404, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + + def test_move_container_before(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for container (smoothly) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.chapter3.slug, + 'container_slug': self.part1.slug, + 'first_level_slug': '', + 'moving_method': 'before:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter4.slug, chapter.slug) + # test changing parent for too deep container + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.part1.slug, + 'container_slug': self.tuto.slug, + 'first_level_slug': '', + 'moving_method': 'before:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter4.slug, chapter.slug) + + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 8301ad233c..c74db53e74 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -51,7 +51,7 @@ def search_container_or_404(base_content, kwargs_array): else: if not isinstance(container, Container): raise Http404 - else: + elif container == base_content: # if we have no subcontainer, there is neither "container_slug" nor "parent_container_slug return base_content if container is None: @@ -160,7 +160,7 @@ def try_adopt_new_child(adoptive_parent, child): if isinstance(child, Container): if not container.can_add_container(): raise TypeError - if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: + if container.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError child.repo_delete('', False) container.add_container(child) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4b37bfb229..1ef0f55553 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,7 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm -from zds.tutorialv2.utils import try_adopt_new_child +from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -1002,7 +1002,7 @@ def form_valid(self, form): versioned = content.load_version() base_container_slug = form.data["container_slug"] child_slug = form.data['child_slug'] - + if base_container_slug == '': raise Http404 @@ -1013,14 +1013,17 @@ def form_valid(self, form): parent = versioned else: search_params = {} + if form.data['first_level_slug'] != '': + search_params['parent_container_slug'] = form.data['first_level_slug'] search_params['container_slug'] = base_container_slug else: - search_params['parent_container_slug'] = base_container_slug + search_params['container_slug'] = base_container_slug parent = search_container_or_404(versioned, search_params) - + try: + child = parent.children_dict[child_slug] if form.data['moving_method'] == MoveElementForm.MOVE_UP: parent.move_child_up(child_slug) if form.data['moving_method'] == MoveElementForm.MOVE_DOWN: @@ -1035,13 +1038,16 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): - - target_parent = search_container_or_404(versioned, target) - print target_parent.get_path() - if target.split("/")[-1] not in target_parent.children_dict: - raise Http404 + if "/" not in target: + target_parent = versioned + else: + target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) + + if target.split("/")[-1] not in target_parent.children_dict: + raise Http404 child = target_parent.children_dict[target.split("/")[-1]] try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) @@ -1053,18 +1059,20 @@ def form_valid(self, form): content.sha_draft = versioned.sha_draft content.save() messages.info(self.request, _(u"L'élément a bien été déplacé.")) - + except TooDeepContainerError: + messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir'\ + ' la sous-section d\'une autre section.')) except ValueError: raise Http404 except IndexError: messages.warning(self.request, _(u"L'élément est déjà à la place souhaitée.")) + except TypeError: + messages.error(self.request, _(u"L'élément ne peut pas être déplacé à cet endroit")) if base_container_slug == versioned.slug: return redirect(reverse("content:view", args=[content.pk, content.slug])) - else: - search_params['slug'] = content.slug - search_params['pk'] = content.pk - return redirect(reverse("content:view-container", kwargs=search_params)) + else: + return redirect(child.get_absolute_url()) @can_write_and_read_now From 4a6daff5b5b6896393d335a9d7c939ebee421e9a Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Thu, 26 Mar 2015 13:55:37 +0100 Subject: [PATCH 079/887] =?UTF-8?q?tests=20du=20d=C3=A9placement=20termin?= =?UTF-8?q?=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 154 ++++++++++++++++++++++++++++ zds/tutorialv2/views.py | 13 ++- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index da7ba39a01..307545774b 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -921,7 +921,161 @@ def test_move_container_before(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[1] self.assertEqual(self.chapter4.slug, chapter.slug) + + def test_move_extract_after(self): + # test 1 : move extract after a sibling + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.extract2 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + self.extract3 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + old_sha = tuto.sha_draft + # test moving smoothly + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.extract3.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] + self.assertEqual(self.extract2.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[1] + self.assertEqual(self.extract3.slug, extract.slug) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for extract (smoothly) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.extract4 = ExtractFactory(container=self.chapter2, db_object=self.tuto) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.extract4.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move to a container that can't get extract + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.chapter1.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move near an extract that does not exist + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(404, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + + def test_move_container_after(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for container (smoothly) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.chapter3.slug, + 'container_slug': self.part1.slug, + 'first_level_slug': '', + 'moving_method': 'after:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter4.slug, chapter.slug) + # test changing parent for too deep container + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.part1.slug, + 'container_slug': self.tuto.slug, + 'first_level_slug': '', + 'moving_method': 'after:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter4.slug, chapter.slug) def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 1ef0f55553..50cb841b0a 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1031,10 +1031,17 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] if not parent.has_child_with_path(target): - target_parent = search_container_or_404(versionned, target.split("/")[1:-2]) + if "/" not in target: + target_parent = versioned + else: + target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) + + if target.split("/")[-1] not in target_parent.children_dict: + raise Http404 child = target_parent.children_dict[target.split("/")[-1]] - try_adopt_new_child(target_parent, parent[child_slug]) - target_parent.move_child_after(child_slug, target) + try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent + parent.move_child_after(child_slug, target.split("/")[-1]) if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): From 16a7cb145d3c7b2a334d406813186b3cd9f1f42b Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Mon, 30 Mar 2015 14:02:16 +0200 Subject: [PATCH 080/887] =?UTF-8?q?pr=C3=A9paration=20=C3=A0=20l'int=C3=A9?= =?UTF-8?q?gration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 21 ++++++++++++++ zds/tutorialv2/utils.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 66061bb1d8..353eb9b739 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -597,6 +597,20 @@ def move_child_before(self, child_slug, refer_slug): # if we want our child to get up (reference is an upper child) for i in range(child_pos, refer_pos, - 1): self.move_child_up(child_slug) + + def traverse(self, only_container=True): + """ + traverse the + :param only_container: if we only want container's paths, not extract + :return: a generator that traverse all the container recursively (depth traversal) + """ + yield self + for child in children: + if isinstance(child, Container): + for _ in child.traverse(only_container): + yield _ + elif not only_container: + yield child class Extract: @@ -666,6 +680,13 @@ def get_edit_url(self): return reverse('content:edit-extract', args=args) + def get_full_slug(self): + """ + get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) + this method is an alias to extract.get_path(True)[:-3] (remove .md extension) + """ + return self.get_path(True)[:-3] + def get_delete_url(self): """ :return: url to delete the extract diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index c74db53e74..ea7c6cdfa6 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -165,3 +165,64 @@ def try_adopt_new_child(adoptive_parent, child): child.repo_delete('', False) container.add_container(child) +def get_target_tagged_tree(moveable_child, root): + """ + Gets the tagged tree with deplacement availability + :param moveable_child: the extract we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + check get_target_tagged_tree_for_extract and get_target_tagged_tree_for_container for format + """ + if isinstance(moveable_child, Extract): + return get_target_tagged_tree_for_extract(moveable_child, root) + else: + return get_target_tagged_tree_for_container(moveable_child, root) + +def get_target_tagged_tree_for_extract(moveable_child, root): + """ + Gets the tagged tree with deplacement availability when moveable_child is an extract + :param moveable_child: the extract we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + .. sourcecode::python + [ + (relative_path_root, False), + (relative_path_of_a_container, False), + (relative_path_of_another_extract, True) + ... + ] + """ + target_tagged_tree = [] + for child in root.traverse(False): + if is_instance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), child != moveable_child)) + else: + target_tagged_tree.append((child.get_path(True), False)) + + return target_tagged_tree + +def get_target_tagged_tree_for_container(moveable_child, root): + """ + Gets the tagged tree with deplacement availability when moveable_child is an extract + :param moveable_child: the container we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + .. sourcecode::python + [ + (relative_path_root, True), + (relative_path_of_a_too_deep_container, False), + (relativbe_path_of_a_good_container, False), + (relative_path_of_another_extract, False) + ... + ] + """ + target_tagged_tree = [] + for child in root.traverse(False): + if is_instance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), False)) + else: + composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + enabled = composed_depth > settings.ZDS_APP['content']['max_tree_depth'] + target_tagged_tree.append((child.get_path(True), enabled and child != moveable_child)) + + return target_tagged_tree From 3b797521313f4797951e1700bab637243581ec19 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 31 Mar 2015 14:03:35 +0200 Subject: [PATCH 081/887] =?UTF-8?q?int=C3=A9gration=20du=20d=C3=A9placemen?= =?UTF-8?q?t=20des=20sections=20dans=20la=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/container.html | 18 ++++++++- zds/tutorialv2/models.py | 3 +- zds/tutorialv2/utils.py | 47 ++++++++---------------- zds/tutorialv2/views.py | 7 ++-- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index d6593a96a0..26815e3a9a 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -159,9 +159,23 @@

    {% trans "Descendre" %} {% endif %} + + {% for element in containers_target %} + + {% endfor %} + + {% for element in containers_target %} + + {% endfor %} - {% if container.parent.parent %} - + {% if container.get_tree_depth = 2 %} + {% endif %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 353eb9b739..2dbad4f899 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -34,7 +34,6 @@ from zds.utils.models import HelpWriting from uuslug import uuslug - TYPE_CHOICES = ( ('TUTORIAL', 'Tutoriel'), ('ARTICLE', 'Article'), @@ -605,7 +604,7 @@ def traverse(self, only_container=True): :return: a generator that traverse all the container recursively (depth traversal) """ yield self - for child in children: + for child in self.children: if isinstance(child, Container): for _ in child.traverse(only_container): yield _ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index ea7c6cdfa6..13393b883e 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,19 +151,19 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - container = adoptive_parent + adoptive_parent if isinstance(child, Extract): - if not container.can_add_extract(): + if not adoptive_parent.can_add_extract(): raise TypeError child.repo_delete('', False) - container.add_extract(child, generate_slug=False) + adoptive_parent.add_extract(child, generate_slug=False) if isinstance(child, Container): - if not container.can_add_container(): + if not adoptive_parent.can_add_container(): raise TypeError - if container.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: + if adoptive_parent.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - child.repo_delete('', False) - container.add_container(child) + adoptive_parent.repo_delete('', False) + adoptive_parent.add_container(child) def get_target_tagged_tree(moveable_child, root): """ @@ -184,20 +184,14 @@ def get_target_tagged_tree_for_extract(moveable_child, root): :param moveable_child: the extract we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - .. sourcecode::python - [ - (relative_path_root, False), - (relative_path_of_a_container, False), - (relative_path_of_another_extract, True) - ... - ] + tuples are (relative_path, title, level, can_be_a_target) """ target_tagged_tree = [] for child in root.traverse(False): if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child != moveable_child)) + target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_level(), child != moveable_child)) else: - target_tagged_tree.append((child.get_path(True), False)) + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) return target_tagged_tree @@ -207,22 +201,13 @@ def get_target_tagged_tree_for_container(moveable_child, root): :param moveable_child: the container we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - .. sourcecode::python - [ - (relative_path_root, True), - (relative_path_of_a_too_deep_container, False), - (relativbe_path_of_a_good_container, False), - (relative_path_of_another_extract, False) - ... - ] + extracts are not included """ target_tagged_tree = [] - for child in root.traverse(False): - if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), False)) - else: - composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() - enabled = composed_depth > settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), enabled and child != moveable_child)) + for child in root.traverse(True): + composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), + enabled and child != moveable_child and child != root)) return target_tagged_tree diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 50cb841b0a..0857185a42 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,7 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm -from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError +from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError, get_target_tagged_tree from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -348,7 +348,7 @@ def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" context = super(DisplayContainer, self).get_context_data(**kwargs) context['container'] = search_container_or_404(context['content'], self.kwargs) - + context['containers_target'] = get_target_tagged_tree(context['container'], context['content']) # pagination: search for `previous` and `next`, if available if context['content'].type != 'ARTICLE' and not context['content'].has_extracts(): chapters = context['content'].get_list_of_chapters() @@ -374,8 +374,7 @@ class EditContainer(LoggedWithReadWriteHability, SingleContentFormViewMixin): content = None def get_context_data(self, **kwargs): - context = super(EditContainer, self).get_context_data(**kwargs) - context['container'] = search_container_or_404(self.versioned_object, self.kwargs) + context = super(EditContainer, self).get_context_data(**kwargs) return context From 90545598df3a0169dfa27ceb5eef7a2a6b95a8a4 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Tue, 31 Mar 2015 22:10:41 -0400 Subject: [PATCH 082/887] Assure qu'on puisse naviguer dans des versions precedentes --- templates/tutorialv2/includes/child.part.html | 8 ++- .../tutorialv2/includes/summary.part.html | 7 ++- templates/tutorialv2/view/content.html | 2 +- zds/tutorialv2/mixins.py | 10 +++- zds/tutorialv2/models.py | 54 +++++++++++++----- zds/tutorialv2/tests/tests_views.py | 48 ++++++++-------- zds/tutorialv2/utils.py | 56 ++++++++++--------- zds/tutorialv2/views.py | 17 +++--- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html index db052d0434..a30c23b4b1 100644 --- a/templates/tutorialv2/includes/child.part.html +++ b/templates/tutorialv2/includes/child.part.html @@ -3,7 +3,13 @@

    - + {{ child.title }}

    diff --git a/templates/tutorialv2/includes/summary.part.html b/templates/tutorialv2/includes/summary.part.html index 11a37419fe..e13e1fbe9f 100644 --- a/templates/tutorialv2/includes/summary.part.html +++ b/templates/tutorialv2/includes/summary.part.html @@ -48,7 +48,12 @@

    {% if online %} href="{{ subchild.get_absolute_url_online }}" {% else %} - href="{{ subchild.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}" + {% if subchild.text %} + {# subchild is an extract #} + href="{{ subchild.container.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}#{{ subchild.position_in_parent }}-{{ subchild.slug }}" + {% else %} + href="{{ subchild.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}" + {% endif %} {% endif %} class="mobile-menu-link mobile-menu-sublink {% if current_container.long_slug == subchild.long_slug %}unread{% endif %}" > diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 5bc7eabd7e..b1d67076b9 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -153,7 +153,7 @@

    {% endif %} {% else %} - + {% trans "Version brouillon" %} {% endif %} diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index aff362c9c7..0f486fca01 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -30,8 +30,6 @@ def get_object(self, queryset=None): obj = queryset.first() else: obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) - if 'slug' in self.kwargs and obj.slug != self.kwargs['slug']: - raise Http404 if self.must_be_author and self.request.user not in obj.authors.all(): if self.authorized_for_staff and self.request.user.has_perm('tutorial.change_tutorial'): return obj @@ -57,7 +55,13 @@ def get_versioned_object(self): raise PermissionDenied # load versioned file - return self.object.load_version_or_404(sha) + versioned = self.object.load_version_or_404(sha) + + if 'slug' in self.kwargs \ + and (versioned.slug != self.kwargs['slug'] and self.object.slug != self.kwargs['slug']): + raise Http404 + + return versioned class SingleContentPostMixin(SingleContentViewMixin): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 2dbad4f899..b9a45e0fe0 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -135,7 +135,7 @@ def get_tree_level(self): Represent the level in the tree of this container, i.e the depth of its deepest child :return: tree level """ - current = self + if len(self.children) == 0: return 1 elif isinstance(self.children[0], Extract): @@ -143,7 +143,6 @@ def get_tree_level(self): else: return 1 + max([i.get_tree_level() for i in self.children]) - def has_child_with_path(self, child_path): """ Check that the given path represent the full path @@ -596,12 +595,12 @@ def move_child_before(self, child_slug, refer_slug): # if we want our child to get up (reference is an upper child) for i in range(child_pos, refer_pos, - 1): self.move_child_up(child_slug) - + def traverse(self, only_container=True): """ - traverse the + Traverse the container :param only_container: if we only want container's paths, not extract - :return: a generator that traverse all the container recursively (depth traversal) + :return: a generator that traverse all the container recursively (depth traversal) """ yield self for child in self.children: @@ -681,7 +680,7 @@ def get_edit_url(self): def get_full_slug(self): """ - get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) + get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) this method is an alias to extract.get_path(True)[:-3] (remove .md extension) """ return self.get_path(True)[:-3] @@ -800,6 +799,7 @@ class VersionedContent(Container): """ current_version = None + slug_repository = '' repository = None # Metadata from json : @@ -836,11 +836,25 @@ class VersionedContent(Container): update_date = None source = None - def __init__(self, current_version, _type, title, slug): + def __init__(self, current_version, _type, title, slug, slug_repository=''): + """ + :param current_version: version of the content + :param _type: either "TUTORIAL" or "ARTICLE" + :param title: title of the content + :param slug: slug of the content + :param slug_repository: slug of the directory that contains the repository, named after database slug. + if not provided, use `self.slug` instead. + """ + Container.__init__(self, title, slug) self.current_version = current_version self.type = _type + if slug_repository != '': + self.slug_repository = slug_repository + else: + self.slug_repository = slug + if os.path.exists(self.get_path()): self.repository = Repo(self.get_path()) @@ -873,16 +887,20 @@ def get_absolute_url_beta(self): else: return self.get_absolute_url() - def get_path(self, relative=False): + def get_path(self, relative=False, use_current_slug=False): """ Get the physical path to the draft version of the Content. :param relative: if `True`, the path will be relative, absolute otherwise. + :param use_current_slug: if `True`, use `self.slug` instead of `self.slug_last_draft` :return: physical path """ if relative: return '' else: - return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) + slug = self.slug_repository + if use_current_slug: + slug = self.slug + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], slug) def get_prod_path(self): """ @@ -943,16 +961,17 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi if slug != self.slug: # move repository - old_path = self.get_path() + old_path = self.get_path(use_current_slug=True) self.slug = slug - new_path = self.get_path() + new_path = self.get_path(use_current_slug=True) shutil.move(old_path, new_path) self.repository = Repo(new_path) + self.slug_repository = slug return self.repo_update(title, introduction, conclusion, commit_message) -def get_content_from_json(json, sha): +def get_content_from_json(json, sha, slug_last_draft): """ Transform the JSON formated data into `VersionedContent` :param json: JSON data from a `manifest.json` file @@ -961,7 +980,7 @@ def get_content_from_json(json, sha): """ # TODO: should definitely be static # create and fill the container - versioned = VersionedContent(sha, 'TUTORIAL', json['title'], json['slug']) + versioned = VersionedContent(sha, 'TUTORIAL', json['title'], json['slug'], slug_last_draft) if 'version' in json and json['version'] == 2: # fill metadata : @@ -1055,6 +1074,7 @@ def init_new_repo(db_object, introduction_text, conclusion_text, commit_message= versioned_content = VersionedContent(None, db_object.type, db_object.title, + db_object.slug, db_object.slug) # fill some information that are missing : @@ -1275,7 +1295,7 @@ def load_version_or_404(self, sha=None, public=False): """ try: return self.load_version(sha, public) - except BadObject: + except (BadObject, IOError): raise Http404 def load_version(self, sha=None, public=False): @@ -1296,10 +1316,14 @@ def load_version(self, sha=None, public=False): sha = self.sha_public path = self.get_repo_path() + + if not os.path.isdir(path): + raise IOError(path) + repo = Repo(path) data = get_blob(repo.commit(sha).tree, 'manifest.json') json = json_reader.loads(data) - versioned = get_content_from_json(json, sha) + versioned = get_content_from_json(json, sha, self.slug) self.insert_data_in_versioned(versioned) return versioned diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 307545774b..1e0c5cfbc9 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -788,7 +788,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.extract3.get_path(True)[:-3], + 'moving_method': 'before:' + self.extract3.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) @@ -797,7 +797,7 @@ def test_move_extract_before(self): versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] self.assertEqual(self.extract2.slug, extract.slug) - + tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft # test changing parent for extract (smoothly) @@ -809,11 +809,11 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.extract4.get_path(True)[:-3], + 'moving_method': 'before:' + self.extract4.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -831,7 +831,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.chapter1.get_path(True), + 'moving_method': 'before:' + self.chapter1.get_path(True), 'pk': tuto.pk }, follow=True) @@ -852,7 +852,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'moving_method': 'before:' + self.chapter1.get_path(True) + "/un-mauvais-extrait", 'pk': tuto.pk }, follow=True) @@ -864,7 +864,7 @@ def test_move_extract_before(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + def test_move_container_before(self): # login with author self.assertEqual( @@ -886,11 +886,11 @@ def test_move_container_before(self): 'child_slug': self.chapter3.slug, 'container_slug': self.part1.slug, 'first_level_slug': '', - 'moving_method': 'before:'+self.chapter4.get_path(True), + 'moving_method': 'before:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -908,11 +908,11 @@ def test_move_container_before(self): 'child_slug': self.part1.slug, 'container_slug': self.tuto.slug, 'first_level_slug': '', - 'moving_method': 'before:'+self.chapter4.get_path(True), + 'moving_method': 'before:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -921,7 +921,7 @@ def test_move_container_before(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[1] self.assertEqual(self.chapter4.slug, chapter.slug) - + def test_move_extract_after(self): # test 1 : move extract after a sibling # login with author @@ -941,7 +941,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.extract3.get_path(True)[:-3], + 'moving_method': 'after:' + self.extract3.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) @@ -952,7 +952,7 @@ def test_move_extract_after(self): self.assertEqual(self.extract2.slug, extract.slug) extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[1] self.assertEqual(self.extract3.slug, extract.slug) - + tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft # test changing parent for extract (smoothly) @@ -964,11 +964,11 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.extract4.get_path(True)[:-3], + 'moving_method': 'after:' + self.extract4.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -986,7 +986,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.chapter1.get_path(True), + 'moving_method': 'after:' + self.chapter1.get_path(True), 'pk': tuto.pk }, follow=True) @@ -1007,7 +1007,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'moving_method': 'after:' + self.chapter1.get_path(True) + "/un-mauvais-extrait", 'pk': tuto.pk }, follow=True) @@ -1019,7 +1019,7 @@ def test_move_extract_after(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + def test_move_container_after(self): # login with author self.assertEqual( @@ -1041,11 +1041,11 @@ def test_move_container_after(self): 'child_slug': self.chapter3.slug, 'container_slug': self.part1.slug, 'first_level_slug': '', - 'moving_method': 'after:'+self.chapter4.get_path(True), + 'moving_method': 'after:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -1063,11 +1063,11 @@ def test_move_container_after(self): 'child_slug': self.part1.slug, 'container_slug': self.tuto.slug, 'first_level_slug': '', - 'moving_method': 'after:'+self.chapter4.get_path(True), + 'moving_method': 'after:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -1076,7 +1076,7 @@ def test_move_container_after(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[0] self.assertEqual(self.chapter4.slug, chapter.slug) - + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 13393b883e..edb6c0c585 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,7 +151,6 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - adoptive_parent if isinstance(child, Extract): if not adoptive_parent.can_add_extract(): raise TypeError @@ -165,49 +164,56 @@ def try_adopt_new_child(adoptive_parent, child): adoptive_parent.repo_delete('', False) adoptive_parent.add_container(child) -def get_target_tagged_tree(moveable_child, root): + +def get_target_tagged_tree(movable_child, root): """ Gets the tagged tree with deplacement availability - :param moveable_child: the extract we want to move + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + :return: an array of tuples that represent the capacity of movable_child to be moved near another child check get_target_tagged_tree_for_extract and get_target_tagged_tree_for_container for format """ - if isinstance(moveable_child, Extract): - return get_target_tagged_tree_for_extract(moveable_child, root) + if isinstance(movable_child, Extract): + return get_target_tagged_tree_for_extract(movable_child, root) else: - return get_target_tagged_tree_for_container(moveable_child, root) - -def get_target_tagged_tree_for_extract(moveable_child, root): + return get_target_tagged_tree_for_container(movable_child, root) + + +def get_target_tagged_tree_for_extract(movable_child, root): """ - Gets the tagged tree with deplacement availability when moveable_child is an extract - :param moveable_child: the extract we want to move + Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - tuples are (relative_path, title, level, can_be_a_target) + :return: an array of tuples that represent the capacity of movable_child to be moved near another child + tuples are (relative_path, title, level, can_be_a_target) """ target_tagged_tree = [] for child in root.traverse(False): - if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_level(), child != moveable_child)) + if isinstance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), + child.title, + child.container.get_tree_level() + 1, + child != movable_child)) else: target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) - + return target_tagged_tree - -def get_target_tagged_tree_for_container(moveable_child, root): + + +def get_target_tagged_tree_for_container(movable_child, root): """ - Gets the tagged tree with deplacement availability when moveable_child is an extract - :param moveable_child: the container we want to move + Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the container we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + :return: an array of tuples that represent the capacity of movable_child to be moved near another child extracts are not included """ target_tagged_tree = [] for child in root.traverse(True): - composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + composed_depth = child.get_tree_depth() + movable_child.get_tree_level() enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), - enabled and child != moveable_child and child != root)) - + target_tagged_tree.append((child.get_path(True), + child.title, child.get_tree_level(), + enabled and child != movable_child and child != root)) + return target_tagged_tree diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 0857185a42..6e271fd5cf 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -374,7 +374,7 @@ class EditContainer(LoggedWithReadWriteHability, SingleContentFormViewMixin): content = None def get_context_data(self, **kwargs): - context = super(EditContainer, self).get_context_data(**kwargs) + context = super(EditContainer, self).get_context_data(**kwargs) return context @@ -1001,7 +1001,7 @@ def form_valid(self, form): versioned = content.load_version() base_container_slug = form.data["container_slug"] child_slug = form.data['child_slug'] - + if base_container_slug == '': raise Http404 @@ -1014,13 +1014,12 @@ def form_valid(self, form): search_params = {} if form.data['first_level_slug'] != '': - search_params['parent_container_slug'] = form.data['first_level_slug'] search_params['container_slug'] = base_container_slug else: search_params['container_slug'] = base_container_slug parent = search_container_or_404(versioned, search_params) - + try: child = parent.children_dict[child_slug] if form.data['moving_method'] == MoveElementForm.MOVE_UP: @@ -1034,7 +1033,7 @@ def form_valid(self, form): target_parent = versioned else: target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) - + if target.split("/")[-1] not in target_parent.children_dict: raise Http404 child = target_parent.children_dict[target.split("/")[-1]] @@ -1053,7 +1052,7 @@ def form_valid(self, form): raise Http404 child = target_parent.children_dict[target.split("/")[-1]] try_adopt_new_child(target_parent, parent.children_dict[child_slug]) - + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) @@ -1066,8 +1065,8 @@ def form_valid(self, form): content.save() messages.info(self.request, _(u"L'élément a bien été déplacé.")) except TooDeepContainerError: - messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir'\ - ' la sous-section d\'une autre section.')) + messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir' + u' la sous-section d\'une autre section.')) except ValueError: raise Http404 except IndexError: @@ -1077,7 +1076,7 @@ def form_valid(self, form): if base_container_slug == versioned.slug: return redirect(reverse("content:view", args=[content.pk, content.slug])) - else: + else: return redirect(child.get_absolute_url()) From c2bb07431918392e65d1e6309d339c75cd3c84a2 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Wed, 1 Apr 2015 14:03:54 +0200 Subject: [PATCH 083/887] =?UTF-8?q?le=20repo=20est=20correctement=20d?= =?UTF-8?q?=C3=A9plac=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 17 +++++++++++++++++ zds/tutorialv2/utils.py | 8 +++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 2dbad4f899..0921900083 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -951,6 +951,23 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi return self.repo_update(title, introduction, conclusion, commit_message) + def change_child_directory(self, child, adoptive_parent): + + old_path = child.get_path(False) # absolute path because we want to access the adress + adoptive_parent_path = adoptive_parent.get_path(False) + if isinstance(child, Extract): + old_parent = child.container + old_parent.children = [c for c in old_parent.children if c.slug != child.slug] + adoptive_parent.add_extract(child) + else: + old_parent = child.parent + old_parent.children = [c for c in old_parent.children if c.slug != child.slug] + adoptive_parent.add_container(child) + self.repository.index.move([old_path, child.get_path(False)]) + + self.dump_json() + + def get_content_from_json(json, sha): """ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 13393b883e..0cf2387339 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,19 +151,17 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - adoptive_parent + if isinstance(child, Extract): if not adoptive_parent.can_add_extract(): raise TypeError - child.repo_delete('', False) - adoptive_parent.add_extract(child, generate_slug=False) if isinstance(child, Container): if not adoptive_parent.can_add_container(): raise TypeError if adoptive_parent.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - adoptive_parent.repo_delete('', False) - adoptive_parent.add_container(child) + adoptive_parent.top_container().change_child_directory(child, adoptive_parent) + def get_target_tagged_tree(moveable_child, root): """ From 79843815f5f8cc565218bf96c1f7288835ff0325 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Wed, 1 Apr 2015 14:26:53 +0200 Subject: [PATCH 084/887] =?UTF-8?q?Am=C3=A9liore=20l'int=C3=A9gration=20da?= =?UTF-8?q?ns=20la=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/container.html | 5 +++-- zds/tutorialv2/utils.py | 8 ++++---- zds/utils/templatetags/times.py | 7 +++++++ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 zds/utils/templatetags/times.py diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index 26815e3a9a..b6de29b5b2 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -3,6 +3,7 @@ {% load thumbnail %} {% load emarkdown %} {% load i18n %} +{% load times %} {% block title %} @@ -163,14 +164,14 @@

    {% for element in containers_target %} {% endfor %} {% for element in containers_target %} {% endfor %} diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 0cf2387339..9d36fe6a64 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -187,9 +187,9 @@ def get_target_tagged_tree_for_extract(moveable_child, root): target_tagged_tree = [] for child in root.traverse(False): if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_level(), child != moveable_child)) + target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_depth(), child != moveable_child)) else: - target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), False)) return target_tagged_tree @@ -203,9 +203,9 @@ def get_target_tagged_tree_for_container(moveable_child, root): """ target_tagged_tree = [] for child in root.traverse(True): - composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + composed_depth = child.get_tree_depth() + moveable_child.get_tree_depth() enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), enabled and child != moveable_child and child != root)) return target_tagged_tree diff --git a/zds/utils/templatetags/times.py b/zds/utils/templatetags/times.py new file mode 100644 index 0000000000..555f6fab2b --- /dev/null +++ b/zds/utils/templatetags/times.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter(name='times') +def times(number): + return range(number) From dff4075732b3e608879d23b11cdff85307f01426 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 1 Apr 2015 17:15:30 -0400 Subject: [PATCH 085/887] Fini le travail - Corrige des incoherences - ajout une fonction `commit_changes()` a `VersionedContent` - Ajoute deux tests unitaires qui fixent le comportement et assurent l'acces a travers l'historique --- templates/tutorialv2/view/content.html | 2 +- zds/tutorialv2/models.py | 106 +++++++------ zds/tutorialv2/tests/tests_models.py | 126 ++++++++++++++++ zds/tutorialv2/tests/tests_views.py | 199 +++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 45 deletions(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index b1d67076b9..37f63b7dc2 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -153,7 +153,7 @@

    {% endif %} {% else %} - + {% trans "Version brouillon" %} {% endif %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index e9ffec39fc..2d46d57978 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -2,8 +2,7 @@ from math import ceil import shutil -from django.http import Http404 -from gitdb.exc import BadObject +from datetime import datetime try: import ujson as json_reader @@ -20,10 +19,13 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import models -from datetime import datetime +from django.http import Http404 +from gitdb.exc import BadObject +from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext as _ + from git.repo import Repo from git import Actor -from django.core.exceptions import PermissionDenied from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user @@ -350,13 +352,14 @@ def get_conclusion(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.conclusion) - def repo_update(self, title, introduction, conclusion, commit_message=''): + def repo_update(self, title, introduction, conclusion, commit_message='', do_commit=True): """ Update the container information and commit them into the repository :param title: the new title :param introduction: the new introduction text :param conclusion: the new conclusion text :param commit_message: commit message that will be used instead of the default one + :param do_commit: perform the commit in repository if `True` :return : commit sha """ @@ -367,6 +370,9 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): self.title = title if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed old_path = self.get_path(relative=True) + old_slug = self.slug + + # move things self.slug = self.parent.get_unique_slug(title) new_path = self.get_path(relative=True) repo.index.move([old_path, new_path]) @@ -374,6 +380,10 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): # update manifest self.update_children() + # update parent children dict: + self.parent.children_dict.pop(old_slug) + self.parent.children_dict[self.slug] = self + # update introduction and conclusion rel_path = self.get_path(relative=True) if self.introduction is None: @@ -393,19 +403,18 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): repo.index.add(['manifest.json', self.introduction, self.conclusion]) if commit_message == '': - commit_message = u'Mise à jour de « ' + self.title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) + commit_message = _(u'Mise à jour de « {} »').format(self.title) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) - def repo_add_container(self, title, introduction, conclusion, commit_message=''): + def repo_add_container(self, title, introduction, conclusion, commit_message='', do_commit=True): """ :param title: title of the new container :param introduction: text of its introduction :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` :return: commit sha """ subcontainer = Container(title) @@ -440,18 +449,17 @@ def repo_add_container(self, title, introduction, conclusion, commit_message='') repo.index.add(['manifest.json', subcontainer.introduction, subcontainer.conclusion]) if commit_message == '': - commit_message = u'Création du conteneur « ' + title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) + commit_message = _(u'Création du conteneur « {} »').format(title) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) - def repo_add_extract(self, title, text, commit_message=''): + def repo_add_extract(self, title, text, commit_message='', do_commit=True): """ :param title: title of the new extract :param text: text of the new extract :param commit_message: commit message that will be used instead of the default one + :param do_commit: perform the commit in repository if `True` :return: commit sha """ extract = Extract(title) @@ -476,12 +484,10 @@ def repo_add_extract(self, title, text, commit_message=''): repo.index.add(['manifest.json', extract.text]) if commit_message == '': - commit_message = u'Création de l\'extrait « ' + title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) - - self.top_container().sha_draft = cm.hexsha + commit_message = _(u'Création de l\'extrait « {} »').format(title) - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) def repo_delete(self, commit_message='', do_commit=True): """ @@ -506,13 +512,9 @@ def repo_delete(self, commit_message='', do_commit=True): if commit_message == '': commit_message = u'Suppression du conteneur « {} »'.format(self.title) - if do_commit: - cm = repo.index.commit(commit_message, **get_commit_author()) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha - return None + if do_commit: + return self.top_container().commit_changes(commit_message) def move_child_up(self, child_slug): """ @@ -605,8 +607,8 @@ def traverse(self, only_container=True): yield self for child in self.children: if isinstance(child, Container): - for _ in child.traverse(only_container): - yield _ + for _y in child.traverse(only_container): + yield _y elif not only_container: yield child @@ -717,7 +719,7 @@ def get_text(self): self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) - def repo_update(self, title, text, commit_message=''): + def repo_update(self, title, text, commit_message='', do_commit=True): """ :param title: new title of the extract :param text: new text of the extract @@ -730,12 +732,18 @@ def repo_update(self, title, text, commit_message=''): if title != self.title: # get a new slug old_path = self.get_path(relative=True) + old_slug = self.slug self.title = title self.slug = self.container.get_unique_slug(title) - new_path = self.get_path(relative=True) + # move file + new_path = self.get_path(relative=True) repo.index.move([old_path, new_path]) + # update parent children dict: + self.container.children_dict.pop(old_slug) + self.container.children_dict[self.slug] = self + # edit text path = self.container.top_container().get_path() @@ -751,11 +759,9 @@ def repo_update(self, title, text, commit_message=''): if commit_message == '': commit_message = u'Modification de l\'extrait « {} », situé dans le conteneur « {} »'\ .format(self.title, self.container.title) - cm = repo.index.commit(commit_message, **get_commit_author()) - - self.container.top_container().sha_draft = cm.hexsha - return cm.hexsha + if do_commit: + return self.container.top_container().commit_changes(commit_message) def repo_delete(self, commit_message='', do_commit=True): """ @@ -780,13 +786,9 @@ def repo_delete(self, commit_message='', do_commit=True): if commit_message == '': commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) - if do_commit: - cm = repo.index.commit(commit_message, **get_commit_author()) - self.container.top_container().sha_draft = cm.hexsha - - return cm.hexsha - return None + if do_commit: + return self.container.top_container().commit_changes(commit_message) class VersionedContent(Container): @@ -970,6 +972,15 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi return self.repo_update(title, introduction, conclusion, commit_message) + def commit_changes(self, commit_message): + """Commit change made to the repository""" + cm = self.repository.index.commit(commit_message, **get_commit_author()) + + self.sha_draft = cm.hexsha + self.current_version = cm.hexsha + + return cm.hexsha + def change_child_directory(self, child, adoptive_parent): old_path = child.get_path(False) # absolute path because we want to access the address @@ -1133,8 +1144,15 @@ def get_commit_author(): :return: correctly formatted commit author for `repo.index.commit()` """ user = get_current_user() - aut_user = str(user.pk) - aut_email = str(user.email) + + if user: + aut_user = str(user.pk) + aut_email = str(user.email) + + else: + aut_user = ZDS_APP['member']['bot_account'] + aut_email = None + if aut_email is None or aut_email.strip() == "": aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) return {'author': Actor(aut_user, aut_email), 'committer': Actor(aut_user, aut_email)} diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index fe13d94dc2..7fc45d5e7b 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -88,6 +88,132 @@ def test_ensure_unique_slug(self): new_extract_3 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) self.assertNotEqual(new_extract_3.slug, new_extract_1.slug) # same parent, forbidden + def test_workflow_repository(self): + """ + Test to ensure the behavior of repo_*() functions : + - if they change the filesystem as they are suppose to ; + - if they change the `self.sha_*` as they are suppose to. + """ + + new_title = u'Un nouveau titre' + other_new_title = u'Un titre différent' + random_text = u'J\'ai faim!' + other_random_text = u'Oh, du chocolat <3' + + versioned = self.tuto.load_version() + current_version = versioned.current_version + slug_repository = versioned.slug_repository + + # VersionedContent: + old_path = versioned.get_path() + self.assertTrue(os.path.isdir(old_path)) + new_slug = versioned.get_unique_slug(new_title) # normally, you get a new slug by asking database ! + + versioned.repo_update_top_container(new_title, new_slug, random_text, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = versioned.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isdir(new_path)) + self.assertFalse(os.path.isdir(old_path)) + + self.assertNotEqual(slug_repository, versioned.slug_repository) # if this test fail, you're in trouble + + # Container: + + # 1. add new part: + versioned.repo_add_container(new_title, random_text, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + part = versioned.children[-1] + old_path = part.get_path() + self.assertTrue(os.path.isdir(old_path)) + self.assertTrue(os.path.exists(os.path.join(versioned.get_path(), part.introduction))) + self.assertTrue(os.path.exists(os.path.join(versioned.get_path(), part.conclusion))) + self.assertEqual(part.get_introduction(), random_text) + self.assertEqual(part.get_conclusion(), random_text) + + # 2. update the part + part.repo_update(other_new_title, other_random_text, other_random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = part.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isdir(new_path)) + self.assertFalse(os.path.isdir(old_path)) + + self.assertEqual(part.get_introduction(), other_random_text) + self.assertEqual(part.get_conclusion(), other_random_text) + + # 3. delete it + part.repo_delete() # boom ! + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + self.assertFalse(os.path.isdir(new_path)) + + # Extract : + + # 1. add new extract + versioned.repo_add_container(new_title, random_text, random_text) # need to add a new part before + part = versioned.children[-1] + + part.repo_add_extract(new_title, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + extract = part.children[-1] + old_path = extract.get_path() + self.assertTrue(os.path.isfile(old_path)) + self.assertEqual(extract.get_text(), random_text) + + # 2. update extract + extract.repo_update(other_new_title, other_random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = extract.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isfile(new_path)) + self.assertFalse(os.path.isfile(old_path)) + + self.assertEqual(extract.get_text(), other_random_text) + + # 3. update parent and see if it still works: + part.repo_update(other_new_title, other_random_text, other_random_text) + + old_path = new_path + new_path = extract.get_path() + + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isfile(new_path)) + self.assertFalse(os.path.isfile(old_path)) + + self.assertEqual(extract.get_text(), other_random_text) + + # 4. Boom, no more extract + extract.repo_delete() + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + + self.assertFalse(os.path.exists(new_path)) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 1e0c5cfbc9..365dc87303 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -1077,6 +1077,205 @@ def test_move_container_after(self): chapter = versioned.children_dict[self.part2.slug].children[0] self.assertEqual(self.chapter4.slug, chapter.slug) + def test_history_navigation(self): + """ensure that, if the title (and so the slug) of the content change, its content remain accessible""" + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': self.part1.slug + }), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }), + follow=False) + self.assertEqual(result.status_code, 200) + + # edit tutorial: + old_slug_tuto = tuto.slug + version_1 = tuto.sha_draft # "version 1" is the one before any change + + new_licence = LicenceFactory() + random = 'Pâques, c\'est bientôt?' + + result = self.client.post( + reverse('content:edit', args=[tuto.pk, tuto.slug]), + { + 'title': random, + 'description': random, + 'introduction': random, + 'conclusion': random, + 'type': u'TUTORIAL', + 'licence': new_licence.pk, + 'subcategory': self.subcategory.pk, + }, + follow=False) + self.assertEqual(result.status_code, 302) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + version_2 = tuto.sha_draft # "version 2" is the one with the different slug for the tutorial + self.assertNotEqual(tuto.slug, old_slug_tuto) + + # check access using old slug and no version + result = self.client.get( + reverse('content:view', args=[tuto.pk, old_slug_tuto]), + follow=False) + self.assertEqual(result.status_code, 404) # it is not possible, so get 404 + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': self.part1.slug + }), + follow=False) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }), + follow=False) + self.assertEqual(result.status_code, 404) + + # check access with old slug and version + result = self.client.get( + reverse('content:view', args=[tuto.pk, old_slug_tuto]) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': self.part1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + # edit container: + old_slug_part = self.part1.slug + result = self.client.post( + reverse('content:edit-container', kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': self.part1.slug + }), + { + 'title': random, + 'introduction': random, + 'conclusion': random + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # we can still access to the container using old slug ! + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + # and even to it using version 1 and old tuto slug !! + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + # but you can also access it with the current slug (for retro-compatibility) + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) From 3cbf1dff609b490bf1d43a6674b2f192828ccd75 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 1 Apr 2015 22:28:49 -0400 Subject: [PATCH 086/887] Ajoute un morceau de test sur le cas de la suppression --- zds/tutorialv2/tests/tests_views.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 365dc87303..8f32c94459 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -264,6 +264,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(versioned.get_conclusion(), random) self.assertEqual(versioned.description, random) self.assertEqual(versioned.licence.pk, new_licence.pk) + self.assertNotEqual(versioned.slug, slug) slug = tuto.slug # make the title change also change the slug !! @@ -292,6 +293,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit container: + old_slug_container = container.slug result = self.client.post( reverse('content:edit-container', kwargs={'pk': pk, 'slug': slug, 'container_slug': container.slug}), { @@ -307,6 +309,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(container.title, random) self.assertEqual(container.get_introduction(), random) self.assertEqual(container.get_conclusion(), random) + self.assertNotEqual(container.slug, old_slug_container) # add a subcontainer result = self.client.post( @@ -339,6 +342,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit subcontainer: + old_slug_subcontainer = subcontainer.slug result = self.client.post( reverse('content:edit-container', kwargs={ @@ -360,6 +364,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(subcontainer.title, random) self.assertEqual(subcontainer.get_introduction(), random) self.assertEqual(subcontainer.get_conclusion(), random) + self.assertNotEqual(subcontainer.slug, old_slug_subcontainer) # add extract to subcontainer: result = self.client.post( @@ -396,6 +401,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit extract: + old_slug_extract = extract.slug result = self.client.post( reverse('content:edit-extract', kwargs={ @@ -416,6 +422,7 @@ def test_basic_tutorial_workflow(self): extract = versioned.children[0].children[0].children[0] self.assertEqual(extract.title, random) self.assertEqual(extract.get_text(), random) + self.assertNotEqual(old_slug_extract, extract.slug) # then, delete extract: result = self.client.get( @@ -1210,6 +1217,11 @@ def test_history_navigation(self): follow=False) self.assertEqual(result.status_code, 302) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + version_3 = tuto.sha_draft # "version 3" is the one with the modified part + versioned = tuto.load_version() + current_slug_part = versioned.children[0].slug + # we can still access to the container using old slug ! result = self.client.get( reverse('content:view-container', @@ -1276,6 +1288,83 @@ def test_history_navigation(self): follow=False) self.assertEqual(result.status_code, 200) + # delete part + result = self.client.post( + reverse('content:delete', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'object_slug': current_slug_part + }), + follow=False) + self.assertEqual(result.status_code, 302) + + # we can still access to the part in version 3: + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': current_slug_part + }) + '?version=' + version_3, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': current_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_3, + follow=False) + + # version 2: + self.assertEqual(result.status_code, 200) + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + # version 1: + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) From bbd77fc9419346b5166abaf0aefe3b34e6938056 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Thu, 2 Apr 2015 14:08:11 +0200 Subject: [PATCH 087/887] =?UTF-8?q?int=C3=A9gration=20compl=C3=A8te=20du?= =?UTF-8?q?=20d=C3=A9placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/includes/child.part.html | 17 ++++++++++++++++- zds/tutorialv2/models.py | 7 ++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html index a30c23b4b1..35154463a7 100644 --- a/templates/tutorialv2/includes/child.part.html +++ b/templates/tutorialv2/includes/child.part.html @@ -1,6 +1,7 @@ {% load emarkdown %} {% load i18n %} - +{% load times %} +{% load target_tree %}

    {% if child.position_in_parent < child.parent.children|length %} {% endif %} + + {% for element in child|target_tree %} + + {% endfor %} + + {% for element in child|target_tree %} + + {% endfor %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 2d46d57978..7c4a51d0ae 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -982,7 +982,12 @@ def commit_changes(self, commit_message): return cm.hexsha def change_child_directory(self, child, adoptive_parent): - + """ + Move an element of this content to a new location. + This method changes the repository index and stage every change but does **not** commit. + :param child: the child we want to move, can be either an Extract or a Container object + :param adoptive_parent: the container where the child *will be* moved, must be a Container object + """ old_path = child.get_path(False) # absolute path because we want to access the address if isinstance(child, Extract): old_parent = child.container From 3cdaee1ace9f46d4b7f1c33095ef93e26b960400 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Fri, 3 Apr 2015 09:00:04 +0200 Subject: [PATCH 088/887] ajout du filtre target_tree --- zds/utils/templatetags/target_tree.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 zds/utils/templatetags/target_tree.py diff --git a/zds/utils/templatetags/target_tree.py b/zds/utils/templatetags/target_tree.py new file mode 100644 index 0000000000..873500838a --- /dev/null +++ b/zds/utils/templatetags/target_tree.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +from zds.tutorialv2.utils import get_target_tagged_tree +from zds.tutorialv2.models import Extract, Container +from django import template + + +register = template.Library() +@register.filter('target_tree') +def target_tree(child): + """ + A django filter that wrap zds.tutorialv2.utils.get_target_tagged_tree function + """ + if isinstance(child, Container): + root = child.top_container() + elif isinstance(child, Extract): + root = child.container.top_container() + return get_target_tagged_tree(child, root) From f38cca45dd288debaa1d5a5abd66f287813d49fd Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 3 Apr 2015 12:30:36 -0400 Subject: [PATCH 089/887] Assure le comportement si intro et conclu sont `None` --- zds/tutorialv2/factories.py | 88 +++------------- zds/tutorialv2/models.py | 145 +++++++++++--------------- zds/tutorialv2/tests/tests_models.py | 35 +++++++ zds/tutorialv2/tests/tests_views.py | 88 ++++++++++++++++ zds/utils/templatetags/target_tree.py | 3 + 5 files changed, 202 insertions(+), 157 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 532cefab79..2848f97737 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -1,13 +1,10 @@ # coding: utf-8 from datetime import datetime -from git.repo import Repo -import os import factory -from zds.tutorialv2.models import PublishableContent, Validation, VersionedContent, ContentReaction, Container, Extract -from zds.utils import slugify +from zds.tutorialv2.models import PublishableContent, Validation, ContentReaction, Container, Extract, init_new_repo from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory @@ -25,37 +22,13 @@ class PublishableContentFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) - path = publishable_content.get_repo_path() - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - Repo.init(path, bare=False) - - introduction = 'introduction.md' - conclusion = 'conclusion.md' - versioned_content = VersionedContent(None, - publishable_content.type, - publishable_content.title, - slugify(publishable_content.title)) - versioned_content.introduction = introduction - versioned_content.conclusion = conclusion - repo = Repo(path) - - versioned_content.dump_json() - f = open(os.path.join(path, introduction), "w") - f.write(text_content.encode('utf-8')) - f.close() - f = open(os.path.join(path, conclusion), "w") - f.write(text_content.encode('utf-8')) - f.close() - repo.index.add(['manifest.json', introduction, conclusion]) - cm = repo.index.commit("Init Tuto") - - publishable_content.sha_draft = cm.hexsha - publishable_content.sha_beta = None + publishable_content.gallery = GalleryFactory() for author in publishable_content.authors.all(): UserGalleryFactory(user=author, gallery=publishable_content.gallery) + + init_new_repo(publishable_content, text_content, text_content) + return publishable_content @@ -63,39 +36,17 @@ class ContainerFactory(factory.Factory): FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n + 1)) - slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) - container = super(ContainerFactory, cls)._prepare(create, **kwargs) - container.parent.add_container(container, generate_slug=True) - - path = container.get_path() - repo = Repo(container.top_container().get_path()) - top_container = container.top_container() - - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - container.introduction = os.path.join(container.get_path(relative=True), 'introduction.md') - container.conclusion = os.path.join(container.get_path(relative=True), 'conclusion.md') + parent = kwargs.pop('parent', None) - f = open(os.path.join(top_container.get_path(), container.introduction), "w") - f.write(text_content.encode('utf-8')) - f.close() - f = open(os.path.join(top_container.get_path(), container.conclusion), "w") - f.write(text_content.encode('utf-8')) - f.close() - repo.index.add([container.introduction, container.conclusion]) - - top_container.dump_json() - repo.index.add(['manifest.json']) - - cm = repo.index.commit("Add container") + sha = parent.repo_add_container(kwargs['title'], text_content, text_content) + container = parent.children[-1] if db_object: - db_object.sha_draft = cm.hexsha + db_object.sha_draft = sha db_object.save() return container @@ -104,30 +55,17 @@ def _prepare(cls, create, **kwargs): class ExtractFactory(factory.Factory): FACTORY_FOR = Extract title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n + 1)) - slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) - extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - extract.container.add_extract(extract, generate_slug=True) - - extract.text = extract.get_path(relative=True) - top_container = extract.container.top_container() - repo = Repo(top_container.get_path()) - f = open(extract.get_path(), 'w') - f.write(text_content.encode('utf-8')) - f.close() - - repo.index.add([extract.text]) - - top_container.dump_json() - repo.index.add(['manifest.json']) + parent = kwargs.pop('container', None) - cm = repo.index.commit("Add extract") + sha = parent.repo_add_extract(kwargs['title'], text_content) + extract = parent.children[-1] if db_object: - db_object.sha_draft = cm.hexsha + db_object.sha_draft = sha db_object.save() return extract diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 7c4a51d0ae..f65852def2 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -348,7 +348,7 @@ def get_conclusion(self): """ :return: the conclusion from the file in `self.conclusion` """ - if self.introduction: + if self.conclusion: return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.conclusion) @@ -363,6 +363,9 @@ def repo_update(self, title, introduction, conclusion, commit_message='', do_com :return : commit sha """ + if title is None: + raise PermissionDenied + repo = self.top_container().repository # update title @@ -384,23 +387,40 @@ 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 + # update introduction and conclusion (if any) + path = self.top_container().get_path() rel_path = self.get_path(relative=True) - if self.introduction is None: - self.introduction = os.path.join(rel_path, 'introduction.md') - if self.conclusion is None: - self.conclusion = os.path.join(rel_path, 'conclusion.md') - path = self.top_container().get_path() - f = open(os.path.join(path, self.introduction), "w") - f.write(introduction.encode('utf-8')) - f.close() - f = open(os.path.join(path, self.conclusion), "w") - f.write(conclusion.encode('utf-8')) - f.close() + if introduction is not None: + if self.introduction is None: + self.introduction = os.path.join(rel_path, 'introduction.md') + + f = open(os.path.join(path, self.introduction), "w") + f.write(introduction.encode('utf-8')) + 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 = open(os.path.join(path, self.conclusion), "w") + f.write(conclusion.encode('utf-8')) + 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 self.top_container().dump_json() - repo.index.add(['manifest.json', self.introduction, self.conclusion]) + repo.index.add(['manifest.json']) if commit_message == '': commit_message = _(u'Mise à jour de « {} »').format(self.title) @@ -433,26 +453,12 @@ def repo_add_container(self, title, introduction, conclusion, commit_message='', repo.index.add([rel_path]) - # create introduction and conclusion - subcontainer.introduction = os.path.join(rel_path, 'introduction.md') - subcontainer.conclusion = os.path.join(rel_path, 'conclusion.md') - - f = open(os.path.join(path, subcontainer.introduction), "w") - f.write(introduction.encode('utf-8')) - f.close() - f = open(os.path.join(path, subcontainer.conclusion), "w") - f.write(conclusion.encode('utf-8')) - f.close() - - # commit - self.top_container().dump_json() - repo.index.add(['manifest.json', subcontainer.introduction, subcontainer.conclusion]) - + # make it if commit_message == '': commit_message = _(u'Création du conteneur « {} »').format(title) - if do_commit: - return self.top_container().commit_changes(commit_message) + return subcontainer.repo_update( + title, introduction, conclusion, commit_message=commit_message, do_commit=do_commit) def repo_add_extract(self, title, text, commit_message='', do_commit=True): """ @@ -470,24 +476,11 @@ def repo_add_extract(self, title, text, commit_message='', do_commit=True): except Exception: raise PermissionDenied - # create text - repo = self.top_container().repository - path = self.top_container().get_path() - - extract.text = extract.get_path(relative=True) - f = open(os.path.join(path, extract.text), "w") - f.write(text.encode('utf-8')) - f.close() - - # commit - self.top_container().dump_json() - repo.index.add(['manifest.json', extract.text]) - + # make it if commit_message == '': commit_message = _(u'Création de l\'extrait « {} »').format(title) - if do_commit: - return self.top_container().commit_changes(commit_message) + return extract.repo_update(title, text, commit_message=commit_message, do_commit=do_commit) def repo_delete(self, commit_message='', do_commit=True): """ @@ -511,7 +504,7 @@ def repo_delete(self, commit_message='', do_commit=True): repo.index.add(['manifest.json']) if commit_message == '': - commit_message = u'Suppression du conteneur « {} »'.format(self.title) + commit_message = _(u'Suppression du conteneur « {} »').format(self.title) if do_commit: return self.top_container().commit_changes(commit_message) @@ -727,6 +720,12 @@ def repo_update(self, title, text, commit_message='', do_commit=True): :return: commit sha """ + if title is None: + raise PermissionDenied + + if text is None: + raise PermissionDenied + repo = self.container.top_container().repository if title != self.title: @@ -752,12 +751,12 @@ def repo_update(self, title, text, commit_message='', do_commit=True): f.write(text.encode('utf-8')) f.close() - # commit + # make it self.container.top_container().dump_json() repo.index.add(['manifest.json', self.text]) if commit_message == '': - commit_message = u'Modification de l\'extrait « {} », situé dans le conteneur « {} »'\ + commit_message = _(u'Modification de l\'extrait « {} », situé dans le conteneur « {} »')\ .format(self.title, self.container.title) if do_commit: @@ -785,7 +784,7 @@ def repo_delete(self, commit_message='', do_commit=True): repo.index.add(['manifest.json']) if commit_message == '': - commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) + commit_message = _(u'Suppression de l\'extrait « {} »').format(self.title) if do_commit: return self.container.top_container().commit_changes(commit_message) @@ -1079,7 +1078,7 @@ def fill_containers_from_json(json_sub, parent): raise Exception('Unknown object type' + child['object']) -def init_new_repo(db_object, introduction_text, conclusion_text, commit_message=''): +def init_new_repo(db_object, introduction_text, conclusion_text, commit_message='', do_commit=True): """ Create a new repository in `settings.ZDS_APP['contents']['private_repo']` to store the files for a new content. Note that `db_object.sha_draft` will be set to the good value @@ -1087,6 +1086,7 @@ def init_new_repo(db_object, introduction_text, conclusion_text, commit_message= :param introduction_text: introduction from form :param conclusion_text: conclusion from form :param commit_message : set a commit message instead of the default one + :param do_commit: do commit if `True` :return: `VersionedContent` object """ # TODO: should be a static function of an object (I don't know which one yet) @@ -1098,48 +1098,29 @@ def init_new_repo(db_object, introduction_text, conclusion_text, commit_message= # init repo: Repo.init(path, bare=False) - repo = Repo(path) - introduction = 'introduction.md' - conclusion = 'conclusion.md' - versioned_content = VersionedContent(None, - db_object.type, - db_object.title, - db_object.slug, - db_object.slug) + # create object + versioned_content = VersionedContent(None, db_object.type, db_object.title, db_object.slug) # fill some information that are missing : versioned_content.licence = db_object.licence versioned_content.description = db_object.description - versioned_content.introduction = introduction - versioned_content.conclusion = conclusion - - # fill intro/conclusion: - f = open(os.path.join(path, introduction), "w") - f.write(introduction_text.encode('utf-8')) - f.close() - f = open(os.path.join(path, conclusion), "w") - f.write(conclusion_text.encode('utf-8')) - f.close() - - versioned_content.dump_json() - # commit change: + # perform changes: if commit_message == '': commit_message = u'Création du contenu' - repo.index.add(['manifest.json', introduction, conclusion]) - cm = repo.index.commit(commit_message, **get_commit_author()) - # update sha: - db_object.sha_draft = cm.hexsha - db_object.sha_beta = None - db_object.sha_public = None - db_object.sha_validation = None + sha = versioned_content.repo_update( + db_object.title, introduction_text, conclusion_text, commit_message=commit_message, do_commit=do_commit) - db_object.save() + # update sha: + if do_commit: + db_object.sha_draft = sha + db_object.sha_beta = None + db_object.sha_public = None + db_object.sha_validation = None - versioned_content.current_version = cm.hexsha - versioned_content.repository = repo + db_object.save() return versioned_content diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 7fc45d5e7b..883e9e31e4 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -214,6 +214,41 @@ def test_workflow_repository(self): self.assertFalse(os.path.exists(new_path)) + def test_if_none(self): + """Test the case where introduction and conclusion are `None`""" + + given_title = u'La vie secrète de Clem\'' + some_text = u'Tous ces secrets (ou pas)' + versioned = self.tuto.load_version() + versioned.repo_add_container(given_title, None, None) # add a new part with `None` for intro and conclusion + + new_part = versioned.children[-1] + self.assertIsNone(new_part.introduction) + self.assertIsNone(new_part.conclusion) + + new_part.repo_update(given_title, None, None) # still `None` + self.assertIsNone(new_part.introduction) + self.assertIsNone(new_part.conclusion) + + new_part.repo_update(given_title, some_text, some_text) + self.assertIsNotNone(new_part.introduction) # now, value given + self.assertIsNotNone(new_part.conclusion) + + old_intro = new_part.introduction + old_conclu = new_part.conclusion + self.assertTrue(os.path.isfile(os.path.join(versioned.get_path(), old_intro))) + self.assertTrue(os.path.isfile(os.path.join(versioned.get_path(), old_conclu))) + + new_part.repo_update(given_title, None, None) # and we go back to `None` + self.assertIsNone(new_part.introduction) + self.assertIsNone(new_part.conclusion) + self.assertFalse(os.path.isfile(os.path.join(versioned.get_path(), old_intro))) # introduction is deleted + self.assertFalse(os.path.isfile(os.path.join(versioned.get_path(), old_conclu))) + + new_part.repo_update(given_title, '', '') # empty string != `None` + self.assertIsNotNone(new_part.introduction) + self.assertIsNotNone(new_part.conclusion) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 8f32c94459..20c2a22745 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -1365,6 +1365,94 @@ def test_history_navigation(self): follow=False) self.assertEqual(result.status_code, 200) + def test_if_none(self): + """ensure that everything is ok if `None` is set""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + given_title = u'Un titre que personne ne lira' + some_text = u'Tralalala !!' + + # let's cheat a little bit and use the "manual way" to force `None` + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + versioned = tuto.load_version() + sha = versioned.repo_add_container(given_title, None, None) + slug_new_container = versioned.children[-1].slug + tuto.sha_draft = sha + tuto.save() + + # test access + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': slug_new_container + }), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:edit-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': slug_new_container + }), + follow=False) + self.assertEqual(result.status_code, 200) # access to edition page is ok + + # edit container: + result = self.client.post( + reverse('content:edit-container', kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': slug_new_container + }), + { + 'title': given_title, + 'introduction': some_text, + 'conclusion': some_text + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # test access + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': slug_new_container + }), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:edit-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': slug_new_container + }), + follow=False) + self.assertEqual(result.status_code, 200) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/utils/templatetags/target_tree.py b/zds/utils/templatetags/target_tree.py index 873500838a..fe572a5ca2 100644 --- a/zds/utils/templatetags/target_tree.py +++ b/zds/utils/templatetags/target_tree.py @@ -6,11 +6,14 @@ register = template.Library() + + @register.filter('target_tree') def target_tree(child): """ A django filter that wrap zds.tutorialv2.utils.get_target_tagged_tree function """ + root = None if isinstance(child, Container): root = child.top_container() elif isinstance(child, Extract): From eb4b8c9582c716364b697a9b0d6517f638f10752 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 20 Dec 2014 12:38:54 +0100 Subject: [PATCH 090/887] =?UTF-8?q?Initialise=20le=20module=20parall=C3=A8?= =?UTF-8?q?le=20pour=20la=20ZEP=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/__init__.py | 1 + zds/tutorialv2/admin.py | 13 + zds/tutorialv2/factories.py | 358 +++ zds/tutorialv2/feeds.py | 44 + zds/tutorialv2/forms.py | 627 +++++ zds/tutorialv2/models.py | 753 ++++++ zds/tutorialv2/search_indexes.py | 68 + zds/tutorialv2/tests/tests_views.py | 0 zds/tutorialv2/urls.py | 123 + zds/tutorialv2/utils.py | 52 + zds/tutorialv2/views.py | 3643 +++++++++++++++++++++++++++ 11 files changed, 5682 insertions(+) create mode 100644 zds/tutorialv2/__init__.py create mode 100644 zds/tutorialv2/admin.py create mode 100644 zds/tutorialv2/factories.py create mode 100644 zds/tutorialv2/feeds.py create mode 100644 zds/tutorialv2/forms.py create mode 100644 zds/tutorialv2/models.py create mode 100644 zds/tutorialv2/search_indexes.py create mode 100644 zds/tutorialv2/tests/tests_views.py create mode 100644 zds/tutorialv2/urls.py create mode 100644 zds/tutorialv2/utils.py create mode 100644 zds/tutorialv2/views.py diff --git a/zds/tutorialv2/__init__.py b/zds/tutorialv2/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/zds/tutorialv2/__init__.py @@ -0,0 +1 @@ + diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py new file mode 100644 index 0000000000..0907547f20 --- /dev/null +++ b/zds/tutorialv2/admin.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +from django.contrib import admin + +from .models import Tutorial, Part, Chapter, Extract, Validation, Note + + +admin.site.register(Tutorial) +admin.site.register(Part) +admin.site.register(Chapter) +admin.site.register(Extract) +admin.site.register(Validation) +admin.site.register(Note) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py new file mode 100644 index 0000000000..dd9147ee72 --- /dev/null +++ b/zds/tutorialv2/factories.py @@ -0,0 +1,358 @@ +# coding: utf-8 + +from datetime import datetime +from git.repo import Repo +import json as json_writer +import os + +import factory + +from zds.tutorial.models import Tutorial, Part, Chapter, Extract, Note,\ + Validation +from zds.utils.models import SubCategory, Licence +from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.utils.tutorials import export_tutorial +from zds.tutorial.views import mep + +content = ( + u'Ceci est un contenu de tutoriel utile et à tester un peu partout\n\n ' + u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits \n\n ' + u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc \n\n ' + u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)\n\n ' + u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)\n\n ' + u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)\n\n ' + u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)\n\n ' + u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2' + u'F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)\n\n ' + u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)\n\n ' + u'\n Attention les tests ne doivent pas crasher \n\n \n\n \n\n ' + u'qu\'un sujet abandonné !\n\n ') + +content_light = u'Un contenu light pour quand ce n\'est pas vraiment ça qui est testé' + + +class BigTutorialFactory(factory.DjangoModelFactory): + FACTORY_FOR = Tutorial + + title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) + description = factory.Sequence( + lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'BIG' + create_at = datetime.now() + introduction = 'introduction.md' + conclusion = 'conclusion.md' + + @classmethod + def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) + tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) + path = tuto.get_path() + real_content = content + if light: + real_content = content_light + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + man = export_tutorial(tuto) + repo = Repo.init(path, bare=False) + repo = Repo(path) + + f = open(os.path.join(path, 'manifest.json'), "w") + f.write(json_writer.dumps(man, indent=4, ensure_ascii=False).encode('utf-8')) + f.close() + f = open(os.path.join(path, tuto.introduction), "w") + f.write(real_content.encode('utf-8')) + f.close() + f = open(os.path.join(path, tuto.conclusion), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + cm = repo.index.commit("Init Tuto") + + tuto.sha_draft = cm.hexsha + tuto.sha_beta = None + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) + return tuto + + +class MiniTutorialFactory(factory.DjangoModelFactory): + FACTORY_FOR = Tutorial + + title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) + description = factory.Sequence( + lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'MINI' + create_at = datetime.now() + introduction = 'introduction.md' + conclusion = 'conclusion.md' + + @classmethod + def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) + tuto = super(MiniTutorialFactory, cls)._prepare(create, **kwargs) + real_content = content + + if light: + real_content = content_light + path = tuto.get_path() + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + man = export_tutorial(tuto) + repo = Repo.init(path, bare=False) + repo = Repo(path) + + file = open(os.path.join(path, 'manifest.json'), "w") + file.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + file.close() + file = open(os.path.join(path, tuto.introduction), "w") + file.write(real_content.encode('utf-8')) + file.close() + file = open(os.path.join(path, tuto.conclusion), "w") + file.write(real_content.encode('utf-8')) + file.close() + + repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + cm = repo.index.commit("Init Tuto") + + tuto.sha_draft = cm.hexsha + tuto.gallery = GalleryFactory() + for author in tuto.authors.all(): + UserGalleryFactory(user=author, gallery=tuto.gallery) + return tuto + + +class PartFactory(factory.DjangoModelFactory): + FACTORY_FOR = Part + + title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + light = kwargs.pop('light', False) + part = super(PartFactory, cls)._prepare(create, **kwargs) + tutorial = kwargs.pop('tutorial', None) + + real_content = content + if light: + real_content = content_light + + path = part.get_path() + repo = Repo(part.tutorial.get_path()) + + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + part.introduction = os.path.join(part.get_phy_slug(), 'introduction.md') + part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') + part.save() + + f = open(os.path.join(tutorial.get_path(), part.introduction), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add([part.introduction]) + f = open(os.path.join(tutorial.get_path(), part.conclusion), "w") + f.write(real_content.encode('utf-8')) + f.close() + repo.index.add([part.conclusion]) + + if tutorial: + tutorial.save() + + man = export_tutorial(tutorial) + f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + + repo.index.add(['manifest.json']) + + cm = repo.index.commit("Init Part") + + if tutorial: + tutorial.sha_draft = cm.hexsha + tutorial.save() + + return part + + +class ChapterFactory(factory.DjangoModelFactory): + FACTORY_FOR = Chapter + + title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + + light = kwargs.pop('light', False) + chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) + tutorial = kwargs.pop('tutorial', None) + part = kwargs.pop('part', None) + + real_content = content + if light: + real_content = content_light + path = chapter.get_path() + + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + if tutorial: + chapter.introduction = '' + chapter.conclusion = '' + tutorial.save() + repo = Repo(tutorial.get_path()) + + man = export_tutorial(tutorial) + f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + repo.index.add(['manifest.json']) + + elif part: + chapter.introduction = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + 'introduction.md') + chapter.conclusion = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + 'conclusion.md') + chapter.save() + f = open( + os.path.join( + part.tutorial.get_path(), + chapter.introduction), + "w") + f.write(real_content.encode('utf-8')) + f.close() + f = open( + os.path.join( + part.tutorial.get_path(), + chapter.conclusion), + "w") + f.write(real_content.encode('utf-8')) + f.close() + part.tutorial.save() + repo = Repo(part.tutorial.get_path()) + + man = export_tutorial(part.tutorial) + f = open( + os.path.join( + part.tutorial.get_path(), + 'manifest.json'), + "w") + f.write( + json_writer.dumps( + man, + indent=4, + ensure_ascii=False).encode('utf-8')) + f.close() + + repo.index.add([chapter.introduction, chapter.conclusion]) + repo.index.add(['manifest.json']) + + cm = repo.index.commit("Init Chapter") + + if tutorial: + tutorial.sha_draft = cm.hexsha + tutorial.save() + chapter.tutorial = tutorial + elif part: + part.tutorial.sha_draft = cm.hexsha + part.tutorial.save() + part.save() + chapter.part = part + + return chapter + + +class ExtractFactory(factory.DjangoModelFactory): + FACTORY_FOR = Extract + + title = factory.Sequence(lambda n: 'Mon Extrait No{0}'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + extract = super(ExtractFactory, cls)._prepare(create, **kwargs) + chapter = kwargs.pop('chapter', None) + if chapter: + if chapter.tutorial: + chapter.tutorial.sha_draft = 'EXTRACT-AAAA' + chapter.tutorial.save() + elif chapter.part: + chapter.part.tutorial.sha_draft = 'EXTRACT-AAAA' + chapter.part.tutorial.save() + + return extract + + +class NoteFactory(factory.DjangoModelFactory): + FACTORY_FOR = Note + + ip_address = '192.168.3.1' + text = 'Bonjour, je me présente, je m\'appelle l\'homme au texte bidonné' + + @classmethod + def _prepare(cls, create, **kwargs): + note = super(NoteFactory, cls)._prepare(create, **kwargs) + note.pubdate = datetime.now() + note.save() + tutorial = kwargs.pop('tutorial', None) + if tutorial: + tutorial.last_note = note + tutorial.save() + return note + + +class SubCategoryFactory(factory.DjangoModelFactory): + FACTORY_FOR = SubCategory + + title = factory.Sequence(lambda n: 'Sous-Categorie {0} pour Tuto'.format(n)) + subtitle = factory.Sequence(lambda n: 'Sous titre de Sous-Categorie {0} pour Tuto'.format(n)) + slug = factory.Sequence(lambda n: 'sous-categorie-{0}'.format(n)) + + +class ValidationFactory(factory.DjangoModelFactory): + FACTORY_FOR = Validation + + +class LicenceFactory(factory.DjangoModelFactory): + FACTORY_FOR = Licence + + code = u'Licence bidon' + title = u'Licence bidon' + + @classmethod + def _prepare(cls, create, **kwargs): + licence = super(LicenceFactory, cls)._prepare(create, **kwargs) + return licence + + +class PublishedMiniTutorial(MiniTutorialFactory): + FACTORY_FOR = Tutorial + + @classmethod + def _prepare(cls, create, **kwargs): + tutorial = super(PublishedMiniTutorial, cls)._prepare(create, **kwargs) + tutorial.pubdate = datetime.now() + tutorial.sha_public = tutorial.sha_draft + tutorial.source = '' + tutorial.sha_validation = None + mep(tutorial, tutorial.sha_draft) + tutorial.save() + return tutorial diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py new file mode 100644 index 0000000000..f2b6a6174e --- /dev/null +++ b/zds/tutorialv2/feeds.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +from django.contrib.syndication.views import Feed +from django.conf import settings + +from django.utils.feedgenerator import Atom1Feed + +from .models import Tutorial + + +class LastTutorialsFeedRSS(Feed): + title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) + link = "/tutoriels/" + description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + def items(self): + return Tutorial.objects\ + .filter(sha_public__isnull=False)\ + .order_by('-pubdate')[:5] + + def item_title(self, item): + return item.title + + def item_pubdate(self, item): + return item.pubdate + + def item_description(self, item): + return item.description + + def item_author_name(self, item): + authors_list = item.authors.all() + authors = [] + for authors_obj in authors_list: + authors.append(authors_obj.username) + authors = ", ".join(authors) + return authors + + def item_link(self, item): + return item.get_absolute_url_online() + + +class LastTutorialsFeedATOM(LastTutorialsFeedRSS): + feed_type = Atom1Feed + subtitle = LastTutorialsFeedRSS.description diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py new file mode 100644 index 0000000000..e2001098e8 --- /dev/null +++ b/zds/tutorialv2/forms.py @@ -0,0 +1,627 @@ +# coding: utf-8 +from django import forms +from django.conf import settings + +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Layout, Fieldset, Submit, Field, \ + ButtonHolder, Hidden +from django.core.urlresolvers import reverse + +from zds.utils.forms import CommonLayoutModalText, CommonLayoutEditor, CommonLayoutVersionEditor +from zds.utils.models import SubCategory, Licence +from zds.tutorial.models import Tutorial, TYPE_CHOICES, HelpWriting +from django.utils.translation import ugettext_lazy as _ + + +class FormWithTitle(forms.Form): + title = forms.CharField( + label=_(u'Titre'), + max_length=Tutorial._meta.get_field('title').max_length, + widget=forms.TextInput( + attrs={ + 'required': 'required', + } + ) + ) + + def clean(self): + cleaned_data = super(FormWithTitle, self).clean() + + title = cleaned_data.get('title') + + if title is not None and title.strip() == '': + self._errors['title'] = self.error_class( + [_(u'Le champ Titre ne peut être vide')]) + if 'title' in cleaned_data: + del cleaned_data['title'] + + return cleaned_data + + +class TutorialForm(FormWithTitle): + + description = forms.CharField( + label=_(u'Description'), + max_length=Tutorial._meta.get_field('description').max_length, + required=False, + ) + + image = forms.ImageField( + label=_(u'Sélectionnez le logo du tutoriel (max. {} Ko)').format( + str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), + required=False + ) + + introduction = forms.CharField( + label=_(u'Introduction'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_('Conclusion'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + type = forms.ChoiceField( + choices=TYPE_CHOICES, + required=False + ) + + subcategory = forms.ModelMultipleChoiceField( + label=_(u"Sous catégories de votre tutoriel. Si aucune catégorie ne convient " + u"n'hésitez pas à en demander une nouvelle lors de la validation !"), + queryset=SubCategory.objects.all(), + required=True, + widget=forms.SelectMultiple( + attrs={ + 'required': 'required', + } + ) + ) + + licence = forms.ModelChoiceField( + label=( + _(u'Licence de votre publication (En savoir plus sur les licences et {2})') + .format( + settings.ZDS_APP['site']['licenses']['licence_info_title'], + settings.ZDS_APP['site']['licenses']['licence_info_link'], + settings.ZDS_APP['site']['name'] + ) + ), + queryset=Licence.objects.all(), + required=True, + empty_label=None + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + helps = forms.ModelMultipleChoiceField( + label=_(u"Pour m'aider je cherche un..."), + queryset=HelpWriting.objects.all(), + required=False, + widget=forms.SelectMultiple() + ) + + def __init__(self, *args, **kwargs): + super(TutorialForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('description'), + Field('type'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), + Field('licence'), + Field('subcategory'), + HTML(_(u"

    Demander de l'aide à la communauté !
    " + u"Si vous avez besoin d'un coup de main," + u"sélectionnez une ou plusieurs catégories d'aide ci-dessous " + u"et votre tutoriel apparaitra alors sur la page d'aide.

    ")), + Field('helps'), + Field('msg_commit'), + ButtonHolder( + StrictButton('Valider', type='submit'), + ), + ) + + if 'type' in self.initial: + self.helper['type'].wrap( + Field, + disabled=True) + + +class PartForm(FormWithTitle): + + introduction = forms.CharField( + label=_(u"Introduction"), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_(u"Conclusion"), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(PartForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'), + StrictButton( + _(u'Ajouter et continuer'), + type='submit', + name='submit_continue'), + ) + ) + + +class ChapterForm(FormWithTitle): + + image = forms.ImageField( + label=_(u'Selectionnez le logo du tutoriel ' + u'(max. {0} Ko)').format(str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), + required=False + ) + + introduction = forms.CharField( + label=_(u'Introduction'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + conclusion = forms.CharField( + label=_(u'Conclusion'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ChapterForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'), + StrictButton( + _(u'Ajouter et continuer'), + type='submit', + name='submit_continue'), + )) + + +class EmbdedChapterForm(forms.Form): + introduction = forms.CharField( + required=False, + widget=forms.Textarea + ) + + image = forms.ImageField( + label=_(u'Sélectionnez une image'), + required=False) + + conclusion = forms.CharField( + required=False, + widget=forms.Textarea + ) + + msg_commit = forms.CharField( + label=_(u'Message de suivi'), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Fieldset( + _(u'Contenu'), + Field('image'), + Field('introduction', css_class='md-editor'), + Field('conclusion', css_class='md-editor'), + Field('msg_commit'), + Hidden('last_hash', '{{ last_hash }}'), + ), + ButtonHolder( + Submit('submit', _(u'Valider')) + ) + ) + super(EmbdedChapterForm, self).__init__(*args, **kwargs) + + +class ExtractForm(FormWithTitle): + + text = forms.CharField( + label=_(u'Texte'), + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.') + } + ) + ) + + msg_commit = forms.CharField( + label=_(u"Message de suivi"), + max_length=80, + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'Un résumé de vos ajouts et modifications') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ExtractForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('title'), + Hidden('last_hash', '{{ last_hash }}'), + CommonLayoutVersionEditor(), + ) + + +class ImportForm(forms.Form): + + file = forms.FileField( + label=_(u'Sélectionnez le tutoriel à importer'), + required=True + ) + images = forms.FileField( + label=_(u'Fichier zip contenant les images du tutoriel'), + required=False + ) + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('file'), + Field('images'), + Submit('import-tuto', _(u'Importer le .tuto')), + ) + super(ImportForm, self).__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super(ImportForm, self).clean() + + # Check that the files extensions are correct + tuto = cleaned_data.get('file') + images = cleaned_data.get('images') + + if tuto is not None: + ext = tuto.name.split(".")[-1] + if ext != "tuto": + del cleaned_data['file'] + msg = _(u'Le fichier doit être au format .tuto') + self._errors['file'] = self.error_class([msg]) + + if images is not None: + ext = images.name.split(".")[-1] + if ext != "zip": + del cleaned_data['images'] + msg = _(u'Le fichier doit être au format .zip') + self._errors['images'] = self.error_class([msg]) + + +class ImportArchiveForm(forms.Form): + + file = forms.FileField( + label=_(u"Sélectionnez l'archive de votre tutoriel"), + required=True + ) + + tutorial = forms.ModelChoiceField( + label=_(u"Tutoriel vers lequel vous souhaitez importer votre archive"), + queryset=Tutorial.objects.none(), + required=True + ) + + def __init__(self, user, *args, **kwargs): + super(ImportArchiveForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = 'content-wrapper' + self.helper.form_method = 'post' + self.fields['tutorial'].queryset = Tutorial.objects.filter(authors__in=[user]) + + self.helper.layout = Layout( + Field('file'), + Field('tutorial'), + Submit('import-archive', _(u"Importer l'archive")), + ) + + +# Notes + + +class NoteForm(forms.Form): + text = forms.CharField( + label='', + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Votre message au format Markdown.'), + 'required': 'required' + } + ) + ) + + def __init__(self, tutorial, user, *args, **kwargs): + super(NoteForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse( + 'zds.tutorial.views.answer') + '?tutorial=' + str(tutorial.pk) + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutEditor(), + Hidden('last_note', '{{ last_note_pk }}'), + ) + + if tutorial.antispam(user): + if 'text' not in self.initial: + self.helper['text'].wrap( + Field, + placeholder=_(u'Vous venez de poster. Merci de patienter ' + u'au moins 15 minutes entre deux messages consécutifs ' + u'afin de limiter le flood.'), + disabled=True) + elif tutorial.is_locked: + self.helper['text'].wrap( + Field, + placeholder=_(u'Ce tutoriel est verrouillé.'), + disabled=True + ) + + def clean(self): + cleaned_data = super(NoteForm, self).clean() + + text = cleaned_data.get('text') + + if text is None or text.strip() == '': + self._errors['text'] = self.error_class( + [_(u'Vous devez écrire une réponse !')]) + if 'text' in cleaned_data: + del cleaned_data['text'] + + elif len(text) > settings.ZDS_APP['forum']['max_post_length']: + self._errors['text'] = self.error_class( + [_(u'Ce message est trop long, il ne doit pas dépasser {0} ' + u'caractères').format(settings.ZDS_APP['forum']['max_post_length'])]) + + return cleaned_data + + +# Validations. + +class AskValidationForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire pour votre demande.'), + 'rows': '3' + } + ) + ) + source = forms.CharField( + label='', + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'URL de la version originale') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(AskValidationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.ask_validation') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + Field('source'), + StrictButton( + _(u'Confirmer'), + type='submit'), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), ) + + +class ValidForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire de publication.'), + 'rows': '2' + } + ) + ) + is_major = forms.BooleanField( + label=_(u'Version majeure ?'), + required=False, + initial=True + ) + source = forms.CharField( + label='', + required=False, + widget=forms.TextInput( + attrs={ + 'placeholder': _(u'URL de la version originale') + } + ) + ) + + def __init__(self, *args, **kwargs): + super(ValidForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.valid_tutorial') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + Field('source'), + Field('is_major'), + StrictButton(_(u'Publier'), type='submit'), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), + ) + + +class RejectForm(forms.Form): + + text = forms.CharField( + label='', + required=False, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Commentaire de rejet.'), + 'rows': '6' + } + ) + ) + + def __init__(self, *args, **kwargs): + super(RejectForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.reject_tutorial') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + ButtonHolder( + StrictButton( + _(u'Rejeter'), + type='submit'),), + Hidden('tutorial', '{{ tutorial.pk }}'), + Hidden('version', '{{ version }}'), ) + + +class ActivJsForm(forms.Form): + + js_support = forms.BooleanField( + label='Cocher pour activer JSFiddle', + required=False, + initial=True + ) + + def __init__(self, *args, **kwargs): + super(ActivJsForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('zds.tutorial.views.activ_js') + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Field('js_support'), + ButtonHolder( + StrictButton( + _(u'Valider'), + type='submit'),), + Hidden('tutorial', '{{ tutorial.pk }}'), ) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py new file mode 100644 index 0000000000..cb0ea3b437 --- /dev/null +++ b/zds/tutorialv2/models.py @@ -0,0 +1,753 @@ +# coding: utf-8 + +from math import ceil +import shutil +try: + import ujson as json_reader +except: + try: + import simplejson as json_reader + except: + import json as json_reader + +import json as json_writer +import os + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.db import models +from datetime import datetime +from git.repo import Repo + +from zds.gallery.models import Image, Gallery +from zds.utils import slugify, get_current_user +from zds.utils.models import SubCategory, Licence, Comment +from zds.utils.tutorials import get_blob, export_tutorial + + +TYPE_CHOICES = ( + ('TUTO', 'Tutoriel'), + ('ARTICLE', 'Article'), +) + +STATUS_CHOICES = ( + ('PENDING', 'En attente d\'un validateur'), + ('PENDING_V', 'En cours de validation'), + ('ACCEPT', 'Publié'), + ('REJECT', 'Rejeté'), +) + + +class InvalidOperationError(RuntimeError): + pass + + +class Container(models.Model): + + """A container (tuto/article, part, chapter).""" + class Meta: + verbose_name = 'Container' + verbose_name_plural = 'Containers' + + title = models.CharField('Titre', max_length=80) + + slug = models.SlugField(max_length=80) + + introduction = models.CharField( + 'chemin relatif introduction', + blank=True, + null=True, + max_length=200) + + conclusion = models.CharField( + 'chemin relatif conclusion', + blank=True, + null=True, + max_length=200) + + parent = models.ForeignKey("self", + verbose_name='Conteneur parent', + blank=True, null=True, + on_delete=models.SET_NULL) + + position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', + blank=False, + null=False, + default=1) + + def get_children(self): + """get this container children""" + if self.has_extract(): + return Extract.objects.filter(container_pk=self.pk) + + return Container.objects.filter(parent_pk=self.pk) + + def has_extract(self): + """Check this container has content extracts""" + return Extract.objects.filter(chapter=self).count() > 0 + + def has_sub_part(self): + """Check this container has a sub container""" + return Container.objects.filter(parent=self).count() > 0 + + def get_last_child_position(self): + """Get the relative position of the last child""" + return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + + def add_container(self, container): + """add a child container. A container can only be added if + no extract had already been added in this container""" + if not self.has_extract(): + container.parent = self + container.position_in_parent = container.get_last_child_position() + 1 + container.save() + else: + raise InvalidOperationError("Can't add a container if this container contains extracts.") + + def get_phy_slug(self): + """Get the physical path as stored in git file system""" + base = "" + if self.parent is not None: + base = self.parent.get_phy_slug() + return os.path.join(base, self.slug) + + def update_children(self): + for child in self.get_children(): + if child is Container: + self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") + self.conclusion = os.path.join(self.get_phy_slug(), "conclusion.md") + self.save() + child.update_children() + else: + child.text = child.get_path(relative=True) + child.save() + + def add_extract(self, extract): + if not self.has_sub_part(): + extract.chapter = self + + + +class PubliableContent(Container): + + """A tutorial whatever its size or an aticle.""" + class Meta: + verbose_name = 'Tutoriel' + verbose_name_plural = 'Tutoriels' + + + description = models.CharField('Description', max_length=200) + source = models.CharField('Source', max_length=200) + authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) + + subcategory = models.ManyToManyField(SubCategory, + verbose_name='Sous-Catégorie', + blank=True, null=True, db_index=True) + + image = models.ForeignKey(Image, + verbose_name='Image du tutoriel', + blank=True, null=True, + on_delete=models.SET_NULL) + + gallery = models.ForeignKey(Gallery, + verbose_name='Galerie d\'images', + blank=True, null=True, db_index=True) + + create_at = models.DateTimeField('Date de création') + pubdate = models.DateTimeField('Date de publication', + blank=True, null=True, db_index=True) + update = models.DateTimeField('Date de mise à jour', + blank=True, null=True) + + sha_public = models.CharField('Sha1 de la version publique', + blank=True, null=True, max_length=80, db_index=True) + sha_beta = models.CharField('Sha1 de la version beta publique', + blank=True, null=True, max_length=80, db_index=True) + sha_validation = models.CharField('Sha1 de la version en validation', + blank=True, null=True, max_length=80, db_index=True) + sha_draft = models.CharField('Sha1 de la version de rédaction', + blank=True, null=True, max_length=80, db_index=True) + + licence = models.ForeignKey(Licence, + verbose_name='Licence', + blank=True, null=True, db_index=True) + # as of ZEP 12 this fiels is no longer the size but the type of content (article/tutorial) + type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) + + images = models.CharField( + 'chemin relatif images', + blank=True, + null=True, + max_length=200) + + last_note = models.ForeignKey('Note', blank=True, null=True, + related_name='last_note', + verbose_name='Derniere note') + is_locked = models.BooleanField('Est verrouillé', default=False) + js_support = models.BooleanField('Support du Javascript', default=False) + + def __unicode__(self): + return self.title + + def get_phy_slug(self): + return str(self.pk) + "_" + self.slug + + def get_absolute_url(self): + return reverse('zds.tutorial.views.view_tutorial', args=[ + self.pk, slugify(self.title) + ]) + + def get_absolute_url_online(self): + return reverse('zds.tutorial.views.view_tutorial_online', args=[ + self.pk, slugify(self.title) + ]) + + def get_absolute_url_beta(self): + if self.sha_beta is not None: + return reverse('zds.tutorial.views.view_tutorial', args=[ + self.pk, slugify(self.title) + ]) + '?version=' + self.sha_beta + else: + return self.get_absolute_url() + + def get_edit_url(self): + return reverse('zds.tutorial.views.modify_tutorial') + \ + '?tutorial={0}'.format(self.pk) + + def get_subcontainers(self): + return Container.objects.all()\ + .filter(tutorial__pk=self.pk)\ + .order_by('position_in_parent') + + def in_beta(self): + return (self.sha_beta is not None) and (self.sha_beta.strip() != '') + + def in_validation(self): + return (self.sha_validation is not None) and (self.sha_validation.strip() != '') + + def in_drafting(self): + return (self.sha_draft is not None) and (self.sha_draft.strip() != '') + + def on_line(self): + return (self.sha_public is not None) and (self.sha_public.strip() != '') + + def is_article(self): + return self.type == 'ARTICLE' + + def is_tuto(self): + return self.type == 'TUTO' + + def get_path(self, relative=False): + if relative: + return '' + else: + return os.path.join(settings.ZDS_APP['tutorial']['repo_path'], self.get_phy_slug()) + + def get_prod_path(self, sha=None): + data = self.load_json_for_public(sha) + return os.path.join( + settings.ZDS_APP['tutorial']['repo_public_path'], + str(self.pk) + '_' + slugify(data['title'])) + + def load_dic(self, mandata, sha=None): + '''fill mandata with informations from database model''' + + fns = [ + 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', + 'have_epub', 'get_path', 'in_beta', 'in_validation', 'on_line' + ] + + attrs = [ + 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', + 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' + ] + + # load functions and attributs in tree + for fn in fns: + mandata[fn] = getattr(self, fn) + for attr in attrs: + mandata[attr] = getattr(self, attr) + + # general information + mandata['slug'] = slugify(mandata['title']) + mandata['is_beta'] = self.in_beta() and self.sha_beta == sha + mandata['is_validation'] = self.in_validation() \ + and self.sha_validation == sha + mandata['is_on_line'] = self.on_line() and self.sha_public == sha + + # url: + mandata['get_absolute_url'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + + if self.in_beta(): + mandata['get_absolute_url_beta'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + '?version=' + self.sha_beta + + else: + mandata['get_absolute_url_beta'] = reverse( + 'zds.tutorial.views.view_tutorial', + args=[self.pk, mandata['slug']] + ) + + mandata['get_absolute_url_online'] = reverse( + 'zds.tutorial.views.view_tutorial_online', + args=[self.pk, mandata['slug']] + ) + + def load_introduction_and_conclusion(self, mandata, sha=None, public=False): + '''Explicitly load introduction and conclusion to avoid useless disk + access in load_dic() + ''' + + if public: + mandata['get_introduction_online'] = self.get_introduction_online() + mandata['get_conclusion_online'] = self.get_conclusion_online() + else: + mandata['get_introduction'] = self.get_introduction(sha) + mandata['get_conclusion'] = self.get_conclusion(sha) + + def load_json_for_public(self, sha=None): + if sha is None: + sha = self.sha_public + repo = Repo(self.get_path()) + mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') + data = json_reader.loads(mantuto) + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + + def load_json(self, path=None, online=False): + + if path is None: + if online: + man_path = os.path.join(self.get_prod_path(), 'manifest.json') + else: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + if os.path.isfile(man_path): + json_data = open(man_path) + data = json_reader.load(json_data) + json_data.close() + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + + def dump_json(self, path=None): + if path is None: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + dct = export_tutorial(self) + data = json_writer.dumps(dct, indent=4, ensure_ascii=False) + json_data = open(man_path, "w") + json_data.write(data.encode('utf-8')) + json_data.close() + + def get_introduction(self, sha=None): + # find hash code + if sha is None: + sha = self.sha_draft + repo = Repo(self.get_path()) + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + content_version = json_reader.loads(manifest) + if "introduction" in content_version: + path_content_intro = content_version["introduction"] + + if path_content_intro: + return get_blob(repo.commit(sha).tree, path_content_intro) + + def get_introduction_online(self): + """get the introduction content for a particular version if sha is not None""" + if self.on_line(): + intro = open( + os.path.join( + self.get_prod_path(), + self.introduction + + '.html'), + "r") + intro_contenu = intro.read() + intro.close() + + return intro_contenu.decode('utf-8') + + def get_conclusion(self, sha=None): + """get the conclusion content for a particular version if sha is not None""" + # find hash code + if sha is None: + sha = self.sha_draft + repo = Repo(self.get_path()) + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + content_version = json_reader.loads(manifest) + if "introduction" in content_version: + path_content_ccl = content_version["conclusion"] + + if path_content_ccl: + return get_blob(repo.commit(sha).tree, path_content_ccl) + + def get_conclusion_online(self): + """get the conclusion content for the online version of the current publiable content""" + if self.on_line(): + conclusion = open( + os.path.join( + self.get_prod_path(), + self.conclusion + + '.html'), + "r") + conlusion_content = conclusion.read() + conclusion.close() + + return conlusion_content.decode('utf-8') + + def delete_entity_and_tree(self): + """deletes the entity and its filesystem counterpart""" + shutil.rmtree(self.get_path(), 0) + Validation.objects.filter(tutorial=self).delete() + + if self.gallery is not None: + self.gallery.delete() + if self.on_line(): + shutil.rmtree(self.get_prod_path()) + self.delete() + + def save(self, *args, **kwargs): + self.slug = slugify(self.title) + + super(PubliableContent, self).save(*args, **kwargs) + + def get_note_count(self): + """Return the number of notes in the tutorial.""" + return Note.objects.filter(tutorial__pk=self.pk).count() + + def get_last_note(self): + """Gets the last answer in the thread, if any.""" + return Note.objects.all()\ + .filter(tutorial__pk=self.pk)\ + .order_by('-pubdate')\ + .first() + + def first_note(self): + """Return the first post of a topic, written by topic's author.""" + return Note.objects\ + .filter(tutorial=self)\ + .order_by('pubdate')\ + .first() + + def last_read_note(self): + """Return the last post the user has read.""" + try: + return ContentRead.objects\ + .select_related()\ + .filter(tutorial=self, user=get_current_user())\ + .latest('note__pubdate').note + except Note.DoesNotExist: + return self.first_post() + + def first_unread_note(self): + """Return the first note the user has unread.""" + try: + last_note = ContentRead.objects\ + .filter(tutorial=self, user=get_current_user())\ + .latest('note__pubdate').note + + next_note = Note.objects.filter( + tutorial__pk=self.pk, + pubdate__gt=last_note.pubdate)\ + .select_related("author").first() + + return next_note + except: + return self.first_note() + + def antispam(self, user=None): + """Check if the user is allowed to post in an tutorial according to the + SPAM_LIMIT_SECONDS value. + + If user shouldn't be able to note, then antispam is activated + and this method returns True. Otherwise time elapsed between + user's last note and now is enough, and the method will return + False. + + """ + if user is None: + user = get_current_user() + + last_user_notes = Note.objects\ + .filter(tutorial=self)\ + .filter(author=user.pk)\ + .order_by('-position') + + if last_user_notes and last_user_notes[0] == self.last_note: + last_user_note = last_user_notes[0] + t = datetime.now() - last_user_note.pubdate + if t.total_seconds() < settings.ZDS_APP['forum']['spam_limit_seconds']: + return True + return False + + def change_type(self, new_type): + """Allow someone to change the content type, basicaly from tutorial to article""" + if new_type not in TYPE_CHOICES: + raise ValueError("This type of content does not exist") + + self.type = new_type + + + def have_markdown(self): + """Check the markdown zip archive is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".md")) + + def have_html(self): + """Check the html version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".html")) + + def have_pdf(self): + """Check the pdf version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".pdf")) + + def have_epub(self): + """Check the standard epub version of the content is available""" + return os.path.isfile(os.path.join(self.get_prod_path(), + self.slug + + ".epub")) + + +class Note(Comment): + + """A comment written by an user about a Publiable content he just read.""" + class Meta: + verbose_name = 'note sur un tutoriel' + verbose_name_plural = 'notes sur un tutoriel' + + related_content = models.ForeignKey(PubliableContent, verbose_name='Tutoriel', db_index=True) + + def __unicode__(self): + """Textual form of a post.""" + return u''.format(self.related_content, self.pk) + + def get_absolute_url(self): + page = int(ceil(float(self.position) / settings.ZDS_APP['forum']['posts_per_page'])) + + return '{0}?page={1}#p{2}'.format( + self.related_content.get_absolute_url_online(), + page, + self.pk) + + +class ContentRead(models.Model): + + """Small model which keeps track of the user viewing tutorials. + + It remembers the topic he looked and what was the last Note at this + time. + + """ + class Meta: + verbose_name = 'Tutoriel lu' + verbose_name_plural = 'Tutoriels lus' + + tutorial = models.ForeignKey(PubliableContent, db_index=True) + note = models.ForeignKey(Note, db_index=True) + user = models.ForeignKey(User, related_name='tuto_notes_read', db_index=True) + + def __unicode__(self): + return u''.format(self.tutorial, + self.user, + self.note.pk) + + +class Extract(models.Model): + + """A content extract from a chapter.""" + class Meta: + verbose_name = 'Extrait' + verbose_name_plural = 'Extraits' + + title = models.CharField('Titre', max_length=80) + container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) + position_in_container = models.IntegerField('Position dans le parent', db_index=True) + + text = models.CharField( + 'chemin relatif du texte', + blank=True, + null=True, + max_length=200) + + def __unicode__(self): + return u''.format(self.title) + + def get_absolute_url(self): + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url(), + self.position_in_chapter, + slugify(self.title) + ) + + def get_absolute_url_online(self): + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_online(), + self.position_in_chapter, + slugify(self.title) + ) + + def get_path(self, relative=False): + if relative: + if self.container.tutorial: + chapter_path = '' + else: + chapter_path = os.path.join( + self.container.part.get_phy_slug(), + self.container.get_phy_slug()) + else: + if self.container.tutorial: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + self.container.tutorial.get_phy_slug()) + else: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + self.container.part.tutorial.get_phy_slug(), + self.container.part.get_phy_slug(), + self.container.get_phy_slug()) + + return os.path.join(chapter_path, str(self.pk) + "_" + slugify(self.title)) + '.md' + + def get_prod_path(self): + + if self.container.tutorial: + data = self.container.tutorial.load_json_for_public() + mandata = self.container.tutorial.load_dic(data) + if "chapter" in mandata: + for ext in mandata["chapter"]["extracts"]: + if ext['pk'] == self.pk: + return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(self.container.tutorial.pk) + '_' + slugify(mandata['title']), + str(ext['pk']) + "_" + slugify(ext['title'])) \ + + '.md.html' + else: + data = self.container.part.tutorial.load_json_for_public() + mandata = self.container.part.tutorial.load_dic(data) + for part in mandata["parts"]: + for chapter in part["chapters"]: + for ext in chapter["extracts"]: + if ext['pk'] == self.pk: + return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(mandata['pk']) + '_' + slugify(mandata['title']), + str(part['pk']) + "_" + slugify(part['title']), + str(chapter['pk']) + "_" + slugify(chapter['title']), + str(ext['pk']) + "_" + slugify(ext['title'])) \ + + '.md.html' + + def get_text(self, sha=None): + + if self.container.tutorial: + tutorial = self.container.tutorial + else: + tutorial = self.container.part.tutorial + repo = Repo(tutorial.get_path()) + + # find hash code + if sha is None: + sha = tutorial.sha_draft + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + tutorial_version = json_reader.loads(manifest) + if "parts" in tutorial_version: + for part in tutorial_version["parts"]: + if "chapters" in part: + for chapter in part["chapters"]: + if "extracts" in chapter: + for extract in chapter["extracts"]: + if extract["pk"] == self.pk: + path_ext = extract["text"] + break + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if "extracts" in chapter: + for extract in chapter["extracts"]: + if extract["pk"] == self.pk: + path_ext = extract["text"] + break + + if path_ext: + return get_blob(repo.commit(sha).tree, path_ext) + else: + return None + + def get_text_online(self): + + if self.container.tutorial: + path = os.path.join( + self.container.tutorial.get_prod_path(), + self.text + + '.html') + else: + path = os.path.join( + self.container.part.tutorial.get_prod_path(), + self.text + + '.html') + + if os.path.isfile(path): + text = open(path, "r") + text_contenu = text.read() + text.close() + + return text_contenu.decode('utf-8') + else: + return None + + +class Validation(models.Model): + + """Tutorial validation.""" + class Meta: + verbose_name = 'Validation' + verbose_name_plural = 'Validations' + + tutorial = models.ForeignKey(PubliableContent, null=True, blank=True, + verbose_name='Tutoriel proposé', db_index=True) + version = models.CharField('Sha1 de la version', + blank=True, null=True, max_length=80, db_index=True) + date_proposition = models.DateTimeField('Date de proposition', db_index=True) + comment_authors = models.TextField('Commentaire de l\'auteur') + validator = models.ForeignKey(User, + verbose_name='Validateur', + related_name='author_validations', + blank=True, null=True, db_index=True) + date_reserve = models.DateTimeField('Date de réservation', + blank=True, null=True) + date_validation = models.DateTimeField('Date de validation', + blank=True, null=True) + comment_validator = models.TextField('Commentaire du validateur', + blank=True, null=True) + status = models.CharField( + max_length=10, + choices=STATUS_CHOICES, + default='PENDING') + + def __unicode__(self): + return self.tutorial.title + + def is_pending(self): + return self.status == 'PENDING' + + def is_pending_valid(self): + return self.status == 'PENDING_V' + + def is_accept(self): + return self.status == 'ACCEPT' + + def is_reject(self): + return self.status == 'REJECT' diff --git a/zds/tutorialv2/search_indexes.py b/zds/tutorialv2/search_indexes.py new file mode 100644 index 0000000000..15e8cf1afa --- /dev/null +++ b/zds/tutorialv2/search_indexes.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +from django.db.models import Q + +from haystack import indexes + +from zds.tutorial.models import Tutorial, Part, Chapter, Extract + + +class TutorialIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr='description') + category = indexes.CharField(model_attr='subcategory') + sha_public = indexes.CharField(model_attr='sha_public') + + def get_model(self): + return Tutorial + + def index_queryset(self, using=None): + """Only tutorials online.""" + return self.get_model().objects.filter(sha_public__isnull=False) + + +class PartIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + tutorial = indexes.CharField(model_attr='tutorial') + + def get_model(self): + return Part + + def index_queryset(self, using=None): + """Only parts online.""" + return self.get_model().objects.filter( + tutorial__sha_public__isnull=False) + + +class ChapterIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + # A Chapter belongs to a Part (big-tuto) **or** a Tutorial (mini-tuto) + part = indexes.CharField(model_attr='part', null=True) + tutorial = indexes.CharField(model_attr='tutorial', null=True) + + def get_model(self): + return Chapter + + def index_queryset(self, using=None): + """Only chapters online.""" + return self.get_model()\ + .objects.filter(Q(tutorial__sha_public__isnull=False) + | Q(part__tutorial__sha_public__isnull=False)) + + +class ExtractIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + chapter = indexes.CharField(model_attr='chapter') + txt = indexes.CharField(model_attr='text') + + def get_model(self): + return Extract + + def index_queryset(self, using=None): + """Only extracts online.""" + return self.get_model() .objects.filter(Q(chapter__tutorial__sha_public__isnull=False) + | Q(chapter__part__tutorial__sha_public__isnull=False)) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py new file mode 100644 index 0000000000..15e3f3e8d3 --- /dev/null +++ b/zds/tutorialv2/urls.py @@ -0,0 +1,123 @@ +# coding: utf-8 + +from django.conf.urls import patterns, url + +from . import views +from . import feeds + +urlpatterns = patterns('', + # Viewing + url(r'^flux/rss/$', feeds.LastTutorialsFeedRSS(), name='tutorial-feed-rss'), + url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), + + # Current URLs + url(r'^recherche/(?P\d+)/$', + 'zds.tutorial.views.find_tuto'), + + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_chapter', + name="view-chapter-url"), + + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_part', + name="view-part-url"), + + url(r'^off/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_tutorial'), + + # View online + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_chapter_online', + name="view-chapter-url-online"), + + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_part_online', + name="view-part-url-online"), + + url(r'^(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.view_tutorial_online'), + + # Editing + url(r'^editer/tutoriel/$', + 'zds.tutorial.views.edit_tutorial'), + url(r'^modifier/tutoriel/$', + 'zds.tutorial.views.modify_tutorial'), + url(r'^modifier/partie/$', + 'zds.tutorial.views.modify_part'), + url(r'^editer/partie/$', + 'zds.tutorial.views.edit_part'), + url(r'^modifier/chapitre/$', + 'zds.tutorial.views.modify_chapter'), + url(r'^editer/chapitre/$', + 'zds.tutorial.views.edit_chapter'), + url(r'^modifier/extrait/$', + 'zds.tutorial.views.modify_extract'), + url(r'^editer/extrait/$', + 'zds.tutorial.views.edit_extract'), + + # Adding + url(r'^nouveau/tutoriel/$', + 'zds.tutorial.views.add_tutorial'), + url(r'^nouveau/partie/$', + 'zds.tutorial.views.add_part'), + url(r'^nouveau/chapitre/$', + 'zds.tutorial.views.add_chapter'), + url(r'^nouveau/extrait/$', + 'zds.tutorial.views.add_extract'), + + url(r'^$', 'zds.tutorial.views.index'), + url(r'^importer/$', 'zds.tutorial.views.import_tuto'), + url(r'^import_local/$', + 'zds.tutorial.views.local_import'), + url(r'^telecharger/$', 'zds.tutorial.views.download'), + url(r'^telecharger/pdf/$', + 'zds.tutorial.views.download_pdf'), + url(r'^telecharger/html/$', + 'zds.tutorial.views.download_html'), + url(r'^telecharger/epub/$', + 'zds.tutorial.views.download_epub'), + url(r'^telecharger/md/$', + 'zds.tutorial.views.download_markdown'), + url(r'^historique/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.history'), + url(r'^comparaison/(?P\d+)/(?P.+)/$', + 'zds.tutorial.views.diff'), + + # user actions + url(r'^suppression/(?P\d+)/$', + 'zds.tutorial.views.delete_tutorial'), + url(r'^validation/tutoriel/$', + 'zds.tutorial.views.ask_validation'), + + # Validation + url(r'^validation/$', + 'zds.tutorial.views.list_validation'), + url(r'^validation/reserver/(?P\d+)/$', + 'zds.tutorial.views.reservation'), + url(r'^validation/reject/$', + 'zds.tutorial.views.reject_tutorial'), + url(r'^validation/valid/$', + 'zds.tutorial.views.valid_tutorial'), + url(r'^validation/invalid/(?P\d+)/$', + 'zds.tutorial.views.invalid_tutorial'), + url(r'^validation/historique/(?P\d+)/$', + 'zds.tutorial.views.history_validation'), + url(r'^activation_js/$', + 'zds.tutorial.views.activ_js'), + # Reactions + url(r'^message/editer/$', + 'zds.tutorial.views.edit_note'), + url(r'^message/nouveau/$', 'zds.tutorial.views.answer'), + url(r'^message/like/$', 'zds.tutorial.views.like_note'), + url(r'^message/dislike/$', + 'zds.tutorial.views.dislike_note'), + + # Moderation + url(r'^resolution_alerte/$', + 'zds.tutorial.views.solve_alert'), + + # Help + url(r'^aides/$', + 'zds.tutorial.views.help_tutorial'), + ) + diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py new file mode 100644 index 0000000000..365abcf760 --- /dev/null +++ b/zds/tutorialv2/utils.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +from zds.tutorialv2.models import PubliableContent, ContentRead +from zds import settings +from zds.utils import get_current_user + +def get_last_tutorials(): + """get the last issued tutorials""" + n = settings.ZDS_APP['tutorial']['home_number'] + tutorials = PubliableContent.objects.all()\ + .exclude(type="ARTICLE")\ + .exclude(sha_public__isnull=True)\ + .exclude(sha_public__exact='')\ + .order_by('-pubdate')[:n] + + return tutorials + + +def get_last_articles(): + """get the last issued articles""" + n = settings.ZDS_APP['tutorial']['home_number'] + articles = PubliableContent.objects.all()\ + .exclude(type="TUTO")\ + .exclude(sha_public__isnull=True)\ + .exclude(sha_public__exact='')\ + .order_by('-pubdate')[:n] + + return articles + + +def never_read(tutorial, user=None): + """Check if a topic has been read by an user since it last post was + added.""" + if user is None: + user = get_current_user() + + return ContentRead.objects\ + .filter(note=tutorial.last_note, tutorial=tutorial, user=user)\ + .count() == 0 + + +def mark_read(tutorial): + """Mark a tutorial as read for the user.""" + if tutorial.last_note is not None: + ContentRead.objects.filter( + tutorial=tutorial, + user=get_current_user()).delete() + a = ContentRead( + note=tutorial.last_note, + tutorial=tutorial, + user=get_current_user()) + a.save() \ No newline at end of file diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py new file mode 100644 index 0000000000..efd77f81f0 --- /dev/null +++ b/zds/tutorialv2/views.py @@ -0,0 +1,3643 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from collections import OrderedDict +from datetime import datetime +from operator import attrgetter +from urllib import urlretrieve +from django.contrib.humanize.templatetags.humanize import naturaltime +from urlparse import urlparse, parse_qs +try: + import ujson as json_reader +except ImportError: + try: + import simplejson as json_reader + except ImportError: + import json as json_reader +import json +import json as json_writer +import shutil +import re +import zipfile +import os +import glob +import tempfile + +from PIL import Image as ImagePIL +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.core.files import File +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.core.urlresolvers import reverse +from django.db import transaction +from django.db.models import Q, Count +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.encoding import smart_str +from django.views.decorators.http import require_POST +from git import Repo, Actor +from lxml import etree + +from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ + ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm +from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ + mark_read, Note, HelpWriting +from zds.gallery.models import Gallery, UserGallery, Image +from zds.member.decorator import can_write_and_read_now +from zds.member.models import get_info_old_tuto, Profile +from zds.member.views import get_client_ip +from zds.forum.models import Forum, Topic +from zds.utils import slugify +from zds.utils.models import Alert +from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ + SubCategory +from zds.utils.mps import send_mp +from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic +from zds.utils.paginator import paginator_range +from zds.utils.templatetags.emarkdown import emarkdown +from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive +from zds.utils.misc import compute_hash, content_has_changed +from django.utils.translation import ugettext as _ + + +def render_chapter_form(chapter): + if chapter.part: + return ChapterForm({"title": chapter.title, + "introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + else: + + return \ + EmbdedChapterForm({"introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + + +def index(request): + """Display all public tutorials of the website.""" + + # The tag indicate what the category tutorial the user would like to + # display. We can display all subcategories for tutorials. + + try: + tag = get_object_or_404(SubCategory, slug=request.GET["tag"]) + except (KeyError, Http404): + tag = None + if tag is None: + tutorials = \ + Tutorial.objects.filter(sha_public__isnull=False).exclude(sha_public="") \ + .order_by("-pubdate") \ + .all() + else: + # The tag isn't None and exist in the system. We can use it to retrieve + # all tutorials in the subcategory specified. + + tutorials = Tutorial.objects.filter( + sha_public__isnull=False, + subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata) + tuto_versions.append(mandata) + return render(request, "tutorial/index.html", {"tutorials": tuto_versions, "tag": tag}) + + +# Staff actions. + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +def list_validation(request): + """Display tutorials list in validation.""" + + # Retrieve type of the validation. Default value is all validations. + + try: + type = request.GET["type"] + except KeyError: + type = None + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + + # Orphan validation. There aren't validator attached to the validations. + + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": + + # Reserved validation. There are a validator attached to the + # validations. + + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + else: + + # Default, we display all validations. + + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" + + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.tutorial.get_absolute_url() + + "?version=" + validation.version + ) + + +@login_required +def diff(request, tutorial_pk, tutorial_slug): + try: + sha = request.GET["sha"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + repo = Repo(tutorial.get_path()) + hcommit = repo.commit(sha) + tdiff = hcommit.diff("HEAD~1") + return render(request, "tutorial/tutorial/diff.html", { + "tutorial": tutorial, + "path_add": tdiff.iter_change_type("A"), + "path_ren": tdiff.iter_change_type("R"), + "path_del": tdiff.iter_change_type("D"), + "path_maj": tdiff.iter_change_type("M"), + }) + + +@login_required +def history(request, tutorial_pk, tutorial_slug): + """History of the tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + repo = Repo(tutorial.get_path()) + logs = repo.head.reference.log() + logs = sorted(logs, key=attrgetter("time"), reverse=True) + return render(request, "tutorial/tutorial/history.html", + {"tutorial": tutorial, "logs": logs}) + + +@login_required +@permission_required("tutorial.change_tutorial", raise_exception=True) +def history_validation(request, tutorial_pk): + """History of the validation of a tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + if subcategory is None: + validations = \ + Validation.objects.filter(tutorial__pk=tutorial_pk) \ + .order_by("date_proposition" + ).all() + else: + validations = Validation.objects.filter(tutorial__pk=tutorial_pk, + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition" + ).all() + return render(request, "tutorial/validation/history.html", + {"validations": validations, "tutorial": tutorial}) + + +@can_write_and_read_now +@login_required +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def reject_tutorial(request): + """Staff reject tutorial of an author.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + validation.comment_validator = request.POST["text"] + validation.status = "REJECT" + validation.date_validation = datetime.now() + validation.save() + + # Remove sha_validation because we rejected this version of the tutorial. + + tutorial.sha_validation = None + tutorial.pubdate = None + tutorial.save() + messages.info(request, _(u"Le tutoriel a bien été refusé.")) + comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) + # send feedback + msg = ( + _(u'Désolé, le zeste **{0}** n\'a malheureusement ' + u'pas passé l’étape de validation. Mais ne désespère pas, ' + u'certaines corrections peuvent surement être faite pour ' + u'l’améliorer et repasser la validation plus tard. ' + u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' + u'N\'hésite pas a lui envoyer un petit message pour discuter ' + u'de la décision ou demander plus de détail si tout cela te ' + u'semble injuste ou manque de clarté.') + .format(tutorial.title, + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), + comment_reject)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Refus de Validation : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le refuser.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +@can_write_and_read_now +@login_required +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def valid_tutorial(request): + """Staff valid tutorial of an author.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + (output, err) = mep(tutorial, tutorial.sha_validation) + messages.info(request, output) + messages.error(request, err) + validation.comment_validator = request.POST["text"] + validation.status = "ACCEPT" + validation.date_validation = datetime.now() + validation.save() + + # Update sha_public with the sha of validation. We don't update sha_draft. + # So, the user can continue to edit his tutorial in offline. + + if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': + tutorial.pubdate = datetime.now() + tutorial.sha_public = validation.version + tutorial.source = request.POST["source"] + tutorial.sha_validation = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été validé.")) + + # send feedback + + msg = ( + _(u'Félicitations ! Le zeste [{0}]({1}) ' + u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' + u'peuvent venir l\'éplucher et réagir a son sujet. ' + u'Je te conseille de rester a leur écoute afin ' + u'd\'apporter des corrections/compléments.' + u'Un Tutoriel vivant et a jour est bien plus lu ' + u'qu\'un sujet abandonné !') + .format(tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Publication : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le valider.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +@can_write_and_read_now +@login_required +@permission_required("tutorial.change_tutorial", raise_exception=True) +@require_POST +def invalid_tutorial(request, tutorial_pk): + """Staff invalid tutorial of an author.""" + + # Retrieve current tutorial + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + un_mep(tutorial) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_public).latest("date_proposition") + validation.status = "PENDING" + validation.date_validation = None + validation.save() + + # Only update sha_validation because contributors can contribute on + # rereading version. + + tutorial.sha_public = None + tutorial.sha_validation = validation.version + tutorial.pubdate = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été dépublié.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + + +# User actions on tutorial. + +@can_write_and_read_now +@login_required +@require_POST +def ask_validation(request): + """User ask validation for his tutorial.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.POST["tutorial"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator + else: + old_validator = None + # delete old pending validation + Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING', 'PENDING_V'])\ + .delete() + # We create and save validation object of the tutorial. + + validation = Validation() + validation.tutorial = tutorial + validation.date_proposition = datetime.now() + validation.comment_authors = request.POST["text"] + validation.version = request.POST["version"] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' + u'consulter l\'historique des versions' + u'\n\n> Merci').format(old_validator.username, tutorial.title)) + send_mp( + bot, + [old_validator], + _(u"Mise à jour de tuto : {0}").format(tutorial.title), + _(u"En validation"), + msg, + False, + ) + validation.save() + validation.tutorial.source = request.POST["source"] + validation.tutorial.sha_validation = request.POST["version"] + validation.tutorial.save() + messages.success(request, + _(u"Votre demande de validation a été envoyée à l'équipe.")) + return redirect(tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +@require_POST +def delete_tutorial(request, tutorial_pk): + """User would like delete his tutorial.""" + + # Retrieve current tutorial + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # when author is alone we can delete definitively tutorial + + if tutorial.authors.count() == 1: + + # user can access to gallery + + try: + ug = UserGallery.objects.filter(user=request.user, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + + # Delete the tutorial on the repo and on the database. + + old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + maj_repo_tuto(request, old_slug_path=old_slug, tuto=tutorial, + action="del") + messages.success(request, + _(u'Le tutoriel {0} a bien ' + u'été supprimé.').format(tutorial.title)) + tutorial.delete() + else: + tutorial.authors.remove(request.user) + + # user can access to gallery + + try: + ug = UserGallery.objects.filter( + user=request.user, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + tutorial.save() + messages.success(request, + _(u'Vous ne faites plus partie des rédacteurs de ce ' + u'tutoriel.')) + return redirect(reverse("zds.tutorial.views.index")) + + +@can_write_and_read_now +@require_POST +def modify_tutorial(request): + tutorial_pk = request.POST["tutorial"] + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + # User actions + + if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): + if "add_author" in request.POST: + redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ + tutorial.pk, + tutorial.slug, + ]) + author_username = request.POST["author"] + author = None + try: + author = User.objects.get(username=author_username) + except User.DoesNotExist: + return redirect(redirect_url) + tutorial.authors.add(author) + tutorial.save() + + # share gallery + + ug = UserGallery() + ug.user = author + ug.gallery = tutorial.gallery + ug.mode = "W" + ug.save() + messages.success(request, + _(u'L\'auteur {0} a bien été ajouté à la rédaction ' + u'du tutoriel.').format(author.username)) + + # send msg to new author + + msg = ( + _(u'Bonjour **{0}**,\n\n' + u'Tu as été ajouté comme auteur du tutoriel [{1}]({2}).\n' + u'Tu peux retrouver ce tutoriel en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' + u'"Tutoriels" sur la page de ton profil.\n\n' + u'Tu peux maintenant commencer à rédiger !').format( + author.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url(), + settings.ZDS_APP['site']['url'] + reverse("zds.member.views.tutorials")) + ) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + [author], + _(u"Ajout en tant qu'auteur : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + + return redirect(redirect_url) + elif "remove_author" in request.POST: + redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ + tutorial.pk, + tutorial.slug, + ]) + + # Avoid orphan tutorials + + if tutorial.authors.all().count() <= 1: + raise Http404 + author_pk = request.POST["author"] + author = get_object_or_404(User, pk=author_pk) + tutorial.authors.remove(author) + + # user can access to gallery + + try: + ug = UserGallery.objects.filter(user=author, + gallery=tutorial.gallery) + ug.delete() + except: + ug = None + tutorial.save() + messages.success(request, + _(u"L'auteur {0} a bien été retiré du tutoriel.") + .format(author.username)) + + # send msg to removed author + + msg = ( + _(u'Bonjour **{0}**,\n\n' + u'Tu as été supprimé des auteurs du tutoriel [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' + u'pourra plus y accéder.\n').format( + author.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url()) + ) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + [author], + _(u"Suppression des auteurs : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + + return redirect(redirect_url) + elif "activ_beta" in request.POST: + if "version" in request.POST: + tutorial.sha_beta = request.POST['version'] + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id'])\ + .first() + msg = \ + (_(u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerais obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide').format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + + create_topic(author=request.user, + forum=forum, + title=_(u"[beta][tutoriel]{0}").format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + tp = Topic.objects.get(key=tutorial.pk) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + private_mp = \ + (_(u'Bonjour {},\n\n' + u'Vous venez de mettre votre tutoriel **{}** en beta. La communauté ' + u'pourra le consulter afin de vous faire des retours ' + u'constructifs avant sa soumission en validation.\n\n' + u'Un sujet dédié pour la beta de votre tutoriel a été ' + u'crée dans le forum et est accessible [ici]({})').format( + request.user.username, + tutorial.title, + settings.ZDS_APP['site']['url'] + tp.get_absolute_url())) + send_mp( + bot, + [request.user], + _(u"Tutoriel en beta : {0}").format(tutorial.title), + "", + private_mp, + False, + ) + else: + msg_up = \ + (_(u'Bonjour,\n\n' + u'La beta du tutoriel est de nouveau active.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures').format(tutorial.title, + settings.ZDS_APP['site']['url'] + + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) + + messages.success(request, _(u"La BETA sur ce tutoriel est bien activée.")) + else: + messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être activée.")) + return redirect(tutorial.get_absolute_url_beta()) + elif "update_beta" in request.POST: + if "version" in request.POST: + tutorial.sha_beta = request.POST['version'] + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, + forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() + msg = \ + (_(u'Bonjour à tous,\n\n' + u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' + u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' + u'sur la forme, afin de proposer en validation un texte de qualité.' + u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' + u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' + u'\n\nMerci d\'avance pour votre aide').format( + naturaltime(tutorial.create_at), + tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) + if topic is None: + forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + + create_topic(author=request.user, + forum=forum, + title=u"[beta][tutoriel]{0}".format(tutorial.title), + subtitle=u"{}".format(tutorial.description), + text=msg, + key=tutorial.pk + ) + else: + msg_up = \ + (_(u'Bonjour, !\n\n' + u'La beta du tutoriel a été mise à jour.' + u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' + u'\n\nMerci pour vos relectures').format(tutorial.title, + settings.ZDS_APP['site']['url'] + + tutorial.get_absolute_url_beta())) + unlock_topic(topic, msg) + send_post(topic, msg_up) + messages.success(request, _(u"La BETA sur ce tutoriel a bien été mise à jour.")) + else: + messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être mise à jour.")) + return redirect(tutorial.get_absolute_url_beta()) + elif "desactiv_beta" in request.POST: + tutorial.sha_beta = None + tutorial.save() + topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() + if topic is not None: + msg = \ + (_(u'Désactivation de la beta du tutoriel **{}**' + u'\n\nPour plus d\'informations envoyez moi un message privé.').format(tutorial.title)) + lock_topic(topic) + send_post(topic, msg) + messages.info(request, _(u"La BETA sur ce tutoriel a bien été désactivée.")) + + return redirect(tutorial.get_absolute_url()) + + # No action performed, raise 403 + + raise PermissionDenied + + +# Tutorials. + + +@login_required +def view_tutorial(request, tutorial_pk, tutorial_slug): + """Show the given offline tutorial if exists.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the article. + + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # Two variables to handle two distinct cases (large/small tutorial) + + chapter = None + parts = None + + # Find the good manifest file + + repo = Repo(tutorial.get_path()) + + # Load the tutorial. + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha) + tutorial.load_introduction_and_conclusion(mandata, sha) + + # If it's a small tutorial, fetch its chapter + + if tutorial.type == "MINI": + if 'chapter' in mandata: + chapter = mandata["chapter"] + chapter["path"] = tutorial.get_path() + chapter["type"] = "MINI" + chapter["pk"] = Chapter.objects.get(tutorial=tutorial).pk + chapter["intro"] = get_blob(repo.commit(sha).tree, + "introduction.md") + chapter["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" + ) + cpt = 1 + for ext in chapter["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt += 1 + else: + chapter = None + else: + + # If it's a big tutorial, fetch parts. + + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + part["tutorial"] = tutorial + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt_e += 1 + cpt_c += 1 + cpt_p += 1 + validation = Validation.objects.filter(tutorial__pk=tutorial.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": tutorial.js_support}) + + if tutorial.source: + form_ask_validation = AskValidationForm(initial={"source": tutorial.source}) + form_valid = ValidForm(initial={"source": tutorial.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + return render(request, "tutorial/tutorial/view.html", { + "tutorial": mandata, + "chapter": chapter, + "parts": parts, + "version": sha, + "validation": validation, + "formAskValidation": form_ask_validation, + "formJs": form_js, + "formValid": form_valid, + "formReject": form_reject, + "is_js": is_js + }) + + +def view_tutorial_online(request, tutorial_pk, tutorial_slug): + """Display a tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the tutorial isn't online, we raise 404 error. + if not tutorial.on_line(): + raise Http404 + + # Two variables to handle two distinct cases (large/small tutorial) + + chapter = None + parts = None + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + tutorial.load_introduction_and_conclusion(mandata, public=True) + mandata["update"] = tutorial.update + mandata["get_note_count"] = tutorial.get_note_count() + + # If it's a small tutorial, fetch its chapter + + if tutorial.type == "MINI": + if "chapter" in mandata: + chapter = mandata["chapter"] + chapter["path"] = tutorial.get_prod_path() + chapter["type"] = "MINI" + intro = open(os.path.join(tutorial.get_prod_path(), + mandata["introduction"] + ".html"), "r") + chapter["intro"] = intro.read() + intro.close() + conclu = open(os.path.join(tutorial.get_prod_path(), + mandata["conclusion"] + ".html"), "r") + chapter["conclu"] = conclu.read() + conclu.close() + cpt = 1 + for ext in chapter["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = tutorial.get_prod_path() + text = open(os.path.join(tutorial.get_prod_path(), ext["text"] + + ".html"), "r") + ext["txt"] = text.read() + text.close() + cpt += 1 + else: + chapter = None + else: + + # chapter = Chapter.objects.get(tutorial=tutorial) + + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + part["tutorial"] = mandata + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + cpt_e += 1 + cpt_c += 1 + part["get_chapters"] = part["chapters"] + cpt_p += 1 + + mandata['get_parts'] = parts + + # If the user is authenticated + if request.user.is_authenticated(): + # We check if he can post a tutorial or not with + # antispam filter. + mandata['antispam'] = tutorial.antispam() + + # If the user is never read, we mark this tutorial read. + if never_read(tutorial): + mark_read(tutorial) + + # Find all notes of the tutorial. + + notes = Note.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() + + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. + + last_note_pk = 0 + if tutorial.last_note: + last_note_pk = tutorial.last_note.pk + + # Handle pagination + + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) + try: + page_nbr = int(request.GET["page"]) + except KeyError: + page_nbr = 1 + except ValueError: + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + + res = [] + if page_nbr != 1: + + # Show the last note of the previous page + + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) + + # Build form to send a note for the current tutorial. + + form = NoteForm(tutorial, request.user) + return render(request, "tutorial/tutorial/view_online.html", { + "tutorial": mandata, + "chapter": chapter, + "parts": parts, + "notes": res, + "pages": paginator_range(page_nbr, paginator.num_pages), + "nb": page_nbr, + "last_note_pk": last_note_pk, + "form": form, + }) + + +@can_write_and_read_now +@login_required +def add_tutorial(request): + """'Adds a tutorial.""" + + if request.method == "POST": + form = TutorialForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + + # Creating a tutorial + + tutorial = Tutorial() + tutorial.title = data["title"] + tutorial.description = data["description"] + tutorial.type = data["type"] + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + tutorial.images = "images" + if "licence" in data and data["licence"] != "": + lc = Licence.objects.filter(pk=data["licence"]).all()[0] + tutorial.licence = lc + else: + tutorial.licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + + # add create date + + tutorial.create_at = datetime.now() + tutorial.pubdate = datetime.now() + + # Creating the gallery + + gal = Gallery() + gal.title = data["title"] + gal.slug = slugify(data["title"]) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = gal + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + tutorial.image = img + tutorial.save() + + # Add subcategories on tutorial + + for subcat in form.cleaned_data["subcategory"]: + tutorial.subcategory.add(subcat) + + # Add helps if needed + for helpwriting in form.cleaned_data["helps"]: + tutorial.helps.add(helpwriting) + + # We need to save the tutorial before changing its author list + # since it's a many-to-many relationship + + tutorial.authors.add(request.user) + + # If it's a small tutorial, create its corresponding chapter + + if tutorial.type == "MINI": + chapter = Chapter() + chapter.tutorial = tutorial + chapter.save() + tutorial.save() + maj_repo_tuto( + request, + new_slug_path=tutorial.get_path(), + tuto=tutorial, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + return redirect(tutorial.get_absolute_url()) + else: + form = TutorialForm( + initial={ + 'licence': Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + } + ) + return render(request, "tutorial/tutorial/new.html", {"form": form}) + + +@can_write_and_read_now +@login_required +def edit_tutorial(request): + """Edit a tutorial.""" + + # Retrieve current tutorial; + + try: + tutorial_pk = request.GET["tutoriel"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + introduction = os.path.join(tutorial.get_path(), "introduction.md") + conclusion = os.path.join(tutorial.get_path(), "conclusion.md") + if request.method == "POST": + form = TutorialForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = TutorialForm(initial={ + "title": tutorial.title, + "type": tutorial.type, + "licence": tutorial.licence, + "description": tutorial.description, + "subcategory": tutorial.subcategory.all(), + "introduction": tutorial.get_introduction(), + "conclusion": tutorial.get_conclusion(), + "helps": tutorial.helps.all(), + }) + return render(request, "tutorial/tutorial/edit.html", + { + "tutorial": tutorial, "form": form, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True + }) + old_slug = tutorial.get_path() + tutorial.title = data["title"] + tutorial.description = data["description"] + if "licence" in data and data["licence"] != "": + lc = Licence.objects.filter(pk=data["licence"]).all()[0] + tutorial.licence = lc + else: + tutorial.licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + + # add MAJ date + + tutorial.update = datetime.now() + + # MAJ gallery + + gal = Gallery.objects.filter(pk=tutorial.gallery.pk) + gal.update(title=data["title"]) + gal.update(slug=slugify(data["title"])) + gal.update(update=datetime.now()) + + # MAJ image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = tutorial.gallery + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + tutorial.image = img + tutorial.save() + tutorial.update_children() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + + maj_repo_tuto( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + tuto=tutorial, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + + tutorial.subcategory.clear() + for subcat in form.cleaned_data["subcategory"]: + tutorial.subcategory.add(subcat) + + tutorial.helps.clear() + for help in form.cleaned_data["helps"]: + tutorial.helps.add(help) + + tutorial.save() + return redirect(tutorial.get_absolute_url()) + else: + json = tutorial.load_json() + if "licence" in json: + licence = json['licence'] + else: + licence = Licence.objects.get( + pk=settings.ZDS_APP['tutorial']['default_license_pk'] + ) + form = TutorialForm(initial={ + "title": json["title"], + "type": json["type"], + "licence": licence, + "description": json["description"], + "subcategory": tutorial.subcategory.all(), + "introduction": tutorial.get_introduction(), + "conclusion": tutorial.get_conclusion(), + "helps": tutorial.helps.all(), + }) + return render(request, "tutorial/tutorial/edit.html", + {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])}) + +# Parts. + + +@login_required +def view_part( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, +): + """Display a part.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + final_part = None + + # find the good manifest file + + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha=sha) + + parts = mandata["parts"] + find = False + cpt_p = 1 + for part in parts: + if part_pk == str(part["pk"]): + find = True + part["tutorial"] = tutorial + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) + part["conclu"] = get_blob(repo.commit(sha).tree, part["conclusion"]) + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + cpt_e += 1 + cpt_c += 1 + final_part = part + break + cpt_p += 1 + + # if part can't find + if not find: + raise Http404 + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + + return render(request, "tutorial/part/view.html", + {"tutorial": mandata, + "part": final_part, + "version": sha, + "is_js": is_js}) + + +def view_part_online( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, +): + """Display a part.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if not tutorial.on_line(): + raise Http404 + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + mandata["update"] = tutorial.update + + mandata["get_parts"] = mandata["parts"] + parts = mandata["parts"] + cpt_p = 1 + final_part = None + find = False + for part in parts: + part["tutorial"] = mandata + part["path"] = tutorial.get_path() + part["slug"] = slugify(part["title"]) + part["position_in_tutorial"] = cpt_p + if part_pk == str(part["pk"]): + find = True + intro = open(os.path.join(tutorial.get_prod_path(), + part["introduction"] + ".html"), "r") + part["intro"] = intro.read() + intro.close() + conclu = open(os.path.join(tutorial.get_prod_path(), + part["conclusion"] + ".html"), "r") + part["conclu"] = conclu.read() + conclu.close() + final_part = part + cpt_c = 1 + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + if part_slug == slugify(part["title"]): + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_prod_path() + cpt_e += 1 + cpt_c += 1 + part["get_chapters"] = part["chapters"] + cpt_p += 1 + + # if part can't find + if not find: + raise Http404 + + return render(request, "tutorial/part/view_online.html", {"part": final_part}) + + +@can_write_and_read_now +@login_required +def add_part(request): + """Add a new part.""" + + try: + tutorial_pk = request.GET["tutoriel"] + except KeyError: + raise Http404 + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Make sure it's a big tutorial, just in case + + if not tutorial.type == "BIG": + raise Http404 + + # Make sure the user belongs to the author list + + if request.user not in tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = PartForm(request.POST) + if form.is_valid(): + data = form.data + part = Part() + part.tutorial = tutorial + part.title = data["title"] + part.position_in_tutorial = tutorial.get_parts().count() + 1 + part.save() + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part.save() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + + maj_repo_part( + request, + new_slug_path=new_slug, + part=part, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + if "submit_continue" in request.POST: + form = PartForm() + messages.success(request, + _(u'La partie « {0} » a été ajoutée ' + u'avec succès.').format(part.title)) + else: + return redirect(part.get_absolute_url()) + else: + form = PartForm() + return render(request, "tutorial/part/new.html", {"tutorial": tutorial, + "form": form}) + + +@can_write_and_read_now +@login_required +def modify_part(request): + """Modifiy the given part.""" + + if not request.method == "POST": + raise Http404 + part_pk = request.POST["part"] + part = get_object_or_404(Part, pk=part_pk) + + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if "move" in request.POST: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + # Invalid conversion, maybe the user played with the move button + return redirect(part.tutorial.get_absolute_url()) + + move(part, new_pos, "position_in_tutorial", "tutorial", "get_parts") + part.save() + + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) + + maj_repo_tuto(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + tuto=part.tutorial, + action="maj", + msg=_(u"Déplacement de la partie {} ").format(part.title)) + elif "delete" in request.POST: + # Delete all chapters belonging to the part + + Chapter.objects.all().filter(part=part).delete() + + # Move other parts + + old_pos = part.position_in_tutorial + for tut_p in part.tutorial.get_parts(): + if old_pos <= tut_p.position_in_tutorial: + tut_p.position_in_tutorial = tut_p.position_in_tutorial - 1 + tut_p.save() + old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + maj_repo_part(request, old_slug_path=old_slug, part=part, action="del") + + new_slug_tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) + # Actually delete the part + part.delete() + + maj_repo_tuto(request, + old_slug_path=new_slug_tuto_path, + new_slug_path=new_slug_tuto_path, + tuto=part.tutorial, + action="maj", + msg=_(u"Suppression de la partie {} ").format(part.title)) + return redirect(part.tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_part(request): + """Edit the given part.""" + + try: + part_pk = int(request.GET["partie"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + part = get_object_or_404(Part, pk=part_pk) + introduction = os.path.join(part.get_path(), "introduction.md") + conclusion = os.path.join(part.get_path(), "conclusion.md") + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = PartForm(request.POST) + if form.is_valid(): + data = form.data + # avoid collision + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = PartForm({"title": part.title, + "introduction": part.get_introduction(), + "conclusion": part.get_conclusion()}) + return render(request, "tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) + # Update title and his slug. + + part.title = data["title"] + old_slug = part.get_path() + part.save() + + # Update path for introduction and conclusion. + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part.save() + part.update_children() + + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + + maj_repo_part( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + part=part, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(part.get_absolute_url()) + else: + form = PartForm({"title": part.title, + "introduction": part.get_introduction(), + "conclusion": part.get_conclusion()}) + return render(request, "tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "form": form + }) + + +# Chapters. + + +@login_required +def view_chapter( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, + chapter_pk, + chapter_slug, +): + """View chapter.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + try: + sha = request.GET["version"] + except KeyError: + sha = tutorial.sha_draft + + is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + + # Only authors of the tutorial and staff can view tutorial in offline. + + if request.user not in tutorial.authors.all() and not is_beta: + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + # find the good manifest file + + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + tutorial.load_dic(mandata, sha=sha) + + parts = mandata["parts"] + cpt_p = 1 + final_chapter = None + chapter_tab = [] + final_position = 0 + find = False + for part in parts: + cpt_c = 1 + part["slug"] = slugify(part["title"]) + part["get_absolute_url"] = reverse( + "zds.tutorial.views.view_part", + args=[ + tutorial.pk, + tutorial.slug, + part["pk"], + part["slug"]]) + part["tutorial"] = tutorial + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + chapter["get_absolute_url"] = part["get_absolute_url"] \ + + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) + if chapter_pk == str(chapter["pk"]): + find = True + chapter["intro"] = get_blob(repo.commit(sha).tree, + chapter["introduction"]) + chapter["conclu"] = get_blob(repo.commit(sha).tree, + chapter["conclusion"]) + + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt_e += 1 + chapter_tab.append(chapter) + if chapter_pk == str(chapter["pk"]): + final_chapter = chapter + final_position = len(chapter_tab) - 1 + cpt_c += 1 + cpt_p += 1 + + # if chapter can't find + if not find: + raise Http404 + + prev_chapter = (chapter_tab[final_position - 1] if final_position + > 0 else None) + next_chapter = (chapter_tab[final_position + 1] if final_position + 1 + < len(chapter_tab) else None) + + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + + return render(request, "tutorial/chapter/view.html", { + "tutorial": mandata, + "chapter": final_chapter, + "prev": prev_chapter, + "next": next_chapter, + "version": sha, + "is_js": is_js + }) + + +def view_chapter_online( + request, + tutorial_pk, + tutorial_slug, + part_pk, + part_slug, + chapter_pk, + chapter_slug, +): + """View chapter.""" + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + if not tutorial.on_line(): + raise Http404 + + # find the good manifest file + + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata, sha=tutorial.sha_public) + mandata["update"] = tutorial.update + + mandata['get_parts'] = mandata["parts"] + parts = mandata["parts"] + cpt_p = 1 + final_chapter = None + chapter_tab = [] + final_position = 0 + + find = False + for part in parts: + cpt_c = 1 + part["slug"] = slugify(part["title"]) + part["get_absolute_url_online"] = reverse( + "zds.tutorial.views.view_part_online", + args=[ + tutorial.pk, + tutorial.slug, + part["pk"], + part["slug"]]) + part["tutorial"] = mandata + part["position_in_tutorial"] = cpt_p + part["get_chapters"] = part["chapters"] + for chapter in part["chapters"]: + chapter["part"] = part + chapter["path"] = tutorial.get_prod_path() + chapter["slug"] = slugify(chapter["title"]) + chapter["type"] = "BIG" + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + chapter["get_absolute_url_online"] = part[ + "get_absolute_url_online"] + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) + if chapter_pk == str(chapter["pk"]): + find = True + intro = open( + os.path.join( + tutorial.get_prod_path(), + chapter["introduction"] + + ".html"), + "r") + chapter["intro"] = intro.read() + intro.close() + conclu = open( + os.path.join( + tutorial.get_prod_path(), + chapter["conclusion"] + + ".html"), + "r") + chapter["conclu"] = conclu.read() + conclu.close() + cpt_e = 1 + for ext in chapter["extracts"]: + ext["chapter"] = chapter + ext["position_in_chapter"] = cpt_e + ext["path"] = tutorial.get_path() + text = open(os.path.join(tutorial.get_prod_path(), + ext["text"] + ".html"), "r") + ext["txt"] = text.read() + text.close() + cpt_e += 1 + else: + intro = None + conclu = None + chapter_tab.append(chapter) + if chapter_pk == str(chapter["pk"]): + final_chapter = chapter + final_position = len(chapter_tab) - 1 + cpt_c += 1 + cpt_p += 1 + + # if chapter can't find + if not find: + raise Http404 + + prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None) + next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) + + return render(request, "tutorial/chapter/view_online.html", { + "chapter": final_chapter, + "parts": parts, + "prev": prev_chapter, + "next": next_chapter, + }) + + +@can_write_and_read_now +@login_required +def add_chapter(request): + """Add a new chapter to given part.""" + + try: + part_pk = request.GET["partie"] + except KeyError: + raise Http404 + part = get_object_or_404(Part, pk=part_pk) + + # Make sure the user is allowed to do that + + if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + form = ChapterForm(request.POST, request.FILES) + if form.is_valid(): + data = form.data + chapter = Chapter() + chapter.title = data["title"] + chapter.part = part + chapter.position_in_part = part.get_chapters().count() + 1 + chapter.update_position_in_tutorial() + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = part.tutorial.gallery + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + chapter.image = img + + chapter.save() + if chapter.tutorial: + chapter_path = os.path.join( + os.path.join( + settings.ZDS_APP['tutorial']['repo_path'], + chapter.tutorial.get_phy_slug()), + chapter.get_phy_slug()) + chapter.introduction = os.path.join(chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join(chapter.get_phy_slug(), + "conclusion.md") + else: + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + chapter.introduction = os.path.join( + chapter.part.get_phy_slug(), + chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug(), "conclusion.md") + chapter.save() + maj_repo_chapter( + request, + new_slug_path=chapter_path, + chapter=chapter, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="add", + msg=request.POST.get('msg_commit', None) + ) + if "submit_continue" in request.POST: + form = ChapterForm() + messages.success(request, + _(u'Le chapitre « {0} » a été ajouté ' + u'avec succès.').format(chapter.title)) + else: + return redirect(chapter.get_absolute_url()) + else: + form = ChapterForm() + + return render(request, "tutorial/chapter/new.html", {"part": part, + "form": form}) + + +@can_write_and_read_now +@login_required +def modify_chapter(request): + """Modify the given chapter.""" + + if not request.method == "POST": + raise Http404 + data = request.POST + try: + chapter_pk = request.POST["chapter"] + except KeyError: + raise Http404 + chapter = get_object_or_404(Chapter, pk=chapter_pk) + + # Make sure the user is allowed to do that + + if request.user not in chapter.get_tutorial().authors.all() and \ + not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if "move" in data: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + + # User misplayed with the move button + + return redirect(chapter.get_absolute_url()) + move(chapter, new_pos, "position_in_part", "part", "get_chapters") + chapter.update_position_in_tutorial() + chapter.save() + + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug()) + + maj_repo_part(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + part=chapter.part, + action="maj", + msg=_(u"Déplacement du chapitre {}").format(chapter.title)) + messages.info(request, _(u"Le chapitre a bien été déplacé.")) + elif "delete" in data: + old_pos = chapter.position_in_part + old_tut_pos = chapter.position_in_tutorial + + if chapter.part: + parent = chapter.part + else: + parent = chapter.tutorial + + # Move other chapters first + + for tut_c in chapter.part.get_chapters(): + if old_pos <= tut_c.position_in_part: + tut_c.position_in_part = tut_c.position_in_part - 1 + tut_c.save() + maj_repo_chapter(request, chapter=chapter, + old_slug_path=chapter.get_path(), action="del") + + # Then delete the chapter + new_slug_path_part = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug()) + chapter.delete() + + # Update all the position_in_tutorial fields for the next chapters + + for tut_c in \ + Chapter.objects.filter(position_in_tutorial__gt=old_tut_pos): + tut_c.update_position_in_tutorial() + tut_c.save() + + maj_repo_part(request, + old_slug_path=new_slug_path_part, + new_slug_path=new_slug_path_part, + part=chapter.part, + action="maj", + msg=_(u"Suppression du chapitre {}").format(chapter.title)) + messages.info(request, _(u"Le chapitre a bien été supprimé.")) + + return redirect(parent.get_absolute_url()) + + return redirect(chapter.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_chapter(request): + """Edit the given chapter.""" + + try: + chapter_pk = int(request.GET["chapitre"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + chapter = get_object_or_404(Chapter, pk=chapter_pk) + big = chapter.part + small = chapter.tutorial + + # Make sure the user is allowed to do that + + if (big and request.user not in chapter.part.tutorial.authors.all() + or small and request.user not in chapter.tutorial.authors.all())\ + and not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + introduction = os.path.join(chapter.get_path(), "introduction.md") + conclusion = os.path.join(chapter.get_path(), "conclusion.md") + if request.method == "POST": + + if chapter.part: + form = ChapterForm(request.POST, request.FILES) + gal = chapter.part.tutorial.gallery + else: + form = EmbdedChapterForm(request.POST, request.FILES) + gal = chapter.tutorial.gallery + if form.is_valid(): + data = form.data + # avoid collision + if content_has_changed([introduction, conclusion], data["last_hash"]): + form = render_chapter_form(chapter) + return render(request, "tutorial/part/edit.html", + { + "chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), + "new_version": True, + "form": form + }) + chapter.title = data["title"] + + old_slug = chapter.get_path() + chapter.save() + chapter.update_children() + + if chapter.part: + if chapter.tutorial: + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.tutorial.get_phy_slug(), + chapter.get_phy_slug()) + else: + new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + # Create image + + if "image" in request.FILES: + img = Image() + img.physical = request.FILES["image"] + img.gallery = gal + img.title = request.FILES["image"] + img.slug = slugify(request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + chapter.image = img + maj_repo_chapter( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + chapter=chapter, + introduction=data["introduction"], + conclusion=data["conclusion"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(chapter.get_absolute_url()) + else: + form = render_chapter_form(chapter) + return render(request, "tutorial/chapter/edit.html", {"chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), + "form": form}) + + +@login_required +def add_extract(request): + """Add extract.""" + + try: + chapter_pk = int(request.GET["chapitre"]) + except KeyError: + raise Http404 + except ValueError: + raise Http404 + + chapter = get_object_or_404(Chapter, pk=chapter_pk) + part = chapter.part + + # If part exist, we check if the user is in authors of the tutorial of the + # part or If part doesn't exist, we check if the user is in authors of the + # tutorial of the chapter. + + if part and request.user not in chapter.part.tutorial.authors.all() \ + or not part and request.user not in chapter.tutorial.authors.all(): + + # If the user isn't an author or a staff, we raise an exception. + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + if request.method == "POST": + data = request.POST + + # Using the « preview button » + + if "preview" in data: + form = ExtractForm(initial={"title": data["title"], + "text": data["text"], + 'msg_commit': data['msg_commit']}) + return render(request, "tutorial/extract/new.html", + {"chapter": chapter, "form": form}) + else: + + # Save extract. + + form = ExtractForm(request.POST) + if form.is_valid(): + data = form.data + extract = Extract() + extract.chapter = chapter + extract.position_in_chapter = chapter.get_extract_count() + 1 + extract.title = data["title"] + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract(request, new_slug_path=extract.get_path(), + extract=extract, text=data["text"], + action="add", + msg=request.POST.get('msg_commit', None)) + return redirect(extract.get_absolute_url()) + else: + form = ExtractForm() + + return render(request, "tutorial/extract/new.html", {"chapter": chapter, + "form": form}) + + +@can_write_and_read_now +@login_required +def edit_extract(request): + """Edit extract.""" + try: + extract_pk = request.GET["extrait"] + except KeyError: + raise Http404 + extract = get_object_or_404(Extract, pk=extract_pk) + part = extract.chapter.part + + # If part exist, we check if the user is in authors of the tutorial of the + # part or If part doesn't exist, we check if the user is in authors of the + # tutorial of the chapter. + + if part and request.user \ + not in extract.chapter.part.tutorial.authors.all() or not part \ + and request.user not in extract.chapter.tutorial.authors.all(): + + # If the user isn't an author or a staff, we raise an exception. + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + if request.method == "POST": + data = request.POST + # Using the « preview button » + + if "preview" in data: + form = ExtractForm(initial={ + "title": data["title"], + "text": data["text"], + 'msg_commit': data['msg_commit'] + }) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, "form": form, + "last_hash": compute_hash([extract.get_path()]) + }) + else: + if content_has_changed([extract.get_path()], data["last_hash"]): + form = ExtractForm(initial={ + "title": extract.title, + "text": extract.get_text(), + 'msg_commit': data['msg_commit']}) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "new_version": True, + "form": form + }) + # Edit extract. + + form = ExtractForm(request.POST) + if form.is_valid(): + data = form.data + old_slug = extract.get_path() + extract.title = data["title"] + extract.text = extract.get_path(relative=True) + + # Use path retrieve before and use it to create the new slug. + extract.save() + new_slug = extract.get_path() + + maj_repo_extract( + request, + old_slug_path=old_slug, + new_slug_path=new_slug, + extract=extract, + text=data["text"], + action="maj", + msg=request.POST.get('msg_commit', None) + ) + return redirect(extract.get_absolute_url()) + else: + form = ExtractForm({"title": extract.title, + "text": extract.get_text()}) + return render(request, "tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "form": form + }) + + +@can_write_and_read_now +def modify_extract(request): + if not request.method == "POST": + raise Http404 + data = request.POST + try: + extract_pk = request.POST["extract"] + except KeyError: + raise Http404 + extract = get_object_or_404(Extract, pk=extract_pk) + chapter = extract.chapter + if "delete" in data: + pos_current_extract = extract.position_in_chapter + for extract_c in extract.chapter.get_extracts(): + if pos_current_extract <= extract_c.position_in_chapter: + extract_c.position_in_chapter = extract_c.position_in_chapter \ + - 1 + extract_c.save() + + # Use path retrieve before and use it to create the new slug. + + old_slug = extract.get_path() + + if extract.chapter.tutorial: + new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug()) + else: + new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + maj_repo_extract(request, old_slug_path=old_slug, extract=extract, + action="del") + + maj_repo_chapter(request, + old_slug_path=new_slug_path_chapter, + new_slug_path=new_slug_path_chapter, + chapter=chapter, + action="maj", + msg=_(u"Suppression de l'extrait {}").format(extract.title)) + return redirect(chapter.get_absolute_url()) + elif "move" in data: + try: + new_pos = int(request.POST["move_target"]) + except ValueError: + # Error, the user misplayed with the move button + return redirect(extract.get_absolute_url()) + + move(extract, new_pos, "position_in_chapter", "chapter", "get_extracts") + extract.save() + + if extract.chapter.tutorial: + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug()) + else: + new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + + maj_repo_chapter(request, + old_slug_path=new_slug_path, + new_slug_path=new_slug_path, + chapter=chapter, + action="maj", + msg=_(u"Déplacement de l'extrait {}").format(extract.title)) + return redirect(extract.get_absolute_url()) + raise Http404 + + +def find_tuto(request, pk_user): + try: + type = request.GET["type"] + except KeyError: + type = None + display_user = get_object_or_404(User, pk=pk_user) + if type == "beta": + tutorials = Tutorial.objects.all().filter( + authors__in=[display_user], + sha_beta__isnull=False).exclude(sha_beta="").order_by("-pubdate") + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public(sha=tutorial.sha_beta) + tutorial.load_dic(mandata, sha=tutorial.sha_beta) + tuto_versions.append(mandata) + + return render(request, "tutorial/member/beta.html", + {"tutorials": tuto_versions, "usr": display_user}) + else: + tutorials = Tutorial.objects.all().filter( + authors__in=[display_user], + sha_public__isnull=False).exclude(sha_public="").order_by("-pubdate") + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public() + tutorial.load_dic(mandata) + tuto_versions.append(mandata) + + return render(request, "tutorial/member/online.html", {"tutorials": tuto_versions, + "usr": display_user}) + + +def upload_images(images, tutorial): + mapping = OrderedDict() + + # download images + + zfile = zipfile.ZipFile(images, "a") + os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + for i in zfile.namelist(): + ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + try: + data = zfile.read(i) + fp = open(ph_temp, "wb") + fp.write(data) + fp.close() + f = File(open(ph_temp, "rb")) + f.name = os.path.basename(i) + pic = Image() + pic.gallery = tutorial.gallery + pic.title = os.path.basename(i) + pic.pubdate = datetime.now() + pic.physical = f + pic.save() + mapping[i] = pic.physical.url + f.close() + except IOError: + try: + os.makedirs(ph_temp) + except OSError: + pass + zfile.close() + return mapping + + +def replace_real_url(md_text, dict): + for (dt_old, dt_new) in dict.iteritems(): + md_text = md_text.replace(dt_old, dt_new) + return md_text + + +def import_content( + request, + tuto, + images, + logo, +): + tutorial = Tutorial() + + # add create date + + tutorial.create_at = datetime.now() + tree = etree.parse(tuto) + racine_big = tree.xpath("/bigtuto") + racine_mini = tree.xpath("/minituto") + if len(racine_big) > 0: + + # it's a big tuto + + tutorial.type = "BIG" + tutorial_title = tree.xpath("/bigtuto/titre")[0] + tutorial_intro = tree.xpath("/bigtuto/introduction")[0] + tutorial_conclu = tree.xpath("/bigtuto/conclusion")[0] + tutorial.title = tutorial_title.text.strip() + tutorial.description = tutorial_title.text.strip() + tutorial.images = "images" + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + + # Creating the gallery + + gal = Gallery() + gal.title = tutorial_title.text + gal.slug = slugify(tutorial_title.text) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + tutorial.save() + tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + mapping = upload_images(images, tutorial) + maj_repo_tuto( + request, + new_slug_path=tuto_path, + tuto=tutorial, + introduction=replace_real_url(tutorial_intro.text, mapping), + conclusion=replace_real_url(tutorial_conclu.text, mapping), + action="add", + ) + tutorial.authors.add(request.user) + part_count = 1 + for partie in tree.xpath("/bigtuto/parties/partie"): + part_title = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/titre")[0] + part_intro = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/introduction")[0] + part_conclu = tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/conclusion")[0] + part = Part() + part.title = part_title.text.strip() + part.position_in_tutorial = part_count + part.tutorial = tutorial + part.save() + part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") + part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") + part_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + part.tutorial.get_phy_slug(), + part.get_phy_slug()) + part.save() + maj_repo_part( + request, + None, + part_path, + part, + replace_real_url(part_intro.text, mapping), + replace_real_url(part_conclu.text, mapping), + action="add", + ) + chapter_count = 1 + for chapitre in tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre"): + chapter_title = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/titre")[0] + chapter_intro = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/introduction")[0] + chapter_conclu = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/conclusion")[0] + chapter = Chapter() + chapter.title = chapter_title.text.strip() + chapter.position_in_part = chapter_count + chapter.position_in_tutorial = part_count * chapter_count + chapter.part = part + chapter.save() + chapter.introduction = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + "introduction.md") + chapter.conclusion = os.path.join( + part.get_phy_slug(), + chapter.get_phy_slug(), + "conclusion.md") + chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + chapter.part.tutorial.get_phy_slug(), + chapter.part.get_phy_slug(), + chapter.get_phy_slug()) + chapter.save() + maj_repo_chapter( + request, + new_slug_path=chapter_path, + chapter=chapter, + introduction=replace_real_url(chapter_intro.text, + mapping), + conclusion=replace_real_url(chapter_conclu.text, mapping), + action="add", + ) + extract_count = 1 + for souspartie in tree.xpath("/bigtuto/parties/partie[" + + str(part_count) + "]/chapitres/chapitre[" + + str(chapter_count) + "]/sousparties/souspartie"): + extract_title = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/sousparties/souspartie[" + + str(extract_count) + + "]/titre")[0] + extract_text = tree.xpath( + "/bigtuto/parties/partie[" + + str(part_count) + + "]/chapitres/chapitre[" + + str(chapter_count) + + "]/sousparties/souspartie[" + + str(extract_count) + + "]/texte")[0] + extract = Extract() + extract.title = extract_title.text.strip() + extract.position_in_chapter = extract_count + extract.chapter = chapter + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract( + request, + new_slug_path=extract.get_path(), + extract=extract, + text=replace_real_url( + extract_text.text, + mapping), + action="add") + extract_count += 1 + chapter_count += 1 + part_count += 1 + elif len(racine_mini) > 0: + + # it's a mini tuto + + tutorial.type = "MINI" + tutorial_title = tree.xpath("/minituto/titre")[0] + tutorial_intro = tree.xpath("/minituto/introduction")[0] + tutorial_conclu = tree.xpath("/minituto/conclusion")[0] + tutorial.title = tutorial_title.text.strip() + tutorial.description = tutorial_title.text.strip() + tutorial.images = "images" + tutorial.introduction = "introduction.md" + tutorial.conclusion = "conclusion.md" + + # Creating the gallery + + gal = Gallery() + gal.title = tutorial_title.text + gal.slug = slugify(tutorial_title.text) + gal.pubdate = datetime.now() + gal.save() + + # Attach user to gallery + + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = request.user + userg.save() + tutorial.gallery = gal + tutorial.save() + tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + mapping = upload_images(images, tutorial) + maj_repo_tuto( + request, + new_slug_path=tuto_path, + tuto=tutorial, + introduction=replace_real_url(tutorial_intro.text, mapping), + conclusion=replace_real_url(tutorial_conclu.text, mapping), + action="add", + ) + tutorial.authors.add(request.user) + chapter = Chapter() + chapter.tutorial = tutorial + chapter.save() + extract_count = 1 + for souspartie in tree.xpath("/minituto/sousparties/souspartie"): + extract_title = tree.xpath("/minituto/sousparties/souspartie[" + + str(extract_count) + "]/titre")[0] + extract_text = tree.xpath("/minituto/sousparties/souspartie[" + + str(extract_count) + "]/texte")[0] + extract = Extract() + extract.title = extract_title.text.strip() + extract.position_in_chapter = extract_count + extract.chapter = chapter + extract.save() + extract.text = extract.get_path(relative=True) + extract.save() + maj_repo_extract(request, new_slug_path=extract.get_path(), + extract=extract, + text=replace_real_url(extract_text.text, + mapping), action="add") + extract_count += 1 + + +@can_write_and_read_now +@login_required +@require_POST +def local_import(request): + import_content(request, request.POST["tuto"], request.POST["images"], + request.POST["logo"]) + return redirect(reverse("zds.member.views.tutorials")) + + +@can_write_and_read_now +@login_required +def import_tuto(request): + if request.method == "POST": + # for import tuto + if "import-tuto" in request.POST: + form = ImportForm(request.POST, request.FILES) + if form.is_valid(): + import_content(request, request.FILES["file"], request.FILES["images"], "") + return redirect(reverse("zds.member.views.tutorials")) + else: + form_archive = ImportArchiveForm(user=request.user) + + elif "import-archive" in request.POST: + form_archive = ImportArchiveForm(request.user, request.POST, request.FILES) + if form_archive.is_valid(): + (check, reason) = import_archive(request) + if not check: + messages.error(request, reason) + else: + messages.success(request, reason) + return redirect(reverse("zds.member.views.tutorials")) + else: + form = ImportForm() + + else: + form = ImportForm() + form_archive = ImportArchiveForm(user=request.user) + + profile = get_object_or_404(Profile, user=request.user) + oldtutos = [] + if profile.sdz_tutorial: + olds = profile.sdz_tutorial.strip().split(":") + else: + olds = [] + for old in olds: + oldtutos.append(get_info_old_tuto(old)) + return render( + request, + "tutorial/tutorial/import.html", + {"form": form, "form_archive": form_archive, "old_tutos": oldtutos} + ) + + +# Handling repo +def maj_repo_tuto( + request, + old_slug_path=None, + new_slug_path=None, + tuto=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + if action == "del": + shutil.rmtree(old_slug_path) + else: + if action == "maj": + if old_slug_path != new_slug_path: + shutil.move(old_slug_path, new_slug_path) + repo = Repo(new_slug_path) + msg = _(u"Modification du tutoriel : «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + repo = Repo.init(new_slug_path, bare=False) + msg = _(u"Création du tutoriel «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg)).strip() + repo = Repo(new_slug_path) + index = repo.index + man_path = os.path.join(new_slug_path, "manifest.json") + tuto.dump_json(path=man_path) + index.add(["manifest.json"]) + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + index.add(["introduction.md"]) + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + index.add(["conclusion.md"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + tuto.sha_draft = com.hexsha + tuto.save() + + +def maj_repo_part( + request, + old_slug_path=None, + new_slug_path=None, + part=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + repo = Repo(part.tutorial.get_path()) + index = repo.index + if action == "del": + shutil.rmtree(old_slug_path) + msg = _(u"Suppresion de la partie : «{}»").format(part.title) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + + msg = _(u"Modification de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + index.add([part.get_phy_slug()]) + man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + part.tutorial.dump_json(path=man_path) + index.add(["manifest.json"]) + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + )]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['litteral_name']) + com_part = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + part.tutorial.sha_draft = com_part.hexsha + part.tutorial.save() + part.save() + + +def maj_repo_chapter( + request, + old_slug_path=None, + new_slug_path=None, + chapter=None, + introduction=None, + conclusion=None, + action=None, + msg=None, +): + + if chapter.tutorial: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.tutorial.get_phy_slug())) + ph = None + else: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug())) + ph = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug()) + index = repo.index + if action == "del": + shutil.rmtree(old_slug_path) + msg = _(u"Suppresion du chapitre : «{}»").format(chapter.title) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + if chapter.tutorial: + msg = _(u"Modification du tutoriel «{}» " + u"{} {}").format(chapter.tutorial.title, get_sep(msg), get_text_is_empty(msg)).strip() + else: + msg = _(u"Modification du chapitre «{}» " + u"{} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg)).strip() + elif action == "add": + if not os.path.exists(new_slug_path): + os.makedirs(new_slug_path, mode=0o777) + msg = _(u"Création du chapitre «{}» {} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + if introduction is not None: + intro = open(os.path.join(new_slug_path, "introduction.md"), "w") + intro.write(smart_str(introduction).strip()) + intro.close() + if conclusion is not None: + conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") + conclu.write(smart_str(conclusion).strip()) + conclu.close() + if ph is not None: + index.add([ph]) + + # update manifest + + if chapter.tutorial: + man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + chapter.tutorial.dump_json(path=man_path) + else: + man_path = os.path.join(chapter.part.tutorial.get_path(), + "manifest.json") + chapter.part.tutorial.dump_json(path=man_path) + index.add(["manifest.json"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com_ch = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + if chapter.tutorial: + chapter.tutorial.sha_draft = com_ch.hexsha + chapter.tutorial.save() + else: + chapter.part.tutorial.sha_draft = com_ch.hexsha + chapter.part.tutorial.save() + chapter.save() + + +def maj_repo_extract( + request, + old_slug_path=None, + new_slug_path=None, + extract=None, + text=None, + action=None, + msg=None, +): + + if extract.chapter.tutorial: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.tutorial.get_phy_slug())) + else: + repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], + extract.chapter.part.tutorial.get_phy_slug())) + index = repo.index + + chap = extract.chapter + + if action == "del": + msg = _(u"Suppression de l'extrait : «{}»").format(extract.title) + extract.delete() + if old_slug_path: + os.remove(old_slug_path) + else: + if action == "maj": + if old_slug_path != new_slug_path: + os.rename(old_slug_path, new_slug_path) + msg = _(u"Mise à jour de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + elif action == "add": + msg = _(u"Création de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ + .strip() + ext = open(new_slug_path, "w") + ext.write(smart_str(text).strip()) + ext.close() + index.add([extract.get_path(relative=True)]) + + # update manifest + if chap.tutorial: + man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + chap.tutorial.dump_json(path=man_path) + else: + man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + chap.part.tutorial.dump_json(path=man_path) + + index.add(["manifest.json"]) + aut_user = str(request.user.pk) + aut_email = str(request.user.email) + if aut_email is None or aut_email.strip() == "": + aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) + com_ex = index.commit( + msg, + author=Actor( + aut_user, + aut_email), + committer=Actor( + aut_user, + aut_email)) + if chap.tutorial: + chap.tutorial.sha_draft = com_ex.hexsha + chap.tutorial.save() + else: + chap.part.tutorial.sha_draft = com_ex.hexsha + chap.part.tutorial.save() + + +def insert_into_zip(zip_file, git_tree): + """recursively add files from a git_tree to a zip archive""" + for blob in git_tree.blobs: # first, add files : + zip_file.writestr(blob.path, blob.data_stream.read()) + if len(git_tree.trees) is not 0: # then, recursively add dirs : + for subtree in git_tree.trees: + insert_into_zip(zip_file, subtree) + + +def download(request): + """Download a tutorial.""" + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + + repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) + repo = Repo(repo_path) + sha = tutorial.sha_draft + if 'online' in request.GET and tutorial.on_line(): + sha = tutorial.sha_public + elif request.user not in tutorial.authors.all(): + if not request.user.has_perm('tutorial.change_tutorial'): + raise PermissionDenied # Only authors can download draft version + zip_path = os.path.join(tempfile.gettempdir(), tutorial.slug + '.zip') + zip_file = zipfile.ZipFile(zip_path, 'w') + insert_into_zip(zip_file, repo.commit(sha).tree) + zip_file.close() + response = HttpResponse(open(zip_path, "rb").read(), content_type="application/zip") + response["Content-Disposition"] = "attachment; filename={0}.zip".format(tutorial.slug) + os.remove(zip_path) + return response + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +def download_markdown(request): + """Download a markdown tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".md") + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/txt") + response["Content-Disposition"] = \ + "attachment; filename={0}.md".format(tutorial.slug) + return response + + +def download_html(request): + """Download a pdf tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".html") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="text/html") + response["Content-Disposition"] = \ + "attachment; filename={0}.html".format(tutorial.slug) + return response + + +def download_pdf(request): + """Download a pdf tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".pdf") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/pdf") + response["Content-Disposition"] = \ + "attachment; filename={0}.pdf".format(tutorial.slug) + return response + + +def download_epub(request): + """Download an epub tutorial.""" + + tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + phy_path = os.path.join( + tutorial.get_prod_path(), + tutorial.slug + + ".epub") + if not os.path.isfile(phy_path): + raise Http404 + response = HttpResponse( + open(phy_path, "rb").read(), + content_type="application/epub") + response["Content-Disposition"] = \ + "attachment; filename={0}.epub".format(tutorial.slug) + return response + + +def get_url_images(md_text, pt): + """find images urls in markdown text and download this.""" + + regex = ur"(!\[.*?\]\()(.+?)(\))" + unknow_path = os.path.join(settings.SITE_ROOT, "fixtures", "noir_black.png") + + # if text is empty don't download + + if md_text is not None: + imgs = re.findall(regex, md_text) + for img in imgs: + real_url = img[1] + # decompose images + parse_object = urlparse(real_url) + if parse_object.query != '': + resp = parse_qs(urlparse(img[1]).query, keep_blank_values=True) + real_url = resp["u"][0] + parse_object = urlparse(real_url) + + # if link is http type + if parse_object.scheme in ["http", "https", "ftp"] or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + (filepath, filename) = os.path.split(parse_object.path) + if not os.path.isdir(os.path.join(pt, "images")): + os.makedirs(os.path.join(pt, "images")) + + # download image + down_path = os.path.abspath(os.path.join(pt, "images", filename)) + try: + urlretrieve(real_url, down_path) + try: + ext = filename.split(".")[-1] + im = ImagePIL.open(down_path) + # if image is gif, convert to png + if ext == "gif": + im.save(os.path.join(pt, "images", filename.split(".")[0] + ".png")) + except IOError: + ext = filename.split(".")[-1] + im = ImagePIL.open(unknow_path) + if ext == "gif": + im.save(os.path.join(pt, "images", filename.split(".")[0] + ".png")) + else: + im.save(os.path.join(pt, "images", filename)) + except IOError: + pass + else: + # relative link + srcfile = settings.SITE_ROOT + real_url + if os.path.isfile(srcfile): + dstroot = pt + real_url + dstdir = os.path.dirname(dstroot) + if not os.path.exists(dstdir): + os.makedirs(dstdir) + shutil.copy(srcfile, dstroot) + + try: + ext = dstroot.split(".")[-1] + im = ImagePIL.open(dstroot) + # if image is gif, convert to png + if ext == "gif": + im.save(os.path.join(dstroot.split(".")[0] + ".png")) + except IOError: + ext = dstroot.split(".")[-1] + im = ImagePIL.open(unknow_path) + if ext == "gif": + im.save(os.path.join(dstroot.split(".")[0] + ".png")) + else: + im.save(os.path.join(dstroot)) + + +def sub_urlimg(g): + start = g.group("start") + url = g.group("url") + parse_object = urlparse(url) + if parse_object.query != '': + resp = parse_qs(urlparse(url).query, keep_blank_values=True) + parse_object = urlparse(resp["u"][0]) + (filepath, filename) = os.path.split(parse_object.path) + if filename != '': + mark = g.group("mark") + ext = filename.split(".")[-1] + if ext == "gif": + if parse_object.scheme in ("http", "https") or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + url = os.path.join("images", filename.split(".")[0] + ".png") + else: + url = (url.split(".")[0])[1:] + ".png" + else: + if parse_object.scheme in ("http", "https") or \ + parse_object.netloc[:3] == "www" or \ + parse_object.path[:3] == "www": + url = os.path.join("images", filename) + else: + url = url[1:] + end = g.group("end") + return start + mark + url + end + else: + return start + + +def markdown_to_out(md_text): + return re.sub(ur"(?P)(?P!\[.*?\]\()(?P.+?)(?P\))", sub_urlimg, + md_text) + + +def mep(tutorial, sha): + (output, err) = (None, None) + repo = Repo(tutorial.get_path()) + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + tutorial_version = json_reader.loads(manifest) + + prod_path = tutorial.get_prod_path(sha) + + pattern = os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], str(tutorial.pk) + '_*') + del_paths = glob.glob(pattern) + for del_path in del_paths: + if os.path.isdir(del_path): + try: + shutil.rmtree(del_path) + except OSError: + shutil.rmtree(u"\\\\?\{0}".format(del_path)) + # WARNING: this can throw another OSError + shutil.copytree(tutorial.get_path(), prod_path) + repo.head.reset(commit=sha, index=True, working_tree=True) + + # collect md files + + fichiers = [] + fichiers.append(tutorial_version["introduction"]) + fichiers.append(tutorial_version["conclusion"]) + if "parts" in tutorial_version: + for part in tutorial_version["parts"]: + fichiers.append(part["introduction"]) + fichiers.append(part["conclusion"]) + if "chapters" in part: + for chapter in part["chapters"]: + fichiers.append(chapter["introduction"]) + fichiers.append(chapter["conclusion"]) + if "extracts" in chapter: + for extract in chapter["extracts"]: + fichiers.append(extract["text"]) + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if "extracts" in tutorial_version["chapter"]: + for extract in chapter["extracts"]: + fichiers.append(extract["text"]) + + # convert markdown file to html file + + for fichier in fichiers: + md_file_contenu = get_blob(repo.commit(sha).tree, fichier) + + # download images + + get_url_images(md_file_contenu, prod_path) + + # convert to out format + out_file = open(os.path.join(prod_path, fichier), "w") + if md_file_contenu is not None: + out_file.write(markdown_to_out(md_file_contenu.encode("utf-8"))) + out_file.close() + target = os.path.join(prod_path, fichier + ".html") + os.chdir(os.path.dirname(target)) + try: + html_file = open(target, "w") + except IOError: + + # handle limit of 255 on windows + + target = u"\\\\?\{0}".format(target) + html_file = open(target, "w") + if tutorial.js_support: + is_js = "js" + else: + is_js = "" + if md_file_contenu is not None: + html_file.write(emarkdown(md_file_contenu, is_js)) + html_file.close() + + # load markdown out + + contenu = export_tutorial_to_md(tutorial, sha).lstrip() + out_file = open(os.path.join(prod_path, tutorial.slug + ".md"), "w") + out_file.write(smart_str(contenu)) + out_file.close() + + # define whether to log pandoc's errors + + pandoc_debug_str = "" + if settings.PANDOC_LOG_STATE: + pandoc_debug_str = " 2>&1 | tee -a " + settings.PANDOC_LOG + + # load pandoc + + os.chdir(prod_path) + os.system(settings.PANDOC_LOC + + "pandoc --latex-engine=xelatex -s -S --toc " + + os.path.join(prod_path, tutorial.slug) + + ".md -o " + os.path.join(prod_path, + tutorial.slug) + ".html" + pandoc_debug_str) + os.system(settings.PANDOC_LOC + "pandoc " + settings.PANDOC_PDF_PARAM + " " + + os.path.join(prod_path, tutorial.slug) + ".md " + + "-o " + os.path.join(prod_path, tutorial.slug) + + ".pdf" + pandoc_debug_str) + os.system(settings.PANDOC_LOC + "pandoc -s -S --toc " + + os.path.join(prod_path, tutorial.slug) + + ".md -o " + os.path.join(prod_path, + tutorial.slug) + ".epub" + pandoc_debug_str) + os.chdir(settings.SITE_ROOT) + return (output, err) + + +def un_mep(tutorial): + del_paths = glob.glob(os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], + str(tutorial.pk) + '_*')) + for del_path in del_paths: + if os.path.isdir(del_path): + try: + shutil.rmtree(del_path) + except OSError: + shutil.rmtree(u"\\\\?\{0}".format(del_path)) + # WARNING: this can throw another OSError + + +@can_write_and_read_now +@login_required +def answer(request): + """Adds an answer from a user to an tutorial.""" + + try: + tutorial_pk = request.GET["tutorial"] + except KeyError: + raise Http404 + + # Retrieve current tutorial. + + tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + + # Making sure reactioning is allowed + + if tutorial.is_locked: + raise PermissionDenied + + # Check that the user isn't spamming + + if tutorial.antispam(request.user): + raise PermissionDenied + + # Retrieve 3 last notes of the current tutorial. + + notes = Note.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] + + # If there is a last notes for the tutorial, we save his pk. Otherwise, we + # save 0. + + last_note_pk = 0 + if tutorial.last_note: + last_note_pk = tutorial.last_note.pk + + # Retrieve lasts notes of the current tutorial. + notes = Note.objects.filter(tutorial=tutorial) \ + .prefetch_related() \ + .order_by("-pubdate")[:settings.ZDS_APP['forum']['posts_per_page']] + + # User would like preview his post or post a new note on the tutorial. + + if request.method == "POST": + data = request.POST + newnote = last_note_pk != int(data["last_note"]) + + # Using the « preview button », the « more » button or new note + + if "preview" in data or newnote: + form = NoteForm(tutorial, request.user, + initial={"text": data["text"]}) + if request.is_ajax(): + return HttpResponse(json.dumps({"text": emarkdown(data["text"])}), + content_type='application/json') + else: + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "last_note_pk": last_note_pk, + "newnote": newnote, + "notes": notes, + "form": form, + }) + else: + + # Saving the message + + form = NoteForm(tutorial, request.user, request.POST) + if form.is_valid(): + data = form.data + note = Note() + note.related_content = tutorial + note.author = request.user + note.text = data["text"] + note.text_html = emarkdown(data["text"]) + note.pubdate = datetime.now() + note.position = tutorial.get_note_count() + 1 + note.ip_address = get_client_ip(request) + note.save() + tutorial.last_note = note + tutorial.save() + return redirect(note.get_absolute_url()) + else: + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "last_note_pk": last_note_pk, + "newnote": newnote, + "notes": notes, + "form": form, + }) + else: + + # Actions from the editor render to answer.html. + text = "" + + # Using the quote button + + if "cite" in request.GET: + note_cite_pk = request.GET["cite"] + note_cite = Note.objects.get(pk=note_cite_pk) + if not note_cite.is_visible: + raise PermissionDenied + + for line in note_cite.text.splitlines(): + text = text + "> " + line + "\n" + + text = u'{0}Source:[{1}]({2}{3})'.format( + text, + note_cite.author.username, + settings.ZDS_APP['site']['url'], + note_cite.get_absolute_url()) + + form = NoteForm(tutorial, request.user, initial={"text": text}) + return render(request, "tutorial/comment/new.html", { + "tutorial": tutorial, + "notes": notes, + "last_note_pk": last_note_pk, + "form": form, + }) + + +@can_write_and_read_now +@login_required +@require_POST +@transaction.atomic +def solve_alert(request): + + # only staff can move topic + + if not request.user.has_perm("tutorial.change_note"): + raise PermissionDenied + + alert = get_object_or_404(Alert, pk=request.POST["alert_pk"]) + note = Note.objects.get(pk=alert.comment.id) + + if "text" in request.POST and request.POST["text"] != "": + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Vous recevez ce message car vous avez signalé le message de *{1}*, ' + u'dans le tutoriel [{2}]({3}). Votre alerte a été traitée par **{4}** ' + u'et il vous a laissé le message suivant :' + u'\n\n> {5}\n\nToute l\'équipe de la modération vous remercie !').format( + alert.author.username, + note.author.username, + note.tutorial.title, + settings.ZDS_APP['site']['url'] + note.get_absolute_url(), + request.user.username, + request.POST["text"],)) + send_mp( + bot, + [alert.author], + _(u"Résolution d'alerte : {0}").format(note.tutorial.title), + "", + msg, + False, + ) + alert.delete() + messages.success(request, _(u"L'alerte a bien été résolue.")) + return redirect(note.get_absolute_url()) + + +@login_required +@require_POST +def activ_js(request): + + # only for staff + + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + tutorial = get_object_or_404(Tutorial, pk=request.POST["tutorial"]) + tutorial.js_support = "js_support" in request.POST + tutorial.save() + + return redirect(tutorial.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def edit_note(request): + """Edit the given user's note.""" + + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + note = get_object_or_404(Note, pk=note_pk) + g_tutorial = None + if note.position >= 1: + g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) + + # Making sure the user is allowed to do that. Author of the note must to be + # the user logged. + + if note.author != request.user \ + and not request.user.has_perm("tutorial.change_note") \ + and "signal_message" not in request.POST: + raise PermissionDenied + if note.author != request.user and request.method == "GET" \ + and request.user.has_perm("tutorial.change_note"): + messages.add_message(request, messages.WARNING, + _(u'Vous éditez ce message en tant que ' + u'modérateur (auteur : {}). Soyez encore plus ' + u'prudent lors de l\'édition de ' + u'celui-ci !').format(note.author.username)) + note.alerts.all().delete() + if request.method == "POST": + if "delete_message" in request.POST: + if note.author == request.user \ + or request.user.has_perm("tutorial.change_note"): + note.alerts.all().delete() + note.is_visible = False + if request.user.has_perm("tutorial.change_note"): + note.text_hidden = request.POST["text_hidden"] + note.editor = request.user + if "show_message" in request.POST: + if request.user.has_perm("tutorial.change_note"): + note.is_visible = True + note.text_hidden = "" + if "signal_message" in request.POST: + alert = Alert() + alert.author = request.user + alert.comment = note + alert.scope = Alert.TUTORIAL + alert.text = request.POST["signal_text"] + alert.pubdate = datetime.now() + alert.save() + + # Using the preview button + if "preview" in request.POST: + form = NoteForm(g_tutorial, request.user, + initial={"text": request.POST["text"]}) + form.helper.form_action = reverse("zds.tutorial.views.edit_note") \ + + "?message=" + str(note_pk) + if request.is_ajax(): + return HttpResponse(json.dumps({"text": emarkdown(request.POST["text"])}), + content_type='application/json') + else: + return render(request, + "tutorial/comment/edit.html", + {"note": note, "tutorial": g_tutorial, "form": form}) + if "delete_message" not in request.POST and "signal_message" \ + not in request.POST and "show_message" not in request.POST: + + # The user just sent data, handle them + + if request.POST["text"].strip() != "": + note.text = request.POST["text"] + note.text_html = emarkdown(request.POST["text"]) + note.update = datetime.now() + note.editor = request.user + note.save() + return redirect(note.get_absolute_url()) + else: + form = NoteForm(g_tutorial, request.user, initial={"text": note.text}) + form.helper.form_action = reverse("zds.tutorial.views.edit_note") \ + + "?message=" + str(note_pk) + return render(request, "tutorial/comment/edit.html", {"note": note, "tutorial": g_tutorial, "form": form}) + + +@can_write_and_read_now +@login_required +def like_note(request): + """Like a note.""" + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + resp = {} + note = get_object_or_404(Note, pk=note_pk) + + user = request.user + if note.author.pk != request.user.pk: + + # Making sure the user is allowed to do that + + if CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() == 0: + like = CommentLike() + like.user = user + like.comments = note + note.like = note.like + 1 + note.save() + like.save() + if CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() > 0: + CommentDislike.objects.filter( + user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.dislike = note.dislike - 1 + note.save() + else: + CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.like = note.like - 1 + note.save() + resp["upvotes"] = note.like + resp["downvotes"] = note.dislike + if request.is_ajax(): + return HttpResponse(json_writer.dumps(resp)) + else: + return redirect(note.get_absolute_url()) + + +@can_write_and_read_now +@login_required +def dislike_note(request): + """Dislike a note.""" + + try: + note_pk = request.GET["message"] + except KeyError: + raise Http404 + resp = {} + note = get_object_or_404(Note, pk=note_pk) + user = request.user + if note.author.pk != request.user.pk: + + # Making sure the user is allowed to do that + + if CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() == 0: + dislike = CommentDislike() + dislike.user = user + dislike.comments = note + note.dislike = note.dislike + 1 + note.save() + dislike.save() + if CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() > 0: + CommentLike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.like = note.like - 1 + note.save() + else: + CommentDislike.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.dislike = note.dislike - 1 + note.save() + resp["upvotes"] = note.like + resp["downvotes"] = note.dislike + if request.is_ajax(): + return HttpResponse(json_writer.dumps(resp)) + else: + return redirect(note.get_absolute_url()) + + +def help_tutorial(request): + """fetch all tutorials that needs help""" + + # Retrieve type of the help. Default value is any help + type = request.GET.get('type', None) + + if type is not None: + aide = get_object_or_404(HelpWriting, slug=type) + tutos = Tutorial.objects.filter(helps=aide) \ + .all() + else: + tutos = Tutorial.objects.annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + + # Paginator + paginator = Paginator(tutos, settings.ZDS_APP['forum']['topics_per_page']) + page = request.GET.get('page') + + try: + shown_tutos = paginator.page(page) + page = int(page) + except PageNotAnInteger: + shown_tutos = paginator.page(1) + page = 1 + except EmptyPage: + shown_tutos = paginator.page(paginator.num_pages) + page = paginator.num_pages + + aides = HelpWriting.objects.all() + + return render(request, "tutorial/tutorial/help.html", { + "tutorials": shown_tutos, + "helps": aides, + "pages": paginator_range(page, paginator.num_pages), + "nb": page + }) From bd42968c77d48cd72c42708a63a3e19834eff6df Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 22 Dec 2014 09:39:38 +0100 Subject: [PATCH 091/887] =?UTF-8?q?mod=C3=A8le=20stabilis=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 4 +- zds/tutorialv2/admin.py | 9 +- zds/tutorialv2/migrations/0001_initial.py | 290 ++++++++++++++++++++++ zds/tutorialv2/migrations/__init__.py | 0 zds/tutorialv2/models.py | 90 ++++--- zds/tutorialv2/utils.py | 10 +- zds/tutorialv2/views.py | 20 +- 7 files changed, 367 insertions(+), 56 deletions(-) create mode 100644 zds/tutorialv2/migrations/0001_initial.py create mode 100644 zds/tutorialv2/migrations/__init__.py diff --git a/zds/settings.py b/zds/settings.py index 5b76ab95f3..191be8b447 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -187,6 +187,7 @@ 'zds.article', 'zds.forum', 'zds.tutorial', + 'zds.tutorialv2', 'zds.member', # Uncomment the next line to enable the admin: 'django.contrib.admin', @@ -451,7 +452,8 @@ 'repo_public_path': os.path.join(SITE_ROOT, 'tutoriels-public'), 'default_license_pk': 7, 'home_number': 5, - 'helps_per_page': 20 + 'helps_per_page': 20, + 'max_tree_depth': 3 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py index 0907547f20..e966d5f7f4 100644 --- a/zds/tutorialv2/admin.py +++ b/zds/tutorialv2/admin.py @@ -2,12 +2,11 @@ from django.contrib import admin -from .models import Tutorial, Part, Chapter, Extract, Validation, Note +from .models import PublishableContent, Container, Extract, Validation, ContentReaction -admin.site.register(Tutorial) -admin.site.register(Part) -admin.site.register(Chapter) +admin.site.register(PublishableContent) +admin.site.register(Container) admin.site.register(Extract) admin.site.register(Validation) -admin.site.register(Note) +admin.site.register(ContentReaction) diff --git a/zds/tutorialv2/migrations/0001_initial.py b/zds/tutorialv2/migrations/0001_initial.py new file mode 100644 index 0000000000..5dca0239ca --- /dev/null +++ b/zds/tutorialv2/migrations/0001_initial.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Container' + db.create_table(u'tutorialv2_container', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), + ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), + ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal(u'tutorialv2', ['Container']) + + # Adding model 'PublishableContent' + db.create_table(u'tutorialv2_publishablecontent', ( + (u'container_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('source', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Image'], null=True, on_delete=models.SET_NULL, blank=True)), + ('gallery', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Gallery'], null=True, blank=True)), + ('creation_date', self.gf('django.db.models.fields.DateTimeField')()), + ('pubdate', self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True)), + ('update_date', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('sha_public', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_beta', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_validation', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('sha_draft', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('licence', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['utils.Licence'], null=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=10, db_index=True)), + ('images', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('last_note', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='last_note', null=True, to=orm['tutorialv2.ContentReaction'])), + ('is_locked', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('js_support', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'tutorialv2', ['PublishableContent']) + + # Adding M2M table for field authors on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_authors') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('user', models.ForeignKey(orm[u'auth.user'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'user_id']) + + # Adding M2M table for field subcategory on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_subcategory') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('subcategory', models.ForeignKey(orm[u'utils.subcategory'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'subcategory_id']) + + # Adding model 'ContentReaction' + db.create_table(u'tutorialv2_contentreaction', ( + (u'comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['utils.Comment'], unique=True, primary_key=True)), + ('related_content', self.gf('django.db.models.fields.related.ForeignKey')(related_name='related_content_note', to=orm['tutorialv2.PublishableContent'])), + )) + db.send_create_signal(u'tutorialv2', ['ContentReaction']) + + # Adding model 'ContentRead' + db.create_table(u'tutorialv2_contentread', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), + ('note', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.ContentReaction'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_notes_read', to=orm['auth.User'])), + )) + db.send_create_signal(u'tutorialv2', ['ContentRead']) + + # Adding model 'Extract' + db.create_table(u'tutorialv2_extract', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), + ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + )) + db.send_create_signal(u'tutorialv2', ['Extract']) + + # Adding model 'Validation' + db.create_table(u'tutorialv2_validation', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), + ('version', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), + ('date_proposition', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('comment_authors', self.gf('django.db.models.fields.TextField')()), + ('validator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='author_content_validations', null=True, to=orm['auth.User'])), + ('date_reserve', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('date_validation', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('comment_validator', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('status', self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=10)), + )) + db.send_create_signal(u'tutorialv2', ['Validation']) + + + def backwards(self, orm): + # Deleting model 'Container' + db.delete_table(u'tutorialv2_container') + + # Deleting model 'PublishableContent' + db.delete_table(u'tutorialv2_publishablecontent') + + # Removing M2M table for field authors on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_authors')) + + # Removing M2M table for field subcategory on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_subcategory')) + + # Deleting model 'ContentReaction' + db.delete_table(u'tutorialv2_contentreaction') + + # Deleting model 'ContentRead' + db.delete_table(u'tutorialv2_contentread') + + # Deleting model 'Extract' + db.delete_table(u'tutorialv2_extract') + + # Deleting model 'Validation' + db.delete_table(u'tutorialv2_validation') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.container': { + 'Meta': {'object_name': 'Container'}, + 'compatibility_pk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'conclusion': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'introduction': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'position_in_parent': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.extract': { + 'Meta': {'object_name': 'Extract'}, + 'container': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'position_in_container': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent', '_ormbases': [u'tutorialv2.Container']}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + u'container_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['tutorialv2.Container']", 'unique': 'True', 'primary_key': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'images': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/migrations/__init__.py b/zds/tutorialv2/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cb0ea3b437..1224e01b1a 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -24,6 +24,7 @@ from zds.utils import slugify, get_current_user from zds.utils.models import SubCategory, Licence, Comment from zds.utils.tutorials import get_blob, export_tutorial +from zds.settings import ZDS_APP TYPE_CHOICES = ( @@ -76,6 +77,9 @@ class Meta: null=False, default=1) + #integer key used to represent the tutorial or article old identifier for url compatibility + compatibility_pk = models.IntegerField(null=False, default=0) + def get_children(self): """get this container children""" if self.has_extract(): @@ -95,10 +99,24 @@ def get_last_child_position(self): """Get the relative position of the last child""" return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + def get_tree_depth(self): + """get the tree depth, basically you don't want to have more than 3 levels : + - tutorial/article + - Part + - Chapter + """ + depth = 0 + current = self + while current.parent is not None: + current = current.parent + depth += 1 + + return depth + def add_container(self, container): """add a child container. A container can only be added if no extract had already been added in this container""" - if not self.has_extract(): + if not self.has_extract() and self.get_tree_depth() == ZDS_APP['tutorial']['max_tree_depth']: container.parent = self container.position_in_parent = container.get_last_child_position() + 1 container.save() @@ -106,11 +124,16 @@ def add_container(self, container): raise InvalidOperationError("Can't add a container if this container contains extracts.") def get_phy_slug(self): - """Get the physical path as stored in git file system""" + """gets the slugified title that is used to store the content into the filesystem""" base = "" if self.parent is not None: base = self.parent.get_phy_slug() - return os.path.join(base, self.slug) + + used_pk = self.compatibility_pk + if used_pk == 0: + used_pk = self.pk + + return os.path.join(base,used_pk + '_' + self.slug) def update_children(self): for child in self.get_children(): @@ -129,7 +152,7 @@ def add_extract(self, extract): -class PubliableContent(Container): +class PublishableContent(Container): """A tutorial whatever its size or an aticle.""" class Meta: @@ -145,19 +168,21 @@ class Meta: verbose_name='Sous-Catégorie', blank=True, null=True, db_index=True) + # store the thumbnail for tutorial or article image = models.ForeignKey(Image, verbose_name='Image du tutoriel', blank=True, null=True, on_delete=models.SET_NULL) + # every publishable content has its own gallery to manage images gallery = models.ForeignKey(Gallery, verbose_name='Galerie d\'images', blank=True, null=True, db_index=True) - create_at = models.DateTimeField('Date de création') + creation_date = models.DateTimeField('Date de création') pubdate = models.DateTimeField('Date de publication', blank=True, null=True, db_index=True) - update = models.DateTimeField('Date de mise à jour', + update_date = models.DateTimeField('Date de mise à jour', blank=True, null=True) sha_public = models.CharField('Sha1 de la version publique', @@ -181,7 +206,7 @@ class Meta: null=True, max_length=200) - last_note = models.ForeignKey('Note', blank=True, null=True, + last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', verbose_name='Derniere note') is_locked = models.BooleanField('Est verrouillé', default=False) @@ -190,10 +215,8 @@ class Meta: def __unicode__(self): return self.title - def get_phy_slug(self): - return str(self.pk) + "_" + self.slug - def get_absolute_url(self): + """gets the url to access the tutorial when offline""" return reverse('zds.tutorial.views.view_tutorial', args=[ self.pk, slugify(self.title) ]) @@ -215,11 +238,6 @@ def get_edit_url(self): return reverse('zds.tutorial.views.modify_tutorial') + \ '?tutorial={0}'.format(self.pk) - def get_subcontainers(self): - return Container.objects.all()\ - .filter(tutorial__pk=self.pk)\ - .order_by('position_in_parent') - def in_beta(self): return (self.sha_beta is not None) and (self.sha_beta.strip() != '') @@ -235,14 +253,15 @@ def on_line(self): def is_article(self): return self.type == 'ARTICLE' - def is_tuto(self): + def is_tutorial(self): return self.type == 'TUTO' def get_path(self, relative=False): if relative: return '' else: - return os.path.join(settings.ZDS_APP['tutorial']['repo_path'], self.get_phy_slug()) + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) def get_prod_path(self, sha=None): data = self.load_json_for_public(sha) @@ -422,22 +441,22 @@ def delete_entity_and_tree(self): def save(self, *args, **kwargs): self.slug = slugify(self.title) - super(PubliableContent, self).save(*args, **kwargs) + super(PublishableContent, self).save(*args, **kwargs) def get_note_count(self): """Return the number of notes in the tutorial.""" - return Note.objects.filter(tutorial__pk=self.pk).count() + return ContentReaction.objects.filter(tutorial__pk=self.pk).count() def get_last_note(self): """Gets the last answer in the thread, if any.""" - return Note.objects.all()\ + return ContentReaction.objects.all()\ .filter(tutorial__pk=self.pk)\ .order_by('-pubdate')\ .first() def first_note(self): """Return the first post of a topic, written by topic's author.""" - return Note.objects\ + return ContentReaction.objects\ .filter(tutorial=self)\ .order_by('pubdate')\ .first() @@ -449,7 +468,7 @@ def last_read_note(self): .select_related()\ .filter(tutorial=self, user=get_current_user())\ .latest('note__pubdate').note - except Note.DoesNotExist: + except ContentReaction.DoesNotExist: return self.first_post() def first_unread_note(self): @@ -459,7 +478,7 @@ def first_unread_note(self): .filter(tutorial=self, user=get_current_user())\ .latest('note__pubdate').note - next_note = Note.objects.filter( + next_note = ContentReaction.objects.filter( tutorial__pk=self.pk, pubdate__gt=last_note.pubdate)\ .select_related("author").first() @@ -481,7 +500,7 @@ def antispam(self, user=None): if user is None: user = get_current_user() - last_user_notes = Note.objects\ + last_user_notes = ContentReaction.objects\ .filter(tutorial=self)\ .filter(author=user.pk)\ .order_by('-position') @@ -526,14 +545,15 @@ def have_epub(self): ".epub")) -class Note(Comment): +class ContentReaction(Comment): """A comment written by an user about a Publiable content he just read.""" class Meta: - verbose_name = 'note sur un tutoriel' - verbose_name_plural = 'notes sur un tutoriel' + verbose_name = 'note sur un contenu' + verbose_name_plural = 'notes sur un contenu' - related_content = models.ForeignKey(PubliableContent, verbose_name='Tutoriel', db_index=True) + related_content = models.ForeignKey(PublishableContent, verbose_name='Contenu', + related_name="related_content_note", db_index=True) def __unicode__(self): """Textual form of a post.""" @@ -557,12 +577,12 @@ class ContentRead(models.Model): """ class Meta: - verbose_name = 'Tutoriel lu' - verbose_name_plural = 'Tutoriels lus' + verbose_name = 'Contenu lu' + verbose_name_plural = 'Contenu lus' - tutorial = models.ForeignKey(PubliableContent, db_index=True) - note = models.ForeignKey(Note, db_index=True) - user = models.ForeignKey(User, related_name='tuto_notes_read', db_index=True) + tutorial = models.ForeignKey(PublishableContent, db_index=True) + note = models.ForeignKey(ContentReaction, db_index=True) + user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) def __unicode__(self): return u''.format(self.tutorial, @@ -716,7 +736,7 @@ class Meta: verbose_name = 'Validation' verbose_name_plural = 'Validations' - tutorial = models.ForeignKey(PubliableContent, null=True, blank=True, + tutorial = models.ForeignKey(PublishableContent, null=True, blank=True, verbose_name='Tutoriel proposé', db_index=True) version = models.CharField('Sha1 de la version', blank=True, null=True, max_length=80, db_index=True) @@ -724,7 +744,7 @@ class Meta: comment_authors = models.TextField('Commentaire de l\'auteur') validator = models.ForeignKey(User, verbose_name='Validateur', - related_name='author_validations', + related_name='author_content_validations', blank=True, null=True, db_index=True) date_reserve = models.DateTimeField('Date de réservation', blank=True, null=True) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 365abcf760..56e6168989 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -1,13 +1,13 @@ # coding: utf-8 -from zds.tutorialv2.models import PubliableContent, ContentRead +from zds.tutorialv2.models import PublishableContent, ContentRead from zds import settings from zds.utils import get_current_user def get_last_tutorials(): """get the last issued tutorials""" n = settings.ZDS_APP['tutorial']['home_number'] - tutorials = PubliableContent.objects.all()\ + tutorials = PublishableContent.objects.all()\ .exclude(type="ARTICLE")\ .exclude(sha_public__isnull=True)\ .exclude(sha_public__exact='')\ @@ -19,7 +19,7 @@ def get_last_tutorials(): def get_last_articles(): """get the last issued articles""" n = settings.ZDS_APP['tutorial']['home_number'] - articles = PubliableContent.objects.all()\ + articles = PublishableContent.objects.all()\ .exclude(type="TUTO")\ .exclude(sha_public__isnull=True)\ .exclude(sha_public__exact='')\ @@ -29,7 +29,7 @@ def get_last_articles(): def never_read(tutorial, user=None): - """Check if a topic has been read by an user since it last post was + """Check if the tutorial note feed has been read by an user since its last post was added.""" if user is None: user = get_current_user() @@ -40,7 +40,7 @@ def never_read(tutorial, user=None): def mark_read(tutorial): - """Mark a tutorial as read for the user.""" + """Mark the last tutorial note as read for the user.""" if tutorial.last_note is not None: ContentRead.objects.filter( tutorial=tutorial, diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index efd77f81f0..373794a27e 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -43,7 +43,7 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ - mark_read, Note, HelpWriting + mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile @@ -997,7 +997,7 @@ def view_tutorial_online(request, tutorial_pk, tutorial_slug): # Find all notes of the tutorial. - notes = Note.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() + notes = ContentReaction.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() # Retrieve pk of the last note. If there aren't notes for the tutorial, we # initialize this last note at 0. @@ -3284,7 +3284,7 @@ def answer(request): # Retrieve 3 last notes of the current tutorial. - notes = Note.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] + notes = ContentReaction.objects.filter(tutorial=tutorial).order_by("-pubdate")[:3] # If there is a last notes for the tutorial, we save his pk. Otherwise, we # save 0. @@ -3294,7 +3294,7 @@ def answer(request): last_note_pk = tutorial.last_note.pk # Retrieve lasts notes of the current tutorial. - notes = Note.objects.filter(tutorial=tutorial) \ + notes = ContentReaction.objects.filter(tutorial=tutorial) \ .prefetch_related() \ .order_by("-pubdate")[:settings.ZDS_APP['forum']['posts_per_page']] @@ -3327,7 +3327,7 @@ def answer(request): form = NoteForm(tutorial, request.user, request.POST) if form.is_valid(): data = form.data - note = Note() + note = ContentReaction() note.related_content = tutorial note.author = request.user note.text = data["text"] @@ -3356,7 +3356,7 @@ def answer(request): if "cite" in request.GET: note_cite_pk = request.GET["cite"] - note_cite = Note.objects.get(pk=note_cite_pk) + note_cite = ContentReaction.objects.get(pk=note_cite_pk) if not note_cite.is_visible: raise PermissionDenied @@ -3390,7 +3390,7 @@ def solve_alert(request): raise PermissionDenied alert = get_object_or_404(Alert, pk=request.POST["alert_pk"]) - note = Note.objects.get(pk=alert.comment.id) + note = ContentReaction.objects.get(pk=alert.comment.id) if "text" in request.POST and request.POST["text"] != "": bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) @@ -3443,7 +3443,7 @@ def edit_note(request): note_pk = request.GET["message"] except KeyError: raise Http404 - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) g_tutorial = None if note.position >= 1: g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) @@ -3526,7 +3526,7 @@ def like_note(request): except KeyError: raise Http404 resp = {} - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) user = request.user if note.author.pk != request.user.pk: @@ -3571,7 +3571,7 @@ def dislike_note(request): except KeyError: raise Http404 resp = {} - note = get_object_or_404(Note, pk=note_pk) + note = get_object_or_404(ContentReaction, pk=note_pk) user = request.user if note.author.pk != request.user.pk: From 60833adb4c07014494aee82aa95189be72a5f568 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 25 Dec 2014 21:33:21 +0100 Subject: [PATCH 092/887] Petit refactoring - Docstring - PEP8 - Des todos pour en discuter --- zds/tutorialv2/__init__.py | 1 - zds/tutorialv2/models.py | 506 ++++++++++++++++++++++++------------- zds/tutorialv2/utils.py | 37 ++- zds/tutorialv2/views.py | 10 +- 4 files changed, 362 insertions(+), 192 deletions(-) diff --git a/zds/tutorialv2/__init__.py b/zds/tutorialv2/__init__.py index 8b13789179..e69de29bb2 100644 --- a/zds/tutorialv2/__init__.py +++ b/zds/tutorialv2/__init__.py @@ -1 +0,0 @@ - diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 1224e01b1a..afb9289fd3 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -28,7 +28,7 @@ TYPE_CHOICES = ( - ('TUTO', 'Tutoriel'), + ('TUTORIAL', 'Tutoriel'), ('ARTICLE', 'Article'), ) @@ -45,8 +45,16 @@ class InvalidOperationError(RuntimeError): class Container(models.Model): + """ + A container, which can have sub-Containers or Extracts. + + A Container has a title, a introduction and a conclusion, a parent (which can be None) and a position into this + parent (which is 1 by default). + + It has also a tree depth. - """A container (tuto/article, part, chapter).""" + There is a `compatibility_pk` for compatibility with older versions. + """ class Meta: verbose_name = 'Container' verbose_name_plural = 'Containers' @@ -68,63 +76,85 @@ class Meta: max_length=200) parent = models.ForeignKey("self", - verbose_name='Conteneur parent', - blank=True, null=True, - on_delete=models.SET_NULL) + verbose_name='Conteneur parent', + blank=True, null=True, + on_delete=models.SET_NULL) position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', blank=False, null=False, default=1) - #integer key used to represent the tutorial or article old identifier for url compatibility + # TODO: thumbnails ? + + # integer key used to represent the tutorial or article old identifier for url compatibility compatibility_pk = models.IntegerField(null=False, default=0) def get_children(self): - """get this container children""" + """ + :return: children of this Container, ordered by position + """ if self.has_extract(): return Extract.objects.filter(container_pk=self.pk) - return Container.objects.filter(parent_pk=self.pk) + return Container.objects.filter(parent_pk=self.pk).order_by('position_in_parent') def has_extract(self): - """Check this container has content extracts""" - return Extract.objects.filter(chapter=self).count() > 0 + """ + :return: `True` if the Container has extract as children, `False` otherwise. + """ + return Extract.objects.filter(parent=self).count() > 0 - def has_sub_part(self): - """Check this container has a sub container""" - return Container.objects.filter(parent=self).count() > 0 + def has_sub_container(self): + """ + :return: `True` if the Container has other Containers as children, `False` otherwise. + """ + return Container.objects.filter(container=self).count() > 0 def get_last_child_position(self): - """Get the relative position of the last child""" - return Container.objects.filter(parent=self).count() + Extract.objects.filter(chapter=self).count() + """ + :return: the relative position of the last child + """ + return Container.objects.filter(parent=self).count() + Extract.objects.filter(container=self).count() def get_tree_depth(self): - """get the tree depth, basically you don't want to have more than 3 levels : - - tutorial/article - - Part - - Chapter + """ + Tree depth is no more than 2, because there is 3 levels for Containers : + - PublishableContent (0), + - Part (1), + - Chapter (2) + Note that `'max_tree_depth` is `2` to ensure that there is no more than 3 levels + :return: Tree depth """ depth = 0 current = self while current.parent is not None: current = current.parent depth += 1 - return depth def add_container(self, container): - """add a child container. A container can only be added if - no extract had already been added in this container""" - if not self.has_extract() and self.get_tree_depth() == ZDS_APP['tutorial']['max_tree_depth']: - container.parent = self - container.position_in_parent = container.get_last_child_position() + 1 - container.save() + """ + Add a child Container, but only if no extract were previously added and tree depth is < 2. + :param container: the new Container + """ + if not self.has_extract(): + if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + container.parent = self + container.position_in_parent = container.get_last_child_position() + 1 + container.save() + else: + raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") + # TODO: limitation if article ? def get_phy_slug(self): - """gets the slugified title that is used to store the content into the filesystem""" + """ + The slugified title is used to store physically the information in filesystem. + A "compatibility pk" can be used instead of real pk to ensure compatibility with previous versions. + :return: the slugified title + """ base = "" if self.parent is not None: base = self.parent.get_phy_slug() @@ -133,9 +163,12 @@ def get_phy_slug(self): if used_pk == 0: used_pk = self.pk - return os.path.join(base,used_pk + '_' + self.slug) + return os.path.join(base, str(used_pk) + '_' + self.slug) def update_children(self): + """ + Update all children of the container. + """ for child in self.get_children(): if child is Container: self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") @@ -143,22 +176,46 @@ def update_children(self): self.save() child.update_children() else: - child.text = child.get_path(relative=True) + child.text = child.get_path(relative=True) child.save() def add_extract(self, extract): - if not self.has_sub_part(): - extract.chapter = self - + """ + Add a child container, but only if no container were previously added + :param extract: the new Extract + """ + if not self.has_sub_container(): + extract.container = self + extract.save() + # TODO: + # - rewrite save() + # - get_absolute_url_*() stuffs, get_path(), get_prod_path() + # - __unicode__() + # - get_introduction_*(), get_conclusion_*() + # - a `top_parent()` function to access directly to the parent PublishableContent and avoid the + # `container.parent.parent.parent` stuff ? + # - a nice `delete_entity_and_tree()` function ? (which also remove the file) + # - the `maj_repo_*()` stuffs should probably be into the model ? class PublishableContent(Container): + """ + A tutorial whatever its size or an article. - """A tutorial whatever its size or an aticle.""" + A PublishableContent is a tree depth 0 Container (no parent) with additional information, such as + - authors, description, source (if the content comes from another website), subcategory and licence ; + - Thumbnail and gallery ; + - Creation, publication and update date ; + - Public, beta, validation and draft sha, for versioning ; + - Comment support ; + - Type, which is either "ARTICLE" or "TUTORIAL" + + These are two repositories : draft and online. + """ class Meta: verbose_name = 'Tutoriel' verbose_name_plural = 'Tutoriels' - + # TODO: "Contenu" ? description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) @@ -183,7 +240,7 @@ class Meta: pubdate = models.DateTimeField('Date de publication', blank=True, null=True, db_index=True) update_date = models.DateTimeField('Date de mise à jour', - blank=True, null=True) + blank=True, null=True) sha_public = models.CharField('Sha1 de la version publique', blank=True, null=True, max_length=80, db_index=True) @@ -205,6 +262,7 @@ class Meta: blank=True, null=True, max_length=200) + # TODO: rename this field ? (`relative_image_path` ?) last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', @@ -216,70 +274,120 @@ def __unicode__(self): return self.title def get_absolute_url(self): - """gets the url to access the tutorial when offline""" - return reverse('zds.tutorial.views.view_tutorial', args=[ - self.pk, slugify(self.title) - ]) + """ + :return: the url to access the tutorial when offline + """ + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) def get_absolute_url_online(self): - return reverse('zds.tutorial.views.view_tutorial_online', args=[ - self.pk, slugify(self.title) - ]) + """ + :return: the url to access the tutorial when online + """ + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ if self.sha_beta is not None: - return reverse('zds.tutorial.views.view_tutorial', args=[ - self.pk, slugify(self.title) - ]) + '?version=' + self.sha_beta + return self.get_absolute_url() + '?version=' + self.sha_beta else: return self.get_absolute_url() def get_edit_url(self): - return reverse('zds.tutorial.views.modify_tutorial') + \ - '?tutorial={0}'.format(self.pk) + """ + :return: the url to edit the tutorial + """ + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) def in_beta(self): + """ + A tutorial is not in beta if sha_beta is `None` or empty + :return: `True` if the tutorial is in beta, `False` otherwise + """ return (self.sha_beta is not None) and (self.sha_beta.strip() != '') def in_validation(self): + """ + A tutorial is not in validation if sha_validation is `None` or empty + :return: `True` if the tutorial is in validation, `False` otherwise + """ return (self.sha_validation is not None) and (self.sha_validation.strip() != '') def in_drafting(self): + """ + A tutorial is not in draft if sha_draft is `None` or empty + :return: `True` if the tutorial is in draft, `False` otherwise + """ + # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') def on_line(self): + """ + A tutorial is not in on line if sha_public is `None` or empty + :return: `True` if the tutorial is on line, `False` otherwise + """ + # TODO: for the logic with previous method, why not `in_public()` ? return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_article(self): + """ + :return: `True` if article, `False` otherwise + """ return self.type == 'ARTICLE' def is_tutorial(self): - return self.type == 'TUTO' + """ + :return: `True` if tutorial, `False` otherwise + """ + return self.type == 'TUTORIAL' + + def get_phy_slug(self): + """ + :return: the physical slug, used to represent data in filesystem + """ + return str(self.pk) + "_" + self.slug def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ if relative: return '' else: # get the full path (with tutorial/article before it) return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + # TODO: versionning ?!? def get_prod_path(self, sha=None): + """ + Get the physical path to the public version of the content + :param sha: version of the content, if `None`, public version is used + :return: physical path + """ data = self.load_json_for_public(sha) return os.path.join( - settings.ZDS_APP['tutorial']['repo_public_path'], + settings.ZDS_APP[self.type.lower()]['repo_public_path'], str(self.pk) + '_' + slugify(data['title'])) def load_dic(self, mandata, sha=None): - '''fill mandata with informations from database model''' + """ + Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. + :param mandata: a dictionary from JSON file + :param sha: current version, used to fill the `is_*` fields by comparison with the corresponding `sha_*` + """ + # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', - 'have_epub', 'get_path', 'in_beta', 'in_validation', 'on_line' + 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'get_path', 'in_beta', + 'in_validation', 'on_line' ] attrs = [ - 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', - 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' + 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', + 'sha_validation', 'sha_public' ] # load functions and attributs in tree @@ -291,37 +399,37 @@ def load_dic(self, mandata, sha=None): # general information mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha - mandata['is_validation'] = self.in_validation() \ - and self.sha_validation == sha + mandata['is_validation'] = self.in_validation() and self.sha_validation == sha mandata['is_on_line'] = self.on_line() and self.sha_public == sha # url: - mandata['get_absolute_url'] = reverse( - 'zds.tutorial.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) if self.in_beta(): mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', + 'zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']] ) + '?version=' + self.sha_beta else: mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorial.views.view_tutorial', + 'zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']] ) mandata['get_absolute_url_online'] = reverse( - 'zds.tutorial.views.view_tutorial_online', + 'zds.tutorialv2.views.view_tutorial_online', args=[self.pk, mandata['slug']] ) def load_introduction_and_conclusion(self, mandata, sha=None, public=False): - '''Explicitly load introduction and conclusion to avoid useless disk - access in load_dic() - ''' + """ + Explicitly load introduction and conclusion to avoid useless disk access in `load_dic()` + :param mandata: dictionary from JSON file + :param sha: version + :param public: if `True`, get introduction and conclusion from the public version instead of the draft one + (`sha` is not used in this case) + """ if public: mandata['get_introduction_online'] = self.get_introduction_online() @@ -331,17 +439,29 @@ def load_introduction_and_conclusion(self, mandata, sha=None, public=False): mandata['get_conclusion'] = self.get_conclusion(sha) def load_json_for_public(self, sha=None): + """ + Fetch the public version of the JSON file for this content. + :param sha: version + :return: a dictionary containing the structure of the JSON file. + """ if sha is None: sha = self.sha_public - repo = Repo(self.get_path()) + repo = Repo(self.get_path()) # should be `get_prod_path()` !?! mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') data = json_reader.loads(mantuto) if 'licence' in data: data['licence'] = Licence.objects.filter(code=data['licence']).first() return data + # TODO: redundant with next function def load_json(self, path=None, online=False): - + """ + Fetch a specific version of the JSON file for this content. + :param sha: version + :param path: path to the repository. If None, the `get_[prod_]path()` function is used + :param public: if `True`fetch the public version instead of the private one + :return: a dictionary containing the structure of the JSON file. + """ if path is None: if online: man_path = os.path.join(self.get_prod_path(), 'manifest.json') @@ -359,6 +479,10 @@ def load_json(self, path=None, online=False): return data def dump_json(self, path=None): + """ + Write the JSON into file + :param path: path to the file. If `None`, use default path. + """ if path is None: man_path = os.path.join(self.get_path(), 'manifest.json') else: @@ -371,6 +495,11 @@ def dump_json(self, path=None): json_data.close() def get_introduction(self, sha=None): + """ + Get the introduction content of a specific version + :param sha: version, if `None`, use draft one + :return: the introduction (as a string) + """ # find hash code if sha is None: sha = self.sha_draft @@ -385,7 +514,10 @@ def get_introduction(self, sha=None): return get_blob(repo.commit(sha).tree, path_content_intro) def get_introduction_online(self): - """get the introduction content for a particular version if sha is not None""" + """ + Get introduction content of the public version + :return: the introduction (as a string) + """ if self.on_line(): intro = open( os.path.join( @@ -399,7 +531,11 @@ def get_introduction_online(self): return intro_contenu.decode('utf-8') def get_conclusion(self, sha=None): - """get the conclusion content for a particular version if sha is not None""" + """ + Get the conclusion content of a specific version + :param sha: version, if `None`, use draft one + :return: the conclusion (as a string) + """ # find hash code if sha is None: sha = self.sha_draft @@ -414,7 +550,10 @@ def get_conclusion(self, sha=None): return get_blob(repo.commit(sha).tree, path_content_ccl) def get_conclusion_online(self): - """get the conclusion content for the online version of the current publiable content""" + """ + Get conclusion content of the public version + :return: the conclusion (as a string) + """ if self.on_line(): conclusion = open( os.path.join( @@ -428,7 +567,9 @@ def get_conclusion_online(self): return conlusion_content.decode('utf-8') def delete_entity_and_tree(self): - """deletes the entity and its filesystem counterpart""" + """ + Delete the entities and their filesystem counterparts + """ shutil.rmtree(self.get_path(), 0) Validation.objects.filter(tutorial=self).delete() @@ -437,6 +578,7 @@ def delete_entity_and_tree(self): if self.on_line(): shutil.rmtree(self.get_prod_path()) self.delete() + # TODO: should use the "git" version of `delete()` !!! def save(self, *args, **kwargs): self.slug = slugify(self.title) @@ -444,25 +586,33 @@ def save(self, *args, **kwargs): super(PublishableContent, self).save(*args, **kwargs) def get_note_count(self): - """Return the number of notes in the tutorial.""" + """ + :return : umber of notes in the tutorial. + """ return ContentReaction.objects.filter(tutorial__pk=self.pk).count() def get_last_note(self): - """Gets the last answer in the thread, if any.""" + """ + :return: the last answer in the thread, if any. + """ return ContentReaction.objects.all()\ .filter(tutorial__pk=self.pk)\ .order_by('-pubdate')\ .first() def first_note(self): - """Return the first post of a topic, written by topic's author.""" + """ + :return: the first post of a topic, written by topic's author, if any. + """ return ContentReaction.objects\ .filter(tutorial=self)\ .order_by('pubdate')\ .first() def last_read_note(self): - """Return the last post the user has read.""" + """ + :return: the last post the user has read. + """ try: return ContentRead.objects\ .select_related()\ @@ -472,7 +622,9 @@ def last_read_note(self): return self.first_post() def first_unread_note(self): - """Return the first note the user has unread.""" + """ + :return: Return the first note the user has unread. + """ try: last_note = ContentRead.objects\ .filter(tutorial=self, user=get_current_user())\ @@ -482,20 +634,15 @@ def first_unread_note(self): tutorial__pk=self.pk, pubdate__gt=last_note.pubdate)\ .select_related("author").first() - return next_note - except: + except: # TODO: `except:` is bad. return self.first_note() def antispam(self, user=None): - """Check if the user is allowed to post in an tutorial according to the - SPAM_LIMIT_SECONDS value. - - If user shouldn't be able to note, then antispam is activated - and this method returns True. Otherwise time elapsed between - user's last note and now is enough, and the method will return - False. - + """ + Check if the user is allowed to post in an tutorial according to the SPAM_LIMIT_SECONDS value. + :param user: the user to check antispam. If `None`, current user is used. + :return: `True` if the user is not able to note (the elapsed time is not enough), `False` otherwise. """ if user is None: user = get_current_user() @@ -513,41 +660,47 @@ def antispam(self, user=None): return False def change_type(self, new_type): - """Allow someone to change the content type, basicaly from tutorial to article""" + """ + Allow someone to change the content type, basically from tutorial to article + :param new_type: the new type, either `"ARTICLE"` or `"TUTORIAL"` + """ if new_type not in TYPE_CHOICES: raise ValueError("This type of content does not exist") - self.type = new_type - def have_markdown(self): - """Check the markdown zip archive is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".md")) + """ + Check if the markdown zip archive is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".md")) def have_html(self): - """Check the html version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".html")) + """ + Check if the html version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".html")) def have_pdf(self): - """Check the pdf version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".pdf")) + """ + Check if the pdf version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".pdf")) def have_epub(self): - """Check the standard epub version of the content is available""" - return os.path.isfile(os.path.join(self.get_prod_path(), - self.slug + - ".epub")) + """ + Check if the standard epub version of the content is available + :return: `True` if available, `False` otherwise + """ + return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) class ContentReaction(Comment): - - """A comment written by an user about a Publiable content he just read.""" + """ + A comment written by any user about a PublishableContent he just read. + """ class Meta: verbose_name = 'note sur un contenu' verbose_name_plural = 'notes sur un contenu' @@ -556,43 +709,40 @@ class Meta: related_name="related_content_note", db_index=True) def __unicode__(self): - """Textual form of a post.""" return u''.format(self.related_content, self.pk) def get_absolute_url(self): + """ + :return: the url of the comment + """ page = int(ceil(float(self.position) / settings.ZDS_APP['forum']['posts_per_page'])) - - return '{0}?page={1}#p{2}'.format( - self.related_content.get_absolute_url_online(), - page, - self.pk) + return '{0}?page={1}#p{2}'.format(self.related_content.get_absolute_url_online(), page, self.pk) class ContentRead(models.Model): + """ + Small model which keeps track of the user viewing tutorials. - """Small model which keeps track of the user viewing tutorials. - - It remembers the topic he looked and what was the last Note at this - time. - + It remembers the PublishableContent he looked and what was the last Note at this time. """ class Meta: verbose_name = 'Contenu lu' verbose_name_plural = 'Contenu lus' - tutorial = models.ForeignKey(PublishableContent, db_index=True) + content = models.ForeignKey(PublishableContent, db_index=True) note = models.ForeignKey(ContentReaction, db_index=True) user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) def __unicode__(self): - return u''.format(self.tutorial, - self.user, - self.note.pk) + return u''.format(self.content, self.user, self.note.pk) class Extract(models.Model): + """ + A content extract from a Container. - """A content extract from a chapter.""" + It has a title, a position in the parent container and a text. + """ class Meta: verbose_name = 'Extrait' verbose_name_plural = 'Extraits' @@ -611,64 +761,57 @@ def __unicode__(self): return u''.format(self.title) def get_absolute_url(self): + """ + :return: the url to access the tutorial offline + """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url(), - self.position_in_chapter, + self.position_in_container, slugify(self.title) ) def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_online(), - self.position_in_chapter, + self.position_in_container, slugify(self.title) ) - def get_path(self, relative=False): - if relative: - if self.container.tutorial: - chapter_path = '' - else: - chapter_path = os.path.join( - self.container.part.get_phy_slug(), - self.container.get_phy_slug()) - else: - if self.container.tutorial: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - self.container.tutorial.get_phy_slug()) - else: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - self.container.part.tutorial.get_phy_slug(), - self.container.part.get_phy_slug(), - self.container.get_phy_slug()) + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_beta(), + self.position_in_container, + slugify(self.title) + ) - return os.path.join(chapter_path, str(self.pk) + "_" + slugify(self.title)) + '.md' + def get_phy_slug(self): + """ + :return: the physical slug + """ + return str(self.pk) + '_' + slugify(self.title) - def get_prod_path(self): + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the extract. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + # TODO: versionning ? - if self.container.tutorial: - data = self.container.tutorial.load_json_for_public() - mandata = self.container.tutorial.load_dic(data) - if "chapter" in mandata: - for ext in mandata["chapter"]["extracts"]: - if ext['pk'] == self.pk: - return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], - str(self.container.tutorial.pk) + '_' + slugify(mandata['title']), - str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' - else: - data = self.container.part.tutorial.load_json_for_public() - mandata = self.container.part.tutorial.load_dic(data) - for part in mandata["parts"]: - for chapter in part["chapters"]: - for ext in chapter["extracts"]: - if ext['pk'] == self.pk: - return os.path.join(settings.ZDS_APP['tutorial']['repo_public_path'], - str(mandata['pk']) + '_' + slugify(mandata['title']), - str(part['pk']) + "_" + slugify(part['title']), - str(chapter['pk']) + "_" + slugify(chapter['title']), - str(ext['pk']) + "_" + slugify(ext['title'])) \ - + '.md.html' + def get_prod_path(self, sha=None): + """ + Get the physical path to the public version of a specific version of the extract. + :param sha: version of the content, if `None`, `sha_public` is used + :return: physical path + """ + return os.path.join(self.container.get_prod_path(sha), self.get_phy_slug()) + '.md.html' def get_text(self, sha=None): @@ -730,14 +873,15 @@ def get_text_online(self): class Validation(models.Model): - - """Tutorial validation.""" + """ + Content validation. + """ class Meta: verbose_name = 'Validation' verbose_name_plural = 'Validations' - tutorial = models.ForeignKey(PublishableContent, null=True, blank=True, - verbose_name='Tutoriel proposé', db_index=True) + content = models.ForeignKey(PublishableContent, null=True, blank=True, + verbose_name='Contenu proposé', db_index=True) version = models.CharField('Sha1 de la version', blank=True, null=True, max_length=80, db_index=True) date_proposition = models.DateTimeField('Date de proposition', db_index=True) @@ -758,16 +902,32 @@ class Meta: default='PENDING') def __unicode__(self): - return self.tutorial.title + return self.content.title def is_pending(self): + """ + Check if the validation is pending + :return: `True` if status is pending, `False` otherwise + """ return self.status == 'PENDING' def is_pending_valid(self): + """ + Check if the validation is pending (but there is a validator) + :return: `True` if status is pending, `False` otherwise + """ return self.status == 'PENDING_V' def is_accept(self): + """ + Check if the content is accepted + :return: `True` if status is accepted, `False` otherwise + """ return self.status == 'ACCEPT' def is_reject(self): + """ + Check if the content is rejected + :return: `True` if status is rejected, `False` otherwise + """ return self.status == 'REJECT' diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 56e6168989..693b09b1d7 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -4,8 +4,11 @@ from zds import settings from zds.utils import get_current_user + def get_last_tutorials(): - """get the last issued tutorials""" + """ + :return: last issued tutorials + """ n = settings.ZDS_APP['tutorial']['home_number'] tutorials = PublishableContent.objects.all()\ .exclude(type="ARTICLE")\ @@ -17,7 +20,9 @@ def get_last_tutorials(): def get_last_articles(): - """get the last issued articles""" + """ + :return: last issued articles + """ n = settings.ZDS_APP['tutorial']['home_number'] articles = PublishableContent.objects.all()\ .exclude(type="TUTO")\ @@ -28,25 +33,31 @@ def get_last_articles(): return articles -def never_read(tutorial, user=None): - """Check if the tutorial note feed has been read by an user since its last post was - added.""" +def never_read(content, user=None): + """ + Check if a content note feed has been read by an user since its last post was added. + :param content: the content to check + :return: `True` if it is the case, `False` otherwise + """ if user is None: user = get_current_user() return ContentRead.objects\ - .filter(note=tutorial.last_note, tutorial=tutorial, user=user)\ + .filter(note=content.last_note, content=content, user=user)\ .count() == 0 -def mark_read(tutorial): - """Mark the last tutorial note as read for the user.""" - if tutorial.last_note is not None: +def mark_read(content): + """ + Mark the last tutorial note as read for the user. + :param content: the content to mark + """ + if content.last_note is not None: ContentRead.objects.filter( - tutorial=tutorial, + content=content, user=get_current_user()).delete() a = ContentRead( - note=tutorial.last_note, - tutorial=tutorial, + note=content.last_note, + content=content, user=get_current_user()) - a.save() \ No newline at end of file + a.save() diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 373794a27e..b90966b8c6 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -195,7 +195,7 @@ def reservation(request, validation_pk): _(u"Le tutoriel a bien été \ réservé par {0}.").format(request.user.username)) return redirect( - validation.tutorial.get_absolute_url() + + validation.content.get_absolute_url() + "?version=" + validation.version ) @@ -468,7 +468,7 @@ def ask_validation(request): # We create and save validation object of the tutorial. validation = Validation() - validation.tutorial = tutorial + validation.content = tutorial validation.date_proposition = datetime.now() validation.comment_authors = request.POST["text"] validation.version = request.POST["version"] @@ -491,9 +491,9 @@ def ask_validation(request): False, ) validation.save() - validation.tutorial.source = request.POST["source"] - validation.tutorial.sha_validation = request.POST["version"] - validation.tutorial.save() + validation.content.source = request.POST["source"] + validation.content.sha_validation = request.POST["version"] + validation.content.save() messages.success(request, _(u"Votre demande de validation a été envoyée à l'équipe.")) return redirect(tutorial.get_absolute_url()) From d32f97f09a0a379e41790a55c9f239f1b0178b7f Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 11:08:15 +0100 Subject: [PATCH 093/887] =?UTF-8?q?D=C3=A9but=20r=C3=A9facto=20des=20vues.?= =?UTF-8?q?=20Feed=20migr=C3=A9s=20Pagination=20param=C3=A9tr=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 3 +- zds/tutorialv2/feeds.py | 41 +++++++++++++++++++++++-- zds/tutorialv2/urls.py | 9 +++++- zds/tutorialv2/views.py | 67 ++++++++++++++++++++++------------------- 4 files changed, 85 insertions(+), 35 deletions(-) diff --git a/zds/settings.py b/zds/settings.py index 191be8b447..454e4e4e92 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -453,7 +453,8 @@ 'default_license_pk': 7, 'home_number': 5, 'helps_per_page': 20, - 'max_tree_depth': 3 + 'max_tree_depth': 3, + 'content_per_page': 50 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index f2b6a6174e..0c7b10a716 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -5,7 +5,7 @@ from django.utils.feedgenerator import Atom1Feed -from .models import Tutorial +from .models import PublishableContent class LastTutorialsFeedRSS(Feed): @@ -14,7 +14,8 @@ class LastTutorialsFeedRSS(Feed): description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) def items(self): - return Tutorial.objects\ + return PublishableContent.objects\ + .filter(type="TUTO")\ .filter(sha_public__isnull=False)\ .order_by('-pubdate')[:5] @@ -42,3 +43,39 @@ def item_link(self, item): class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description + +class LastArticlesFeedRSS(Feed): + title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) + link = "/articles/" + description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + def items(self): + return PublishableContent.objects\ + .filter(type="ARTICLE")\ + .filter(sha_public__isnull=False)\ + .order_by('-pubdate')[:5] + + def item_title(self, item): + return item.title + + def item_pubdate(self, item): + return item.pubdate + + def item_description(self, item): + return item.description + + def item_author_name(self, item): + authors_list = item.authors.all() + authors = [] + for authors_obj in authors_list: + authors.append(authors_obj.username) + authors = ", ".join(authors) + return authors + + def item_link(self, item): + return item.get_absolute_url_online() + + +class LastTutorialsFeedATOM(LastArticlesFeedRSS): + feed_type = Atom1Feed + subtitle = LastTutorialsFeedRSS.description diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 15e3f3e8d3..97a24e5ce3 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -4,8 +4,15 @@ from . import views from . import feeds +from .views import * urlpatterns = patterns('', + # viewing articles + url(r'^articles/$', ArticleList.as_view(), name="index-article"), + url(r'^articles/flux/rss/$', feeds.LastArticlesFeedRSS(), name='article-feed-rss'), + url(r'^articles/flux/atom/$', feeds.LastArticlesFeedATOM(), name='article-feed-atom'), + + # Viewing url(r'^flux/rss/$', feeds.LastTutorialsFeedRSS(), name='tutorial-feed-rss'), url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), @@ -65,7 +72,7 @@ url(r'^nouveau/extrait/$', 'zds.tutorial.views.add_extract'), - url(r'^$', 'zds.tutorial.views.index'), + url(r'^$', TutorialList.as_view, name='index-tutorial'), url(r'^importer/$', 'zds.tutorial.views.import_tuto'), url(r'^import_local/$', 'zds.tutorial.views.local_import'), diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index b90966b8c6..5ed99a824f 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -45,6 +45,7 @@ from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image +from .models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -60,6 +61,41 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ +from django.views.generic import ListView, DetailView, UpdateView + + +class ArticleList(ListView): + + """Displays the list of published articles.""" + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type="ARTICLE" + template_name = 'article/index.html' + tag = None + + def get_queryset(self): + """filter the content to obtain the list of only articles. If tag parameter is provided, only article + which has this category will be listed.""" + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') + + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + return context + + +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" + + context_object_name = 'tutorials' + type="TUTO" + template_name = 'tutorial/index.html' def render_chapter_form(chapter): @@ -74,37 +110,6 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) -def index(request): - """Display all public tutorials of the website.""" - - # The tag indicate what the category tutorial the user would like to - # display. We can display all subcategories for tutorials. - - try: - tag = get_object_or_404(SubCategory, slug=request.GET["tag"]) - except (KeyError, Http404): - tag = None - if tag is None: - tutorials = \ - Tutorial.objects.filter(sha_public__isnull=False).exclude(sha_public="") \ - .order_by("-pubdate") \ - .all() - else: - # The tag isn't None and exist in the system. We can use it to retrieve - # all tutorials in the subcategory specified. - - tutorials = Tutorial.objects.filter( - sha_public__isnull=False, - subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() - - tuto_versions = [] - for tutorial in tutorials: - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata) - tuto_versions.append(mandata) - return render(request, "tutorial/index.html", {"tutorials": tuto_versions, "tag": tag}) - - # Staff actions. From 97752f5d62d71f964a19280bfac5a0aa51c52e0d Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 26 Dec 2014 13:40:30 +0100 Subject: [PATCH 094/887] Petit refactoring - PEP8 - Docstring - TODO a discuter - Corrections "evidentes" --- zds/tutorialv2/feeds.py | 51 +++++++++++++++++++++++++++++++++------- zds/tutorialv2/models.py | 16 +++++++++---- zds/tutorialv2/views.py | 20 +++++++++------- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index 0c7b10a716..c2cc90b347 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -5,19 +5,27 @@ from django.utils.feedgenerator import Atom1Feed -from .models import PublishableContent +from models import PublishableContent -class LastTutorialsFeedRSS(Feed): - title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) - link = "/tutoriels/" - description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) +class LastContentFeedRSS(Feed): + """ + RSS feed for any type of content. + """ + title = u"Contenu sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers contenus parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + link = "" + content_type = None def items(self): - return PublishableContent.objects\ - .filter(type="TUTO")\ - .filter(sha_public__isnull=False)\ - .order_by('-pubdate')[:5] + """ + :return: The last 5 contents (sorted by publication date). If `self.type` is not `None`, the contents will only + be of this type. + """ + contents = PublishableContent.objects.filter(sha_public__isnull=False) + if self.content_type is not None: + contents.filter(type=self.content_type) + return contents.order_by('-pubdate')[:5] def item_title(self, item): return item.title @@ -40,6 +48,16 @@ def item_link(self, item): return item.get_absolute_url_online() +class LastTutorialsFeedRSS(LastContentFeedRSS): + """ + Redefinition of `LastContentFeedRSS` for tutorials only + """ + content_type = "TUTORIAL" + link = "/tutoriels/" + title = u"Tutoriels sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers tutoriels parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description @@ -79,3 +97,18 @@ def item_link(self, item): class LastTutorialsFeedATOM(LastArticlesFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description + + +class LastArticlesFeedRSS(LastContentFeedRSS): + """ + Redefinition of `LastContentFeedRSS` for articles only + """ + content_type = "ARTICLE" + link = "/articles/" + title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) + description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) + + +class LastArticlesFeedATOM(LastArticlesFeedRSS): + feed_type = Atom1Feed + subtitle = LastArticlesFeedRSS.description diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index afb9289fd3..24544ea3ac 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -59,6 +59,8 @@ class Meta: verbose_name = 'Container' verbose_name_plural = 'Containers' + # TODO: clear all database related information ? + title = models.CharField('Titre', max_length=80) slug = models.SlugField(max_length=80) @@ -92,7 +94,7 @@ class Meta: def get_children(self): """ - :return: children of this Container, ordered by position + :return: children of this container, ordered by position """ if self.has_extract(): return Extract.objects.filter(container_pk=self.pk) @@ -101,13 +103,13 @@ def get_children(self): def has_extract(self): """ - :return: `True` if the Container has extract as children, `False` otherwise. + :return: `True` if the container has extract as children, `False` otherwise. """ return Extract.objects.filter(parent=self).count() > 0 def has_sub_container(self): """ - :return: `True` if the Container has other Containers as children, `False` otherwise. + :return: `True` if the container has other Containers as children, `False` otherwise. """ return Container.objects.filter(container=self).count() > 0 @@ -136,7 +138,7 @@ def get_tree_depth(self): def add_container(self, container): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. - :param container: the new Container + :param container: the new container """ if not self.has_extract(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: @@ -182,7 +184,7 @@ def update_children(self): def add_extract(self, extract): """ Add a child container, but only if no container were previously added - :param extract: the new Extract + :param extract: the new extract """ if not self.has_sub_container(): extract.container = self @@ -270,6 +272,8 @@ class Meta: is_locked = models.BooleanField('Est verrouillé', default=False) js_support = models.BooleanField('Support du Javascript', default=False) + # TODO : split this class in two part (one for the DB object, another one for JSON [versionned] file) ? + def __unicode__(self): return self.title @@ -747,6 +751,8 @@ class Meta: verbose_name = 'Extrait' verbose_name_plural = 'Extraits' + # TODO: clear all database related information ? + title = models.CharField('Titre', max_length=80) container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) position_in_container = models.IntegerField('Position dans le parent', db_index=True) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 5ed99a824f..52e5f1e770 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -45,7 +45,7 @@ from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ mark_read, ContentReaction, HelpWriting from zds.gallery.models import Gallery, UserGallery, Image -from .models import PublishableContent +from models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -61,21 +61,25 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView, UpdateView +from django.views.generic import ListView, DetailView# , UpdateView class ArticleList(ListView): - - """Displays the list of published articles.""" + """ + Displays the list of published articles. + """ context_object_name = 'articles' paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type="ARTICLE" + type = "ARTICLE" template_name = 'article/index.html' tag = None def get_queryset(self): - """filter the content to obtain the list of only articles. If tag parameter is provided, only article - which has this category will be listed.""" + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ if self.request.GET.get('tag') is not None: self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ @@ -94,7 +98,7 @@ class TutorialList(ArticleList): """Displays the list of published tutorials.""" context_object_name = 'tutorials' - type="TUTO" + type = "TUTORIAL" template_name = 'tutorial/index.html' From d24c979106e0cbb442d457ecee8a9c05723acb2a Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 15:24:51 +0100 Subject: [PATCH 095/887] travail sur l'affichage DetailView --- zds/tutorialv2/views.py | 121 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 52e5f1e770..bc6ae57810 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,8 +42,8 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import Tutorial, Part, Chapter, Extract, Validation, never_read, \ - mark_read, ContentReaction, HelpWriting +from models import PublishableContent, Container, Extract, Validation +from utils import never_read from zds.gallery.models import Gallery, UserGallery, Image from models import PublishableContent from zds.member.decorator import can_write_and_read_now @@ -208,6 +208,123 @@ def reservation(request, validation_pk): "?version=" + validation.version ) +class DisplayContent(DetailView): + model = PublishableContent + type = "TUTO" + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p + + cpt_c = 1 + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary + chapter["slug"] = slugify(chapter["title"]) + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + self.compatibility_chapter(content, repo, sha, chapter) + cpt_c += 1 + + def compatibility_chapter(self,content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = get_blob(repo.commit(sha).tree, + "introduction.md") + dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" + ) + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_path() + ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) + cpt += 1 + + + def get_object(self): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + + def get_context_data(self, **kwargs): + """Show the given offline tutorial if exists.""" + + context = super(DisplayContent, self).get_context_data(**kwargs) + content = context[self.context_object_name] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the content. + + try: + sha = self.request.GET.get("version") + except KeyError: + sha = content.sha_draft + + # check that if we ask for beta, we also ask for the sha version + is_beta = (sha == content.sha_beta and content.in_beta()) + + # Only authors of the tutorial and staff can view tutorial in offline. + + if self.request.user not in content.authors.all() and not is_beta: + # if we are not author of this content or if we did not ask for beta + # the only members that can display and modify the tutorial are validators + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + + + # Find the good manifest file + + repo = Repo(content.get_path()) + + # Load the tutorial. + + manifest = get_blob(repo.commit(sha).tree, "manifest.json") + mandata = json_reader.loads(manifest) + content.load_dic(mandata, sha) + content.load_introduction_and_conclusion(mandata, sha) + children_tree = {} + + if 'chapter' in mandata: + # compatibility with old "Mini Tuto" + self.compatibility_chapter(content, repo, sha, mandata["chapter"]) + children_tree = mandata['chapter'] + elif 'parts' in mandata: + # compatibility with old "big tuto". + parts = mandata["parts"] + cpt_p = 1 + for part in parts: + self.compatibility_parts(content, repo, sha, part, cpt_p) + cpt_p += 1 + children_tree = parts + validation = Validation.objects.filter(tutorial__pk=content.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": content.js_support}) + + if content.source: + form_ask_validation = AskValidationForm(initial={"source": content.source}) + form_valid = ValidForm(initial={"source": content.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + if content.js_support: + is_js = "js" + else: + is_js = "" + context["tutorial"] = mandata # TODO : change to "content" + context["children"] = children_tree + context["version"] = sha + context["validation"] = validation + context["formAskValidation"] = form_ask_validation + context["formJs"] = form_js + context["formValid"] = form_valid + context["formReject"] = form_reject, + context["is_js"] = is_js + + return context + @login_required def diff(request, tutorial_pk, tutorial_slug): From 258ca2a7d13603334926145a8337baa86ff9631d Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 15:29:34 +0100 Subject: [PATCH 096/887] merge conflict --- zds/tutorialv2/views.py | 195 ++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index bc6ae57810..1d9f7227ff 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -114,103 +114,9 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) -# Staff actions. - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -def list_validation(request): - """Display tutorials list in validation.""" - - # Retrieve type of the validation. Default value is all validations. - - try: - type = request.GET["type"] - except KeyError: - type = None - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - - # Orphan validation. There aren't validator attached to the validations. - - if type == "orphan": - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=True, - status="PENDING").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=True, - status="PENDING", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - elif type == "reserved": - - # Reserved validation. There are a validator attached to the - # validations. - - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=False, - status="PENDING_V").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=False, - status="PENDING_V", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - else: - - # Default, we display all validations. - - if subcategory is None: - validations = Validation.objects.filter( - Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() - else: - validations = Validation.objects.filter(Q(status="PENDING") - | Q(status="PENDING_V" - )).filter(tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition")\ - .all() - return render(request, "tutorial/validation/index.html", - {"validations": validations}) - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -@require_POST -def reservation(request, validation_pk): - """Display tutorials list in validation.""" - - validation = get_object_or_404(Validation, pk=validation_pk) - if validation.validator: - validation.validator = None - validation.date_reserve = None - validation.status = "PENDING" - validation.save() - messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) - return redirect(reverse("zds.tutorial.views.list_validation")) - else: - validation.validator = request.user - validation.date_reserve = datetime.now() - validation.status = "PENDING_V" - validation.save() - messages.info(request, - _(u"Le tutoriel a bien été \ - réservé par {0}.").format(request.user.username)) - return redirect( - validation.content.get_absolute_url() + - "?version=" + validation.version - ) - class DisplayContent(DetailView): model = PublishableContent - type = "TUTO" + type = "TUTORIAL" def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content @@ -326,6 +232,105 @@ def get_context_data(self, **kwargs): return context +class DisplayArticle(DisplayContent): + type = "Article" + + +# Staff actions. + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +def list_validation(request): + """Display tutorials list in validation.""" + + # Retrieve type of the validation. Default value is all validations. + + try: + type = request.GET["type"] + except KeyError: + type = None + + # Get subcategory to filter validations. + + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + + # Orphan validation. There aren't validator attached to the validations. + + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": + + # Reserved validation. There are a validator attached to the + # validations. + + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + else: + + # Default, we display all validations. + + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) + + +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" + + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.content.get_absolute_url() + + "?version=" + validation.version + ) + + @login_required def diff(request, tutorial_pk, tutorial_slug): try: From cc6c6f82a17330e56baaf282f15ed948db82df20 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 16:05:34 +0100 Subject: [PATCH 097/887] =?UTF-8?q?rend=20param=C3=A9trable=20la=20longeur?= =?UTF-8?q?=20des=20feeds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/settings.py | 3 ++- zds/tutorialv2/feeds.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zds/settings.py b/zds/settings.py index 454e4e4e92..90f40d7651 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -454,7 +454,8 @@ 'home_number': 5, 'helps_per_page': 20, 'max_tree_depth': 3, - 'content_per_page': 50 + 'content_per_page': 50, + 'feed_length': 5 }, 'forum': { 'posts_per_page': 21, diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index c2cc90b347..c5b48eb2c1 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -6,7 +6,7 @@ from django.utils.feedgenerator import Atom1Feed from models import PublishableContent - +from zds.settings import ZDS_APP class LastContentFeedRSS(Feed): """ @@ -19,13 +19,13 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last 5 contents (sorted by publication date). If `self.type` is not `None`, the contents will only + :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: contents.filter(type=self.content_type) - return contents.order_by('-pubdate')[:5] + return contents.order_by('-pubdate')[:ZDS_APP['tutorial']['feed_length']] def item_title(self, item): return item.title From 8a1a25fb4eccaf5dc5603af84db960ba869c6ec2 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 26 Dec 2014 20:56:23 +0100 Subject: [PATCH 098/887] Migration online_view --- zds/tutorialv2/models.py | 35 +-- zds/tutorialv2/views.py | 450 ++++++++++++++------------------------- 2 files changed, 159 insertions(+), 326 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 24544ea3ac..385fbfefee 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -326,7 +326,7 @@ def in_drafting(self): # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') - def on_line(self): + def is_online(self): """ A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise @@ -404,7 +404,7 @@ def load_dic(self, mandata, sha=None): mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.on_line() and self.sha_public == sha + mandata['is_on_line'] = self.is_online() and self.sha_public == sha # url: mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) @@ -456,31 +456,6 @@ def load_json_for_public(self, sha=None): if 'licence' in data: data['licence'] = Licence.objects.filter(code=data['licence']).first() return data - # TODO: redundant with next function - - def load_json(self, path=None, online=False): - """ - Fetch a specific version of the JSON file for this content. - :param sha: version - :param path: path to the repository. If None, the `get_[prod_]path()` function is used - :param public: if `True`fetch the public version instead of the private one - :return: a dictionary containing the structure of the JSON file. - """ - if path is None: - if online: - man_path = os.path.join(self.get_prod_path(), 'manifest.json') - else: - man_path = os.path.join(self.get_path(), 'manifest.json') - else: - man_path = path - - if os.path.isfile(man_path): - json_data = open(man_path) - data = json_reader.load(json_data) - json_data.close() - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data def dump_json(self, path=None): """ @@ -522,7 +497,7 @@ def get_introduction_online(self): Get introduction content of the public version :return: the introduction (as a string) """ - if self.on_line(): + if self.is_online(): intro = open( os.path.join( self.get_prod_path(), @@ -558,7 +533,7 @@ def get_conclusion_online(self): Get conclusion content of the public version :return: the conclusion (as a string) """ - if self.on_line(): + if self.is_online(): conclusion = open( os.path.join( self.get_prod_path(), @@ -579,7 +554,7 @@ def delete_entity_and_tree(self): if self.gallery is not None: self.gallery.delete() - if self.on_line(): + if self.is_online(): shutil.rmtree(self.get_prod_path()) self.delete() # TODO: should use the "git" version of `delete()` !!! diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 1d9f7227ff..de4cfd43d2 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,10 +42,9 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation -from utils import never_read +from models import PublishableContent, Container, Extract, Validation, ContentRead, ContentReaction +from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image -from models import PublishableContent from zds.member.decorator import can_write_and_read_now from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip @@ -115,8 +114,12 @@ def render_chapter_form(chapter): class DisplayContent(DetailView): + """Base class that can show any content in any state, by default it shows offline tutorials""" + model = PublishableContent + template_name = 'tutorial/view.html' type = "TUTORIAL" + is_public = False def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content @@ -149,12 +152,33 @@ def compatibility_chapter(self,content, repo, sha, dictionary): ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt += 1 + def get_forms(self, context, content): + """get all the auxiliary forms about validation, js fiddle...""" + validation = Validation.objects.filter(tutorial__pk=content.pk)\ + .order_by("-date_proposition")\ + .first() + form_js = ActivJsForm(initial={"js_support": content.js_support}) + + if content.source: + form_ask_validation = AskValidationForm(initial={"source": content.source}) + form_valid = ValidForm(initial={"source": content.source}) + else: + form_ask_validation = AskValidationForm() + form_valid = ValidForm() + form_reject = RejectForm() + + context["validation"] = validation + context["formAskValidation"] = form_ask_validation + context["formJs"] = form_js + context["formValid"] = form_valid + context["formReject"] = form_reject, + def get_object(self): return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) def get_context_data(self, **kwargs): - """Show the given offline tutorial if exists.""" + """Show the given tutorial if exists.""" context = super(DisplayContent, self).get_context_data(**kwargs) content = context[self.context_object_name] @@ -164,30 +188,32 @@ def get_context_data(self, **kwargs): try: sha = self.request.GET.get("version") except KeyError: - sha = content.sha_draft + if self.sha is not None: + sha = self.sha + else: + sha = content.sha_draft # check that if we ask for beta, we also ask for the sha version is_beta = (sha == content.sha_beta and content.in_beta()) - + # check that if we ask for public version, we also ask for the sha version + is_online = (sha == content.sha_public and content.is_online()) # Only authors of the tutorial and staff can view tutorial in offline. - if self.request.user not in content.authors.all() and not is_beta: + if self.request.user not in content.authors.all() and not is_beta and not is_online: # if we are not author of this content or if we did not ask for beta # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Find the good manifest file repo = Repo(content.get_path()) # Load the tutorial. - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) + mandata = content.load_json_for_public(sha) content.load_dic(mandata, sha) - content.load_introduction_and_conclusion(mandata, sha) + content.load_introduction_and_conclusion(mandata, sha, sha == content.sha_public) children_tree = {} if 'chapter' in mandata: @@ -202,38 +228,132 @@ def get_context_data(self, **kwargs): self.compatibility_parts(content, repo, sha, part, cpt_p) cpt_p += 1 children_tree = parts - validation = Validation.objects.filter(tutorial__pk=content.pk)\ - .order_by("-date_proposition")\ - .first() - form_js = ActivJsForm(initial={"js_support": content.js_support}) - - if content.source: - form_ask_validation = AskValidationForm(initial={"source": content.source}) - form_valid = ValidForm(initial={"source": content.source}) - else: - form_ask_validation = AskValidationForm() - form_valid = ValidForm() - form_reject = RejectForm() + # check whether this tuto support js fiddle if content.js_support: is_js = "js" else: is_js = "" + context["is_js"] = is_js context["tutorial"] = mandata # TODO : change to "content" context["children"] = children_tree context["version"] = sha - context["validation"] = validation - context["formAskValidation"] = form_ask_validation - context["formJs"] = form_js - context["formValid"] = form_valid - context["formReject"] = form_reject, - context["is_js"] = is_js + self.get_forms(context, content) return context class DisplayArticle(DisplayContent): - type = "Article" + type = "ARTICLE" + + +class DisplayOnlineContent(DisplayContent): + """Display online tutorial""" + type = "TUTORIAL" + template_name = "tutorial/view_online.html" + + def get_forms(self, context, content): + + # Build form to send a note for the current tutorial. + context['form'] = NoteForm(content, self.request.user) + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p + + cpt_c = 1 + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary + chapter["slug"] = slugify(chapter["title"]) + chapter["position_in_part"] = cpt_c + chapter["position_in_tutorial"] = cpt_c * cpt_p + self.compatibility_chapter(content, repo, sha, chapter) + cpt_c += 1 + + def compatibility_chapter(self,content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_prod_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), + "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), + "conclusion.md" + ".html"), "r") + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_prod_path() + text = open(os.path.join(content.get_prod_path(), ext["text"] + + ".html"), "r") + ext["txt"] = text.read() + cpt += 1 + + def get_context_data(self, **kwargs): + content = self.get_object() + # If the tutorial isn't online, we raise 404 error. + if not content.is_online(): + raise Http404 + self.sha = content.sha_public + context = super(DisplayOnlineContent, self).get_context_data(**kwargs) + + context["tutorial"]["update"] = content.update + context["tutorial"]["get_note_count"] = content.get_note_count() + + if self.request.user.is_authenticated(): + # If the user is authenticated, he may want to tell the world how cool the content is + # We check if he can post a not or not with + # antispam filter. + context['tutorial']['antispam'] = content.antispam() + + # If the user has never read this before, we mark this tutorial read. + if never_read(content): + mark_read(content) + + # Find all notes of the tutorial. + + notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() + + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. + + last_note_pk = 0 + if content.last_note: + last_note_pk = content.last_note.pk + + # Handle pagination + + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) + try: + page_nbr = int(self.request.GET.get("page")) + except KeyError: + page_nbr = 1 + except ValueError: + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + + res = [] + if page_nbr != 1: + + # Show the last note of the previous page + + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) + + context['notes'] = res + context['last_note_pk'] = last_note_pk + context['pages'] = paginator_range(page_nbr, paginator.num_pages) + context['nb'] = page_nbr # Staff actions. @@ -917,268 +1037,6 @@ def modify_tutorial(request): raise PermissionDenied -# Tutorials. - - -@login_required -def view_tutorial(request, tutorial_pk, tutorial_slug): - """Show the given offline tutorial if exists.""" - - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - - # Retrieve sha given by the user. This sha must to be exist. If it doesn't - # exist, we take draft version of the article. - - try: - sha = request.GET["version"] - except KeyError: - sha = tutorial.sha_draft - - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() - - # Only authors of the tutorial and staff can view tutorial in offline. - - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - - # Two variables to handle two distinct cases (large/small tutorial) - - chapter = None - parts = None - - # Find the good manifest file - - repo = Repo(tutorial.get_path()) - - # Load the tutorial. - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha) - tutorial.load_introduction_and_conclusion(mandata, sha) - - # If it's a small tutorial, fetch its chapter - - if tutorial.type == "MINI": - if 'chapter' in mandata: - chapter = mandata["chapter"] - chapter["path"] = tutorial.get_path() - chapter["type"] = "MINI" - chapter["pk"] = Chapter.objects.get(tutorial=tutorial).pk - chapter["intro"] = get_blob(repo.commit(sha).tree, - "introduction.md") - chapter["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" - ) - cpt = 1 - for ext in chapter["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = tutorial.get_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt += 1 - else: - chapter = None - else: - - # If it's a big tutorial, fetch parts. - - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - part["tutorial"] = tutorial - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt_e += 1 - cpt_c += 1 - cpt_p += 1 - validation = Validation.objects.filter(tutorial__pk=tutorial.pk)\ - .order_by("-date_proposition")\ - .first() - form_js = ActivJsForm(initial={"js_support": tutorial.js_support}) - - if tutorial.source: - form_ask_validation = AskValidationForm(initial={"source": tutorial.source}) - form_valid = ValidForm(initial={"source": tutorial.source}) - else: - form_ask_validation = AskValidationForm() - form_valid = ValidForm() - form_reject = RejectForm() - - if tutorial.js_support: - is_js = "js" - else: - is_js = "" - return render(request, "tutorial/tutorial/view.html", { - "tutorial": mandata, - "chapter": chapter, - "parts": parts, - "version": sha, - "validation": validation, - "formAskValidation": form_ask_validation, - "formJs": form_js, - "formValid": form_valid, - "formReject": form_reject, - "is_js": is_js - }) - - -def view_tutorial_online(request, tutorial_pk, tutorial_slug): - """Display a tutorial.""" - - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - - # If the tutorial isn't online, we raise 404 error. - if not tutorial.on_line(): - raise Http404 - - # Two variables to handle two distinct cases (large/small tutorial) - - chapter = None - parts = None - - # find the good manifest file - - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - tutorial.load_introduction_and_conclusion(mandata, public=True) - mandata["update"] = tutorial.update - mandata["get_note_count"] = tutorial.get_note_count() - - # If it's a small tutorial, fetch its chapter - - if tutorial.type == "MINI": - if "chapter" in mandata: - chapter = mandata["chapter"] - chapter["path"] = tutorial.get_prod_path() - chapter["type"] = "MINI" - intro = open(os.path.join(tutorial.get_prod_path(), - mandata["introduction"] + ".html"), "r") - chapter["intro"] = intro.read() - intro.close() - conclu = open(os.path.join(tutorial.get_prod_path(), - mandata["conclusion"] + ".html"), "r") - chapter["conclu"] = conclu.read() - conclu.close() - cpt = 1 - for ext in chapter["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = tutorial.get_prod_path() - text = open(os.path.join(tutorial.get_prod_path(), ext["text"] - + ".html"), "r") - ext["txt"] = text.read() - text.close() - cpt += 1 - else: - chapter = None - else: - - # chapter = Chapter.objects.get(tutorial=tutorial) - - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - part["tutorial"] = mandata - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - cpt_e += 1 - cpt_c += 1 - part["get_chapters"] = part["chapters"] - cpt_p += 1 - - mandata['get_parts'] = parts - - # If the user is authenticated - if request.user.is_authenticated(): - # We check if he can post a tutorial or not with - # antispam filter. - mandata['antispam'] = tutorial.antispam() - - # If the user is never read, we mark this tutorial read. - if never_read(tutorial): - mark_read(tutorial) - - # Find all notes of the tutorial. - - notes = ContentReaction.objects.filter(tutorial__pk=tutorial.pk).order_by("position").all() - - # Retrieve pk of the last note. If there aren't notes for the tutorial, we - # initialize this last note at 0. - - last_note_pk = 0 - if tutorial.last_note: - last_note_pk = tutorial.last_note.pk - - # Handle pagination - - paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) - try: - page_nbr = int(request.GET["page"]) - except KeyError: - page_nbr = 1 - except ValueError: - raise Http404 - - try: - notes = paginator.page(page_nbr) - except PageNotAnInteger: - notes = paginator.page(1) - except EmptyPage: - raise Http404 - - res = [] - if page_nbr != 1: - - # Show the last note of the previous page - - last_page = paginator.page(page_nbr - 1).object_list - last_note = last_page[len(last_page) - 1] - res.append(last_note) - for note in notes: - res.append(note) - - # Build form to send a note for the current tutorial. - - form = NoteForm(tutorial, request.user) - return render(request, "tutorial/tutorial/view_online.html", { - "tutorial": mandata, - "chapter": chapter, - "parts": parts, - "notes": res, - "pages": paginator_range(page_nbr, paginator.num_pages), - "nb": page_nbr, - "last_note_pk": last_note_pk, - "form": form, - }) - @can_write_and_read_now @login_required @@ -1384,7 +1242,7 @@ def edit_tutorial(request): tutorial.save() return redirect(tutorial.get_absolute_url()) else: - json = tutorial.load_json() + json = tutorial.load_json_for_public(tutorial.sha_draft) if "licence" in json: licence = json['licence'] else: @@ -1497,7 +1355,7 @@ def view_part_online( """Display a part.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.on_line(): + if not tutorial.is_online(): raise Http404 # find the good manifest file @@ -1868,7 +1726,7 @@ def view_chapter_online( """View chapter.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.on_line(): + if not tutorial.is_online(): raise Http404 # find the good manifest file @@ -3077,7 +2935,7 @@ def download(request): repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) sha = tutorial.sha_draft - if 'online' in request.GET and tutorial.on_line(): + if 'online' in request.GET and tutorial.is_online(): sha = tutorial.sha_public elif request.user not in tutorial.authors.all(): if not request.user.has_perm('tutorial.change_tutorial'): From 8daf4aab74fda01fdfe837db5a9ec59a09d05a71 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 11:23:19 +0100 Subject: [PATCH 099/887] =?UTF-8?q?compatibilit=C3=A9=20ZEP03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 385fbfefee..ed3343f8de 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -25,6 +25,7 @@ from zds.utils.models import SubCategory, Licence, Comment from zds.utils.tutorials import get_blob, export_tutorial from zds.settings import ZDS_APP +from zds.utils.models import HelpWriting TYPE_CHOICES = ( @@ -256,8 +257,10 @@ class Meta: licence = models.ForeignKey(Licence, verbose_name='Licence', blank=True, null=True, db_index=True) - # as of ZEP 12 this fiels is no longer the size but the type of content (article/tutorial) + # as of ZEP 12 this field is no longer the size but the type of content (article/tutorial) type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) + #zep03 field + helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) images = models.CharField( 'chemin relatif images', From 9089d1644b35c00f3062e5c88d34c4aa81221cdb Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 12:27:21 +0100 Subject: [PATCH 100/887] =?UTF-8?q?compatibilit=C3=A9=20ZEP03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/urls.py | 24 +++++++++++++++--------- zds/tutorialv2/views.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 97a24e5ce3..9871095ea8 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -21,28 +21,34 @@ url(r'^recherche/(?P\d+)/$', 'zds.tutorial.views.find_tuto'), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_chapter', name="view-chapter-url"), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_part', name="view-part-url"), - url(r'^off/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_tutorial'), + url(r'^tutorial/off/(?P\d+)/(?P.+)/$', + DisplayContent.as_view()), + + url(r'^article/off/(?P\d+)/(?P.+)/$', + DisplayArticle.as_view()), # View online - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_chapter_online', name="view-chapter-url-online"), - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', 'zds.tutorial.views.view_part_online', name="view-part-url-online"), - url(r'^(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_tutorial_online'), + url(r'^tutoriel/(?P\d+)/(?P.+)/$', + DisplayOnlineContent.as_view()), + + url(r'^article/(?P\d+)/(?P.+)/$', + DisplayOnlineArticle.as_view()), # Editing url(r'^editer/tutoriel/$', @@ -125,6 +131,6 @@ # Help url(r'^aides/$', - 'zds.tutorial.views.help_tutorial'), + TutorialWithHelp.as_view()), ) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index de4cfd43d2..5fe7a3a922 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -52,7 +52,7 @@ from zds.utils import slugify from zds.utils.models import Alert from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ - SubCategory + SubCategory, HelpWriting from zds.utils.mps import send_mp from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic from zds.utils.paginator import paginator_range @@ -113,6 +113,29 @@ def render_chapter_form(chapter): "conclusion": chapter.get_conclusion()}) +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorial/help.html' + + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects.exclude(Q(helps_count=0) and Q(sha_beta='')) + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context + + class DisplayContent(DetailView): """Base class that can show any content in any state, by default it shows offline tutorials""" @@ -356,6 +379,10 @@ def get_context_data(self, **kwargs): context['nb'] = page_nbr +class DisplayOnlineArticle(DisplayOnlineContent): + type = "ARTICLE" + + # Staff actions. From 56557f89f65cb4863d6f32304facbb6e723285c6 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sat, 27 Dec 2014 14:13:23 +0100 Subject: [PATCH 101/887] Separation des objets "BDD" et "JSON" --- zds/tutorialv2/admin.py | 4 +- zds/tutorialv2/feeds.py | 41 +- ...t__del_field_validation_tutorial__add_f.py | 262 ++++++ zds/tutorialv2/models.py | 781 ++++++++---------- zds/tutorialv2/tests/__init__.py | 0 zds/tutorialv2/tests/tests_models.py | 32 + zds/tutorialv2/views.py | 47 +- zds/utils/tutorialv2.py | 55 ++ 8 files changed, 733 insertions(+), 489 deletions(-) create mode 100644 zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py create mode 100644 zds/tutorialv2/tests/__init__.py create mode 100644 zds/tutorialv2/tests/tests_models.py create mode 100644 zds/utils/tutorialv2.py diff --git a/zds/tutorialv2/admin.py b/zds/tutorialv2/admin.py index e966d5f7f4..79f515425f 100644 --- a/zds/tutorialv2/admin.py +++ b/zds/tutorialv2/admin.py @@ -2,11 +2,9 @@ from django.contrib import admin -from .models import PublishableContent, Container, Extract, Validation, ContentReaction +from .models import PublishableContent, Validation, ContentReaction admin.site.register(PublishableContent) -admin.site.register(Container) -admin.site.register(Extract) admin.site.register(Validation) admin.site.register(ContentReaction) diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index c5b48eb2c1..6f92671c3e 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -8,6 +8,7 @@ from models import PublishableContent from zds.settings import ZDS_APP + class LastContentFeedRSS(Feed): """ RSS feed for any type of content. @@ -19,8 +20,8 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the contents will only - be of this type. + :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the + contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: @@ -62,42 +63,6 @@ class LastTutorialsFeedATOM(LastTutorialsFeedRSS): feed_type = Atom1Feed subtitle = LastTutorialsFeedRSS.description -class LastArticlesFeedRSS(Feed): - title = u"Articles sur {}".format(settings.ZDS_APP['site']['litteral_name']) - link = "/articles/" - description = u"Les derniers articles parus sur {}.".format(settings.ZDS_APP['site']['litteral_name']) - - def items(self): - return PublishableContent.objects\ - .filter(type="ARTICLE")\ - .filter(sha_public__isnull=False)\ - .order_by('-pubdate')[:5] - - def item_title(self, item): - return item.title - - def item_pubdate(self, item): - return item.pubdate - - def item_description(self, item): - return item.description - - def item_author_name(self, item): - authors_list = item.authors.all() - authors = [] - for authors_obj in authors_list: - authors.append(authors_obj.username) - authors = ", ".join(authors) - return authors - - def item_link(self, item): - return item.get_absolute_url_online() - - -class LastTutorialsFeedATOM(LastArticlesFeedRSS): - feed_type = Atom1Feed - subtitle = LastTutorialsFeedRSS.description - class LastArticlesFeedRSS(LastContentFeedRSS): """ diff --git a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py new file mode 100644 index 0000000000..a979a2e9df --- /dev/null +++ b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting model 'Container' + db.delete_table(u'tutorialv2_container') + + # Deleting model 'Extract' + db.delete_table(u'tutorialv2_extract') + + # Deleting field 'Validation.tutorial' + db.delete_column(u'tutorialv2_validation', 'tutorial_id') + + # Adding field 'Validation.content' + db.add_column(u'tutorialv2_validation', 'content', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), + keep_default=False) + + # Deleting field 'PublishableContent.container_ptr' + db.delete_column(u'tutorialv2_publishablecontent', u'container_ptr_id') + + # Deleting field 'PublishableContent.images' + db.delete_column(u'tutorialv2_publishablecontent', 'images') + + # Adding field 'PublishableContent.id' + db.add_column(u'tutorialv2_publishablecontent', u'id', + self.gf('django.db.models.fields.AutoField')(default=0, primary_key=True), + keep_default=False) + + # Adding field 'PublishableContent.title' + db.add_column(u'tutorialv2_publishablecontent', 'title', + self.gf('django.db.models.fields.CharField')(default='', max_length=80), + keep_default=False) + + # Adding field 'PublishableContent.relative_images_path' + db.add_column(u'tutorialv2_publishablecontent', 'relative_images_path', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), + keep_default=False) + + # Deleting field 'ContentRead.tutorial' + db.delete_column(u'tutorialv2_contentread', 'tutorial_id') + + # Adding field 'ContentRead.content' + db.add_column(u'tutorialv2_contentread', 'content', + self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['tutorialv2.PublishableContent']), + keep_default=False) + + + def backwards(self, orm): + # Adding model 'Container' + db.create_table(u'tutorialv2_container', ( + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + )) + db.send_create_signal(u'tutorialv2', ['Container']) + + # Adding model 'Extract' + db.create_table(u'tutorialv2_extract', ( + ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal(u'tutorialv2', ['Extract']) + + # Adding field 'Validation.tutorial' + db.add_column(u'tutorialv2_validation', 'tutorial', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), + keep_default=False) + + # Deleting field 'Validation.content' + db.delete_column(u'tutorialv2_validation', 'content_id') + + + # User chose to not deal with backwards NULL issues for 'PublishableContent.container_ptr' + raise RuntimeError("Cannot reverse this migration. 'PublishableContent.container_ptr' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'PublishableContent.container_ptr' + db.add_column(u'tutorialv2_publishablecontent', u'container_ptr', + self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True), + keep_default=False) + + # Adding field 'PublishableContent.images' + db.add_column(u'tutorialv2_publishablecontent', 'images', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), + keep_default=False) + + # Deleting field 'PublishableContent.id' + db.delete_column(u'tutorialv2_publishablecontent', u'id') + + # Deleting field 'PublishableContent.title' + db.delete_column(u'tutorialv2_publishablecontent', 'title') + + # Deleting field 'PublishableContent.relative_images_path' + db.delete_column(u'tutorialv2_publishablecontent', 'relative_images_path') + + + # User chose to not deal with backwards NULL issues for 'ContentRead.tutorial' + raise RuntimeError("Cannot reverse this migration. 'ContentRead.tutorial' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'ContentRead.tutorial' + db.add_column(u'tutorialv2_contentread', 'tutorial', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent']), + keep_default=False) + + # Deleting field 'ContentRead.content' + db.delete_column(u'tutorialv2_contentread', 'content_id') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index ed3343f8de..cb3643d8c6 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -23,7 +23,8 @@ from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user from zds.utils.models import SubCategory, Licence, Comment -from zds.utils.tutorials import get_blob, export_tutorial +from zds.utils.tutorials import get_blob +from zds.utils.tutorialv2 import export_content from zds.settings import ZDS_APP from zds.utils.models import HelpWriting @@ -45,7 +46,7 @@ class InvalidOperationError(RuntimeError): pass -class Container(models.Model): +class Container: """ A container, which can have sub-Containers or Extracts. @@ -54,71 +55,54 @@ class Container(models.Model): It has also a tree depth. - There is a `compatibility_pk` for compatibility with older versions. + A container could be either a tutorial/article, a part or a chapter. """ - class Meta: - verbose_name = 'Container' - verbose_name_plural = 'Containers' - - # TODO: clear all database related information ? - - title = models.CharField('Titre', max_length=80) - - slug = models.SlugField(max_length=80) - - introduction = models.CharField( - 'chemin relatif introduction', - blank=True, - null=True, - max_length=200) - - conclusion = models.CharField( - 'chemin relatif conclusion', - blank=True, - null=True, - max_length=200) - - parent = models.ForeignKey("self", - verbose_name='Conteneur parent', - blank=True, null=True, - on_delete=models.SET_NULL) - position_in_parent = models.IntegerField(verbose_name='position dans le conteneur parent', - blank=False, - null=False, - default=1) + pk = 0 + title = '' + slug = '' + introduction = None + conclusion = None + parent = None + position_in_parent = 1 + children = [] # TODO: thumbnails ? - # integer key used to represent the tutorial or article old identifier for url compatibility - compatibility_pk = models.IntegerField(null=False, default=0) + def __init__(self, pk, title, parent=None, position_in_parent=1): + self.pk = pk + self.title = title + self.slug = slugify(title) + self.parent = parent + self.position_in_parent = position_in_parent + self.children = [] # even if you want, do NOT remove this line - def get_children(self): - """ - :return: children of this container, ordered by position - """ - if self.has_extract(): - return Extract.objects.filter(container_pk=self.pk) - - return Container.objects.filter(parent_pk=self.pk).order_by('position_in_parent') + def __unicode__(self): + return u''.format(self.title) - def has_extract(self): + def has_extracts(self): """ + Note : this function rely on the fact that the children can only be of one type. :return: `True` if the container has extract as children, `False` otherwise. """ - return Extract.objects.filter(parent=self).count() > 0 + if len(self.children) == 0: + return False + return isinstance(self.children[0], Extract) - def has_sub_container(self): + def has_sub_containers(self): """ - :return: `True` if the container has other Containers as children, `False` otherwise. + Note : this function rely on the fact that the children can only be of one type. + :return: `True` if the container has containers as children, `False` otherwise. """ - return Container.objects.filter(container=self).count() > 0 + if len(self.children) == 0: + return False + return isinstance(self.children[0], Container) def get_last_child_position(self): """ - :return: the relative position of the last child + :return: the position of the last child """ - return Container.objects.filter(parent=self).count() + Extract.objects.filter(container=self).count() + return len(self.children) def get_tree_depth(self): """ @@ -136,76 +120,330 @@ def get_tree_depth(self): depth += 1 return depth + def top_container(self): + """ + :return: Top container (for which parent is `None`) + """ + current = self + while current.parent is not None: + current = current.parent + return current + def add_container(self, container): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. :param container: the new container """ - if not self.has_extract(): + if not self.has_extracts(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: container.parent = self - container.position_in_parent = container.get_last_child_position() + 1 - container.save() + container.position_in_parent = self.get_last_child_position() + 1 + self.children.append(container) else: raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") # TODO: limitation if article ? - def get_phy_slug(self): + def add_extract(self, extract): """ - The slugified title is used to store physically the information in filesystem. - A "compatibility pk" can be used instead of real pk to ensure compatibility with previous versions. - :return: the slugified title + Add a child container, but only if no container were previously added + :param extract: the new extract """ - base = "" - if self.parent is not None: - base = self.parent.get_phy_slug() - - used_pk = self.compatibility_pk - if used_pk == 0: - used_pk = self.pk + if not self.has_sub_containers(): + extract.container = self + extract.position_in_parent = self.get_last_child_position() + 1 + self.children.append(extract) - return os.path.join(base, str(used_pk) + '_' + self.slug) + def get_phy_slug(self): + """ + :return: the physical slug, used to represent data in filesystem + """ + return str(self.pk) + '_' + self.slug def update_children(self): """ - Update all children of the container. + Update the path for introduction and conclusion for the container and all its children. If the children is an + extract, update the path to the text instead. This function is useful when `self.pk` or `self.title` has + changed. + Note : this function does not account for a different arrangement of the files. """ - for child in self.get_children(): - if child is Container: - self.introduction = os.path.join(self.get_phy_slug(), "introduction.md") - self.conclusion = os.path.join(self.get_phy_slug(), "conclusion.md") - self.save() + self.introduction = os.path.join(self.get_path(relative=True), "introduction.md") + self.conclusion = os.path.join(self.get_path(relative=True), "conclusion.md") + for child in self.children: + if isinstance(child, Container): child.update_children() - else: + elif isinstance(child, Extract): child.text = child.get_path(relative=True) - child.save() - def add_extract(self, extract): + def get_path(self, relative=False): """ - Add a child container, but only if no container were previously added - :param extract: the new extract + Get the physical path to the draft version of the container. + Note: this function rely on the fact that the top container is VersionedContainer. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path """ - if not self.has_sub_container(): - extract.container = self - extract.save() + base = '' + if self.parent: + base = self.parent.get_path(relative=relative) + return os.path.join(base, self.get_phy_slug()) + + def get_prod_path(self): + """ + Get the physical path to the public version of the container. + Note: this function rely on the fact that the top container is VersionedContainer. + :return: physical path + """ + base = '' + if self.parent: + base = self.parent.get_prod_path() + return os.path.join(base, self.get_phy_slug()) + + def get_introduction(self): + """ + :return: the introduction from the file in `self.introduction` + """ + if self.introduction: + return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, + self.introduction) + + def get_introduction_online(self): + """ + Get introduction content of the public version + :return: the introduction + """ + path = self.top_container().get_prod_path() + self.introduction + '.html' + if os.path.exists(path): + intro = open(path) + intro_content = intro.read() + intro.close() + return intro_content.decode('utf-8') + + def get_conclusion(self): + """ + :return: the conclusion from the file in `self.conclusion` + """ + if self.introduction: + return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, + self.conclusion) + + def get_conclusion_online(self): + """ + Get conclusion content of the public version + :return: the conclusion + """ + path = self.top_container().get_prod_path() + self.conclusion + '.html' + if os.path.exists(path): + conclusion = open(path) + conclusion_content = conclusion.read() + conclusion.close() + return conclusion_content.decode('utf-8') + # TODO: - # - rewrite save() - # - get_absolute_url_*() stuffs, get_path(), get_prod_path() - # - __unicode__() - # - get_introduction_*(), get_conclusion_*() - # - a `top_parent()` function to access directly to the parent PublishableContent and avoid the - # `container.parent.parent.parent` stuff ? - # - a nice `delete_entity_and_tree()` function ? (which also remove the file) + # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) # - the `maj_repo_*()` stuffs should probably be into the model ? -class PublishableContent(Container): +class Extract: + """ + A content extract from a Container. + + It has a title, a position in the parent container and a text. + """ + + title = '' + container = None + position_in_container = 1 + text = None + pk = 0 + + def __init__(self, pk, title, container=None, position_in_container=1): + self.pk = pk + self.title = title + self.container = container + self.position_in_container = position_in_container + + def __unicode__(self): + return u''.format(self.title) + + def get_absolute_url(self): + """ + :return: the url to access the tutorial offline + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url(), + self.position_in_container, + slugify(self.title) + ) + + def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_online(), + self.position_in_container, + slugify(self.title) + ) + + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + return '{0}#{1}-{2}'.format( + self.container.get_absolute_url_beta(), + self.position_in_container, + slugify(self.title) + ) + + def get_phy_slug(self): + """ + :return: the physical slug + """ + return str(self.pk) + '_' + slugify(self.title) + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the extract. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + + def get_prod_path(self): + """ + Get the physical path to the public version of a specific version of the extract. + :return: physical path + """ + return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' + + def get_text(self): + if self.text: + return get_blob( + self.container.top_container().repository.commit(self.container.top_container().current_version).tree, + self.text) + + def get_text_online(self): + path = self.container.top_container().get_prod_path() + self.text + '.html' + if os.path.exists(path): + txt = open(path) + txt_content = txt.read() + txt.close() + return txt_content.decode('utf-8') + + +class VersionedContent(Container): + """ + This class is used to handle a specific version of a tutorial. + + It is created from the "manifest.json" file, and could dump information in it. + + For simplicity, it also contains DB information (but cannot modified them!), filled at the creation. + """ + + current_version = None + repository = None + + description = '' + type = '' + licence = '' + + # Information from DB + sha_draft = None + sha_beta = None + sha_public = None + sha_validation = None + is_beta = False + is_validation = False + is_public = False + + # TODO `load_dic()` provide more information, actually + + def __init__(self, current_version, pk, _type, title): + Container.__init__(self, pk, title) + self.current_version = current_version + self.type = _type + self.repository = Repo(self.get_path()) + # so read JSON ? + + def __unicode__(self): + return self.title + + def get_absolute_url(self): + """ + :return: the url to access the tutorial when offline + """ + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) + + def get_absolute_url_online(self): + """ + :return: the url to access the tutorial when online + """ + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) + + def get_absolute_url_beta(self): + """ + :return: the url to access the tutorial when in beta + """ + if self.is_beta: + return self.get_absolute_url() + '?version=' + self.sha_beta + else: + return self.get_absolute_url() + + def get_edit_url(self): + """ + :return: the url to edit the tutorial + """ + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + if relative: + return '' + else: + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + + def get_prod_path(self): + """ + Get the physical path to the public version of the content + :return: physical path + """ + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.get_phy_slug()) + + def get_json(self): + """ + :return: raw JSON file + """ + dct = export_content(self) + data = json_writer.dumps(dct, indent=4, ensure_ascii=False) + return data + + def dump_json(self, path=None): + """ + Write the JSON into file + :param path: path to the file. If `None`, write in "manifest.json" + """ + if path is None: + man_path = os.path.join(self.get_path(), 'manifest.json') + else: + man_path = path + + json_data = open(man_path, "w") + json_data.write(self.get_json().encode('utf-8')) + json_data.close() + + +class PublishableContent(models.Model): """ A tutorial whatever its size or an article. - A PublishableContent is a tree depth 0 Container (no parent) with additional information, such as + A PublishableContent retains metadata about a content in database, such as + - authors, description, source (if the content comes from another website), subcategory and licence ; - Thumbnail and gallery ; - Creation, publication and update date ; @@ -216,10 +454,10 @@ class PublishableContent(Container): These are two repositories : draft and online. """ class Meta: - verbose_name = 'Tutoriel' - verbose_name_plural = 'Tutoriels' - # TODO: "Contenu" ? + verbose_name = 'Contenu' + verbose_name_plural = 'Contenus' + title = models.CharField('Titre', max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -262,12 +500,11 @@ class Meta: #zep03 field helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) - images = models.CharField( + relative_images_path = models.CharField( 'chemin relatif images', blank=True, null=True, max_length=200) - # TODO: rename this field ? (`relative_image_path` ?) last_note = models.ForeignKey('ContentReaction', blank=True, null=True, related_name='last_note', @@ -275,38 +512,9 @@ class Meta: is_locked = models.BooleanField('Est verrouillé', default=False) js_support = models.BooleanField('Support du Javascript', default=False) - # TODO : split this class in two part (one for the DB object, another one for JSON [versionned] file) ? - def __unicode__(self): return self.title - def get_absolute_url(self): - """ - :return: the url to access the tutorial when offline - """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) - - def get_absolute_url_online(self): - """ - :return: the url to access the tutorial when online - """ - return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) - - def get_absolute_url_beta(self): - """ - :return: the url to access the tutorial when in beta - """ - if self.sha_beta is not None: - return self.get_absolute_url() + '?version=' + self.sha_beta - else: - return self.get_absolute_url() - - def get_edit_url(self): - """ - :return: the url to edit the tutorial - """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) - def in_beta(self): """ A tutorial is not in beta if sha_beta is `None` or empty @@ -326,10 +534,9 @@ def in_drafting(self): A tutorial is not in draft if sha_draft is `None` or empty :return: `True` if the tutorial is in draft, `False` otherwise """ - # TODO: probably always True !! return (self.sha_draft is not None) and (self.sha_draft.strip() != '') - def is_online(self): + def in_public(self): """ A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise @@ -349,37 +556,24 @@ def is_tutorial(self): """ return self.type == 'TUTORIAL' - def get_phy_slug(self): - """ - :return: the physical slug, used to represent data in filesystem - """ - return str(self.pk) + "_" + self.slug - - def get_path(self, relative=False): - """ - Get the physical path to the draft version of the Content. - :param relative: if `True`, the path will be relative, absolute otherwise. - :return: physical path - """ - if relative: - return '' - else: - # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) - # TODO: versionning ?!? - - def get_prod_path(self, sha=None): + def load_json_for_public(self, sha=None): """ - Get the physical path to the public version of the content - :param sha: version of the content, if `None`, public version is used - :return: physical path + Fetch the public version of the JSON file for this content. + :param sha: version + :return: a dictionary containing the structure of the JSON file. """ - data = self.load_json_for_public(sha) - return os.path.join( - settings.ZDS_APP[self.type.lower()]['repo_public_path'], - str(self.pk) + '_' + slugify(data['title'])) + if sha is None: + sha = self.sha_public + repo = Repo(self.get_path()) # should be `get_prod_path()` !?! + mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') + data = json_reader.loads(mantuto) + if 'licence' in data: + data['licence'] = Licence.objects.filter(code=data['licence']).first() + return data + # TODO: mix that with next function def load_dic(self, mandata, sha=None): + # TODO should load JSON and store it in VersionedContent """ Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. :param mandata: a dictionary from JSON file @@ -407,7 +601,7 @@ def load_dic(self, mandata, sha=None): mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.is_online() and self.sha_public == sha + mandata['is_on_line'] = self.in_public() and self.sha_public == sha # url: mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) @@ -429,139 +623,6 @@ def load_dic(self, mandata, sha=None): args=[self.pk, mandata['slug']] ) - def load_introduction_and_conclusion(self, mandata, sha=None, public=False): - """ - Explicitly load introduction and conclusion to avoid useless disk access in `load_dic()` - :param mandata: dictionary from JSON file - :param sha: version - :param public: if `True`, get introduction and conclusion from the public version instead of the draft one - (`sha` is not used in this case) - """ - - if public: - mandata['get_introduction_online'] = self.get_introduction_online() - mandata['get_conclusion_online'] = self.get_conclusion_online() - else: - mandata['get_introduction'] = self.get_introduction(sha) - mandata['get_conclusion'] = self.get_conclusion(sha) - - def load_json_for_public(self, sha=None): - """ - Fetch the public version of the JSON file for this content. - :param sha: version - :return: a dictionary containing the structure of the JSON file. - """ - if sha is None: - sha = self.sha_public - repo = Repo(self.get_path()) # should be `get_prod_path()` !?! - mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') - data = json_reader.loads(mantuto) - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data - - def dump_json(self, path=None): - """ - Write the JSON into file - :param path: path to the file. If `None`, use default path. - """ - if path is None: - man_path = os.path.join(self.get_path(), 'manifest.json') - else: - man_path = path - - dct = export_tutorial(self) - data = json_writer.dumps(dct, indent=4, ensure_ascii=False) - json_data = open(man_path, "w") - json_data.write(data.encode('utf-8')) - json_data.close() - - def get_introduction(self, sha=None): - """ - Get the introduction content of a specific version - :param sha: version, if `None`, use draft one - :return: the introduction (as a string) - """ - # find hash code - if sha is None: - sha = self.sha_draft - repo = Repo(self.get_path()) - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - content_version = json_reader.loads(manifest) - if "introduction" in content_version: - path_content_intro = content_version["introduction"] - - if path_content_intro: - return get_blob(repo.commit(sha).tree, path_content_intro) - - def get_introduction_online(self): - """ - Get introduction content of the public version - :return: the introduction (as a string) - """ - if self.is_online(): - intro = open( - os.path.join( - self.get_prod_path(), - self.introduction + - '.html'), - "r") - intro_contenu = intro.read() - intro.close() - - return intro_contenu.decode('utf-8') - - def get_conclusion(self, sha=None): - """ - Get the conclusion content of a specific version - :param sha: version, if `None`, use draft one - :return: the conclusion (as a string) - """ - # find hash code - if sha is None: - sha = self.sha_draft - repo = Repo(self.get_path()) - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - content_version = json_reader.loads(manifest) - if "introduction" in content_version: - path_content_ccl = content_version["conclusion"] - - if path_content_ccl: - return get_blob(repo.commit(sha).tree, path_content_ccl) - - def get_conclusion_online(self): - """ - Get conclusion content of the public version - :return: the conclusion (as a string) - """ - if self.is_online(): - conclusion = open( - os.path.join( - self.get_prod_path(), - self.conclusion + - '.html'), - "r") - conlusion_content = conclusion.read() - conclusion.close() - - return conlusion_content.decode('utf-8') - - def delete_entity_and_tree(self): - """ - Delete the entities and their filesystem counterparts - """ - shutil.rmtree(self.get_path(), 0) - Validation.objects.filter(tutorial=self).delete() - - if self.gallery is not None: - self.gallery.delete() - if self.is_online(): - shutil.rmtree(self.get_prod_path()) - self.delete() - # TODO: should use the "git" version of `delete()` !!! - def save(self, *args, **kwargs): self.slug = slugify(self.title) @@ -678,6 +739,19 @@ def have_epub(self): """ return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) + def delete_entity_and_tree(self): + """ + Delete the entities and their filesystem counterparts + """ + shutil.rmtree(self.get_path(), False) + Validation.objects.filter(tutorial=self).delete() + + if self.gallery is not None: + self.gallery.delete() + if self.in_public(): + shutil.rmtree(self.get_prod_path()) + self.delete() + class ContentReaction(Comment): """ @@ -719,143 +793,6 @@ def __unicode__(self): return u''.format(self.content, self.user, self.note.pk) -class Extract(models.Model): - """ - A content extract from a Container. - - It has a title, a position in the parent container and a text. - """ - class Meta: - verbose_name = 'Extrait' - verbose_name_plural = 'Extraits' - - # TODO: clear all database related information ? - - title = models.CharField('Titre', max_length=80) - container = models.ForeignKey(Container, verbose_name='Chapitre parent', db_index=True) - position_in_container = models.IntegerField('Position dans le parent', db_index=True) - - text = models.CharField( - 'chemin relatif du texte', - blank=True, - null=True, - max_length=200) - - def __unicode__(self): - return u''.format(self.title) - - def get_absolute_url(self): - """ - :return: the url to access the tutorial offline - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url(), - self.position_in_container, - slugify(self.title) - ) - - def get_absolute_url_online(self): - """ - :return: the url to access the tutorial when online - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url_online(), - self.position_in_container, - slugify(self.title) - ) - - def get_absolute_url_beta(self): - """ - :return: the url to access the tutorial when in beta - """ - return '{0}#{1}-{2}'.format( - self.container.get_absolute_url_beta(), - self.position_in_container, - slugify(self.title) - ) - - def get_phy_slug(self): - """ - :return: the physical slug - """ - return str(self.pk) + '_' + slugify(self.title) - - def get_path(self, relative=False): - """ - Get the physical path to the draft version of the extract. - :param relative: if `True`, the path will be relative, absolute otherwise. - :return: physical path - """ - return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' - # TODO: versionning ? - - def get_prod_path(self, sha=None): - """ - Get the physical path to the public version of a specific version of the extract. - :param sha: version of the content, if `None`, `sha_public` is used - :return: physical path - """ - return os.path.join(self.container.get_prod_path(sha), self.get_phy_slug()) + '.md.html' - - def get_text(self, sha=None): - - if self.container.tutorial: - tutorial = self.container.tutorial - else: - tutorial = self.container.part.tutorial - repo = Repo(tutorial.get_path()) - - # find hash code - if sha is None: - sha = tutorial.sha_draft - - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - tutorial_version = json_reader.loads(manifest) - if "parts" in tutorial_version: - for part in tutorial_version["parts"]: - if "chapters" in part: - for chapter in part["chapters"]: - if "extracts" in chapter: - for extract in chapter["extracts"]: - if extract["pk"] == self.pk: - path_ext = extract["text"] - break - if "chapter" in tutorial_version: - chapter = tutorial_version["chapter"] - if "extracts" in chapter: - for extract in chapter["extracts"]: - if extract["pk"] == self.pk: - path_ext = extract["text"] - break - - if path_ext: - return get_blob(repo.commit(sha).tree, path_ext) - else: - return None - - def get_text_online(self): - - if self.container.tutorial: - path = os.path.join( - self.container.tutorial.get_prod_path(), - self.text + - '.html') - else: - path = os.path.join( - self.container.part.tutorial.get_prod_path(), - self.text + - '.html') - - if os.path.isfile(path): - text = open(path, "r") - text_contenu = text.read() - text.close() - - return text_contenu.decode('utf-8') - else: - return None - - class Validation(models.Model): """ Content validation. diff --git a/zds/tutorialv2/tests/__init__.py b/zds/tutorialv2/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py new file mode 100644 index 0000000000..ca9f27c9ed --- /dev/null +++ b/zds/tutorialv2/tests/tests_models.py @@ -0,0 +1,32 @@ +# coding: utf-8 + +# NOTE : this file is only there for tests purpose, it will be deleted in final version + +import os + +from django.test import TestCase +from django.test.utils import override_settings +from zds.settings import SITE_ROOT + +from zds.tutorialv2.models import Container, Extract, VersionedContent + + +@override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) +@override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) +@override_settings(REPO_PATH_PROD=os.path.join(SITE_ROOT, 'tutoriels-public-test')) +@override_settings(REPO_ARTICLE_PATH=os.path.join(SITE_ROOT, 'articles-data-test')) +class ContentTests(TestCase): + + def setUp(self): + self.start_version = 'ca5508a' # real version, adapt it ! + self.content = VersionedContent(self.start_version, 1, 'TUTORIAL', 'Mon tutoriel no1') + + self.container = Container(1, 'Mon chapitre no1') + self.content.add_container(self.container) + + self.extract = Extract(1, 'Un premier extrait') + self.container.add_extract(self.extract) + self.content.update_children() + + def test_workflow_content(self): + print(self.container.position_in_parent) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 5fe7a3a922..ad7a7b4199 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -42,7 +42,7 @@ from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation, ContentRead, ContentReaction +from models import PublishableContent, Container, Extract, Validation, ContentReaction # , ContentRead from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now @@ -60,7 +60,9 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView# , UpdateView +from django.views.generic import ListView, DetailView # , UpdateView +# until we completely get rid of these, import them : +from zds.tutorial.models import Tutorial, Chapter, Part, HelpWriting class ArticleList(ListView): @@ -159,15 +161,13 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - def compatibility_chapter(self,content, repo, sha, dictionary): + def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" dictionary["path"] = content.get_path() dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = get_blob(repo.commit(sha).tree, - "introduction.md") - dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md" - ) + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") + dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md") cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt @@ -191,12 +191,11 @@ def get_forms(self, context, content): form_reject = RejectForm() context["validation"] = validation - context["formAskValidation"] = form_ask_validation + context["formAskValidation"] = form_ask_validation context["formJs"] = form_js context["formValid"] = form_valid context["formReject"] = form_reject, - def get_object(self): return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) @@ -219,7 +218,7 @@ def get_context_data(self, **kwargs): # check that if we ask for beta, we also ask for the sha version is_beta = (sha == content.sha_beta and content.in_beta()) # check that if we ask for public version, we also ask for the sha version - is_online = (sha == content.sha_public and content.is_online()) + is_online = (sha == content.sha_public and content.in_public()) # Only authors of the tutorial and staff can view tutorial in offline. if self.request.user not in content.authors.all() and not is_beta and not is_online: @@ -258,7 +257,7 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = mandata # TODO : change to "content" + context["tutorial"] = mandata # TODO : change to "content" context["children"] = children_tree context["version"] = sha self.get_forms(context, content) @@ -295,28 +294,25 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - def compatibility_chapter(self,content, repo, sha, dictionary): + def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" dictionary["path"] = content.get_prod_path() dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = open(os.path.join(content.get_prod_path(), - "introduction.md" + ".html"), "r") - dictionary["conclu"] = open(os.path.join(content.get_prod_path(), - "conclusion.md" + ".html"), "r") + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt ext["path"] = content.get_prod_path() - text = open(os.path.join(content.get_prod_path(), ext["text"] - + ".html"), "r") + text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") ext["txt"] = text.read() cpt += 1 def get_context_data(self, **kwargs): content = self.get_object() - # If the tutorial isn't online, we raise 404 error. - if not content.is_online(): + # If the tutorial isn't online, we raise 404 error. + if not content.in_public(): raise Http404 self.sha = content.sha_public context = super(DisplayOnlineContent, self).get_context_data(**kwargs) @@ -1064,7 +1060,6 @@ def modify_tutorial(request): raise PermissionDenied - @can_write_and_read_now @login_required def add_tutorial(request): @@ -1382,7 +1377,7 @@ def view_part_online( """Display a part.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.is_online(): + if not tutorial.in_public(): raise Http404 # find the good manifest file @@ -1753,7 +1748,7 @@ def view_chapter_online( """View chapter.""" tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if not tutorial.is_online(): + if not tutorial.in_public(): raise Http404 # find the good manifest file @@ -2962,7 +2957,7 @@ def download(request): repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) sha = tutorial.sha_draft - if 'online' in request.GET and tutorial.is_online(): + if 'online' in request.GET and tutorial.in_public(): sha = tutorial.sha_public elif request.user not in tutorial.authors.all(): if not request.user.has_perm('tutorial.change_tutorial'): diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py new file mode 100644 index 0000000000..6959b0c60d --- /dev/null +++ b/zds/utils/tutorialv2.py @@ -0,0 +1,55 @@ +from collections import OrderedDict + + +def export_extract(extract): + """ + Export an extract to a dictionary + :param extract: extract to export + :return: dictionary containing the information + """ + dct = OrderedDict() + dct['pk'] = extract.pk + dct['title'] = extract.title + dct['text'] = extract.text + return dct + + +def export_container(container): + """ + Export a container to a dictionary + :param container: the container + :return: dictionary containing the information + """ + dct = OrderedDict() + dct['pk'] = container.pk + dct['title'] = container.title + dct['obj_type'] = "container" + dct['introduction'] = container.introduction + dct['conclusion'] = container.conclusion + dct['children'] = [] + + if container.has_sub_containers(): + for child in container.children: + dct['children'].append(export_container(child)) + elif container.has_extracts(): + for child in container.children: + dct['children'].append(export_extract(child)) + + return dct + + +def export_content(content): + """ + Export a content to dictionary in order to store them in a JSON file + :param content: content to be exported + :return: dictionary containing the information + """ + dct = export_container(content) + + # append metadata : + dct['description'] = content.description + dct['type'] = content.type + if content.licence: + dct['licence'] = content.licence.code + + return dct From c2bb380e725a64bf27adaa8d2995ae6aa7feef27 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 15:33:00 +0100 Subject: [PATCH 102/887] travail sur les factory zep12 --- zds/tutorialv2/factories.py | 101 ++++++++++++++---------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index dd9147ee72..38e625545b 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -7,7 +7,7 @@ import factory -from zds.tutorial.models import Tutorial, Part, Chapter, Extract, Note,\ +from models import PublishableContent, Container, Extract, ContentReaction,\ Validation from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory @@ -32,12 +32,12 @@ class BigTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) description = factory.Sequence( lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'BIG' + type = 'TUTORIAL' create_at = datetime.now() introduction = 'introduction.md' conclusion = 'conclusion.md' @@ -79,12 +79,12 @@ def _prepare(cls, create, **kwargs): class MiniTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) description = factory.Sequence( lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'MINI' + type = 'TUTORIAL' create_at = datetime.now() introduction = 'introduction.md' conclusion = 'conclusion.md' @@ -130,7 +130,7 @@ def _prepare(cls, create, **kwargs): class PartFactory(factory.DjangoModelFactory): - FACTORY_FOR = Part + FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) @@ -138,7 +138,7 @@ class PartFactory(factory.DjangoModelFactory): def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) part = super(PartFactory, cls)._prepare(create, **kwargs) - tutorial = kwargs.pop('tutorial', None) + parent = kwargs.pop('tutorial', None) real_content = content if light: @@ -154,20 +154,20 @@ def _prepare(cls, create, **kwargs): part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') part.save() - f = open(os.path.join(tutorial.get_path(), part.introduction), "w") + f = open(os.path.join(parent.get_path(), part.introduction), "w") f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.introduction]) - f = open(os.path.join(tutorial.get_path(), part.conclusion), "w") + f = open(os.path.join(parent.get_path(), part.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() repo.index.add([part.conclusion]) - if tutorial: - tutorial.save() + if parent: + parent.save() - man = export_tutorial(tutorial) - f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") + man = export_tutorial(parent) + f = open(os.path.join(parent.get_path(), 'manifest.json'), "w") f.write( json_writer.dumps( man, @@ -179,15 +179,15 @@ def _prepare(cls, create, **kwargs): cm = repo.index.commit("Init Part") - if tutorial: - tutorial.sha_draft = cm.hexsha - tutorial.save() + if parent: + parent.sha_draft = cm.hexsha + parent.save() return part class ChapterFactory(factory.DjangoModelFactory): - FACTORY_FOR = Chapter + FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) @@ -196,8 +196,7 @@ def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) - tutorial = kwargs.pop('tutorial', None) - part = kwargs.pop('part', None) + parent = kwargs.pop('part', None) real_content = content if light: @@ -207,53 +206,37 @@ def _prepare(cls, create, **kwargs): if not os.path.isdir(path): os.makedirs(path, mode=0o777) - if tutorial: - chapter.introduction = '' - chapter.conclusion = '' - tutorial.save() - repo = Repo(tutorial.get_path()) - - man = export_tutorial(tutorial) - f = open(os.path.join(tutorial.get_path(), 'manifest.json'), "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - repo.index.add(['manifest.json']) - - elif part: + if parent: chapter.introduction = os.path.join( - part.get_phy_slug(), + parent.get_phy_slug(), chapter.get_phy_slug(), 'introduction.md') chapter.conclusion = os.path.join( - part.get_phy_slug(), + parent.get_phy_slug(), chapter.get_phy_slug(), 'conclusion.md') chapter.save() f = open( os.path.join( - part.tutorial.get_path(), + parent.tutorial.get_path(), chapter.introduction), "w") f.write(real_content.encode('utf-8')) f.close() f = open( os.path.join( - part.tutorial.get_path(), + parent.tutorial.get_path(), chapter.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() - part.tutorial.save() - repo = Repo(part.tutorial.get_path()) + parent.tutorial.save() + repo = Repo(parent.tutorial.get_path()) - man = export_tutorial(part.tutorial) + man = export_tutorial(parent.tutorial) f = open( os.path.join( - part.tutorial.get_path(), + parent.parent.get_path(), 'manifest.json'), "w") f.write( @@ -268,15 +251,11 @@ def _prepare(cls, create, **kwargs): cm = repo.index.commit("Init Chapter") - if tutorial: - tutorial.sha_draft = cm.hexsha - tutorial.save() - chapter.tutorial = tutorial - elif part: - part.tutorial.sha_draft = cm.hexsha - part.tutorial.save() - part.save() - chapter.part = part + if parent: + parent.parent.sha_draft = cm.hexsha + parent.parent.save() + parent.save() + chapter.parent = parent return chapter @@ -289,14 +268,14 @@ class ExtractFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - chapter = kwargs.pop('chapter', None) - if chapter: - if chapter.tutorial: - chapter.tutorial.sha_draft = 'EXTRACT-AAAA' - chapter.tutorial.save() - elif chapter.part: - chapter.part.tutorial.sha_draft = 'EXTRACT-AAAA' - chapter.part.tutorial.save() + container = kwargs.pop('container', None) + if container: + if container.parent is PublishableContent: + container.parent.sha_draft = 'EXTRACT-AAAA' + container.parent.save() + elif container.parent.parent is PublishableContent: + container.parent.parent.sha_draft = 'EXTRACT-AAAA' + container.parent.parent.tutorial.save() return extract From bef60f834e8f87ce11369d8729edfabeb36773a5 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 27 Dec 2014 16:59:29 +0100 Subject: [PATCH 103/887] =?UTF-8?q?pep8=20+=20diff=20migr=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/factories.py | 4 +- zds/tutorialv2/feeds.py | 4 +- zds/tutorialv2/models.py | 2 +- zds/tutorialv2/views.py | 184 ++++++++++++++++-------------------- 4 files changed, 86 insertions(+), 108 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 38e625545b..91fe32412d 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -281,7 +281,7 @@ def _prepare(cls, create, **kwargs): class NoteFactory(factory.DjangoModelFactory): - FACTORY_FOR = Note + FACTORY_FOR = ContentReaction ip_address = '192.168.3.1' text = 'Bonjour, je me présente, je m\'appelle l\'homme au texte bidonné' @@ -323,7 +323,7 @@ def _prepare(cls, create, **kwargs): class PublishedMiniTutorial(MiniTutorialFactory): - FACTORY_FOR = Tutorial + FACTORY_FOR = PublishableContent @classmethod def _prepare(cls, create, **kwargs): diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index 6f92671c3e..f1dcbfff93 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -20,8 +20,8 @@ class LastContentFeedRSS(Feed): def items(self): """ - :return: The last (typically 5) contents (sorted by publication date). If `self.type` is not `None`, the - contents will only be of this type. + :return: The last (typically 5) contents (sorted by publication date). + If `self.type` is not `None`, the contents will only be of this type. """ contents = PublishableContent.objects.filter(sha_public__isnull=False) if self.content_type is not None: diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cb3643d8c6..16d9ea5d53 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -497,7 +497,7 @@ class Meta: blank=True, null=True, db_index=True) # as of ZEP 12 this field is no longer the size but the type of content (article/tutorial) type = models.CharField(max_length=10, choices=TYPE_CHOICES, db_index=True) - #zep03 field + # zep03 field helps = models.ManyToManyField(HelpWriting, verbose_name='Aides', db_index=True) relative_images_path = models.CharField( diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index ad7a7b4199..c2b875b4c3 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -123,7 +123,10 @@ class TutorialWithHelp(TutorialList): def get_queryset(self): """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects.exclude(Q(helps_count=0) and Q(sha_beta='')) + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() try: type_filter = self.request.GET.get('type') query_set = query_set.filter(helps_title__in=[type_filter]) @@ -131,6 +134,7 @@ def get_queryset(self): # if no filter, no need to change pass return query_set + def get_context_data(self, **kwargs): """Add all HelpWriting objects registered to the context so that the template can use it""" context = super(TutorialWithHelp, self).get_context_data(**kwargs) @@ -265,6 +269,40 @@ def get_context_data(self, **kwargs): return context +class DisplayDiff(DetailView): + """Display the difference between two version of a content. + Reference is always HEAD and compared version is a GET query parameter named sha + this class has no reason to be adapted to any content type""" + model = PublishableContent + template_name = "tutorial/diff.html" + context_object_name = "tutorial" + + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + + def get_context_data(self, **kwargs): + + context = super(DisplayDiff, self).get_context_data(**kwargs) + + try: + sha = self.request.GET.get("sha") + except KeyError: + sha = self.get_object().sha_draft + + if self.request.user not in context[self.context_object_name].authors.all(): + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + # open git repo and find diff between displayed version and head + repo = Repo(context[self.context_object_name].get_path()) + current_version_commit = repo.commit(sha) + diff_with_head = current_version_commit.diff("HEAD~1") + context["path_add"] = diff_with_head.iter_change_type("A") + context["path_ren"] = diff_with_head.iter_change_type("R") + context["path_del"] = diff_with_head.iter_change_type("D") + context["path_maj"] = diff_with_head.iter_change_type("M") + return context + + class DisplayArticle(DisplayContent): type = "ARTICLE" @@ -301,6 +339,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") + # load extracts cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt @@ -474,33 +513,11 @@ def reservation(request, validation_pk): ) -@login_required -def diff(request, tutorial_pk, tutorial_slug): - try: - sha = request.GET["sha"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - repo = Repo(tutorial.get_path()) - hcommit = repo.commit(sha) - tdiff = hcommit.diff("HEAD~1") - return render(request, "tutorial/tutorial/diff.html", { - "tutorial": tutorial, - "path_add": tdiff.iter_change_type("A"), - "path_ren": tdiff.iter_change_type("R"), - "path_del": tdiff.iter_change_type("D"), - "path_maj": tdiff.iter_change_type("M"), - }) - - @login_required def history(request, tutorial_pk, tutorial_slug): """History of the tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied @@ -517,7 +534,7 @@ def history(request, tutorial_pk, tutorial_slug): def history_validation(request, tutorial_pk): """History of the validation of a tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Get subcategory to filter validations. @@ -552,7 +569,7 @@ def reject_tutorial(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, version=tutorial.sha_validation).latest("date_proposition") @@ -617,7 +634,7 @@ def valid_tutorial(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, version=tutorial.sha_validation).latest("date_proposition") @@ -685,7 +702,7 @@ def invalid_tutorial(request, tutorial_pk): # Retrieve current tutorial - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) un_mep(tutorial) validation = Validation.objects.filter( tutorial__pk=tutorial_pk, @@ -720,7 +737,7 @@ def ask_validation(request): tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -781,7 +798,7 @@ def delete_tutorial(request, tutorial_pk): # Retrieve current tutorial - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -835,7 +852,7 @@ def delete_tutorial(request, tutorial_pk): @require_POST def modify_tutorial(request): tutorial_pk = request.POST["tutorial"] - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # User actions if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): @@ -1072,7 +1089,7 @@ def add_tutorial(request): # Creating a tutorial - tutorial = Tutorial() + tutorial = PublishableContent() tutorial.title = data["title"] tutorial.description = data["description"] tutorial.type = data["type"] @@ -1139,7 +1156,7 @@ def add_tutorial(request): # If it's a small tutorial, create its corresponding chapter if tutorial.type == "MINI": - chapter = Chapter() + chapter = Container() chapter.tutorial = tutorial chapter.save() tutorial.save() @@ -1173,7 +1190,7 @@ def edit_tutorial(request): tutorial_pk = request.GET["tutoriel"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # If the user isn't an author of the tutorial or isn't in the staff, he # hasn't permission to execute this method: @@ -1297,7 +1314,7 @@ def view_part( ): """Display a part.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) try: sha = request.GET["version"] except KeyError: @@ -1376,7 +1393,7 @@ def view_part_online( ): """Display a part.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if not tutorial.in_public(): raise Http404 @@ -1442,7 +1459,7 @@ def add_part(request): tutorial_pk = request.GET["tutoriel"] except KeyError: raise Http404 - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Make sure it's a big tutorial, just in case @@ -1457,7 +1474,7 @@ def add_part(request): form = PartForm(request.POST) if form.is_valid(): data = form.data - part = Part() + part = Container() part.tutorial = tutorial part.title = data["title"] part.position_in_tutorial = tutorial.get_parts().count() + 1 @@ -1500,7 +1517,7 @@ def modify_part(request): if not request.method == "POST": raise Http404 part_pk = request.POST["part"] - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) # Make sure the user is allowed to do that @@ -1527,7 +1544,7 @@ def modify_part(request): elif "delete" in request.POST: # Delete all chapters belonging to the part - Chapter.objects.all().filter(part=part).delete() + Container.objects.all().filter(part=part).delete() # Move other parts @@ -1566,7 +1583,7 @@ def edit_part(request): except ValueError: raise Http404 - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) introduction = os.path.join(part.get_path(), "introduction.md") conclusion = os.path.join(part.get_path(), "conclusion.md") # Make sure the user is allowed to do that @@ -1628,7 +1645,7 @@ def edit_part(request): }) -# Chapters. +# Containers. @login_required @@ -1643,7 +1660,7 @@ def view_chapter( ): """View chapter.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) try: sha = request.GET["version"] @@ -1747,7 +1764,7 @@ def view_chapter_online( ): """View chapter.""" - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) if not tutorial.in_public(): raise Http404 @@ -1849,7 +1866,7 @@ def add_chapter(request): part_pk = request.GET["partie"] except KeyError: raise Http404 - part = get_object_or_404(Part, pk=part_pk) + part = get_object_or_404(Container, pk=part_pk) # Make sure the user is allowed to do that @@ -1859,7 +1876,7 @@ def add_chapter(request): form = ChapterForm(request.POST, request.FILES) if form.is_valid(): data = form.data - chapter = Chapter() + chapter = Container() chapter.title = data["title"] chapter.part = part chapter.position_in_part = part.get_chapters().count() + 1 @@ -1934,7 +1951,7 @@ def modify_chapter(request): chapter_pk = request.POST["chapter"] except KeyError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) # Make sure the user is allowed to do that @@ -1988,7 +2005,7 @@ def modify_chapter(request): # Update all the position_in_tutorial fields for the next chapters for tut_c in \ - Chapter.objects.filter(position_in_tutorial__gt=old_tut_pos): + Container.objects.filter(position_in_tutorial__gt=old_tut_pos): tut_c.update_position_in_tutorial() tut_c.save() @@ -2017,7 +2034,7 @@ def edit_chapter(request): except ValueError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) big = chapter.part small = chapter.tutorial @@ -2106,7 +2123,7 @@ def add_extract(request): except ValueError: raise Http404 - chapter = get_object_or_404(Chapter, pk=chapter_pk) + chapter = get_object_or_404(Container, pk=chapter_pk) part = chapter.part # If part exist, we check if the user is in authors of the tutorial of the @@ -2321,7 +2338,7 @@ def find_tuto(request, pk_user): type = None display_user = get_object_or_404(User, pk=pk_user) if type == "beta": - tutorials = Tutorial.objects.all().filter( + tutorials = PublishableContent.objects.all().filter( authors__in=[display_user], sha_beta__isnull=False).exclude(sha_beta="").order_by("-pubdate") @@ -2334,7 +2351,7 @@ def find_tuto(request, pk_user): return render(request, "tutorial/member/beta.html", {"tutorials": tuto_versions, "usr": display_user}) else: - tutorials = Tutorial.objects.all().filter( + tutorials = PublishableContent.objects.all().filter( authors__in=[display_user], sha_public__isnull=False).exclude(sha_public="").order_by("-pubdate") @@ -2393,7 +2410,7 @@ def import_content( images, logo, ): - tutorial = Tutorial() + tutorial = PublishableContent() # add create date @@ -2451,7 +2468,7 @@ def import_content( + str(part_count) + "]/introduction")[0] part_conclu = tree.xpath("/bigtuto/parties/partie[" + str(part_count) + "]/conclusion")[0] - part = Part() + part = Container() part.title = part_title.text.strip() part.position_in_tutorial = part_count part.tutorial = tutorial @@ -2493,7 +2510,7 @@ def import_content( "]/chapitres/chapitre[" + str(chapter_count) + "]/conclusion")[0] - chapter = Chapter() + chapter = Container() chapter.title = chapter_title.text.strip() chapter.position_in_part = chapter_count chapter.position_in_tutorial = part_count * chapter_count @@ -2601,7 +2618,7 @@ def import_content( action="add", ) tutorial.authors.add(request.user) - chapter = Chapter() + chapter = Container() chapter.tutorial = tutorial chapter.save() extract_count = 1 @@ -2952,7 +2969,7 @@ def insert_into_zip(zip_file, git_tree): def download(request): """Download a tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) repo_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) repo = Repo(repo_path) @@ -2976,7 +2993,7 @@ def download(request): def download_markdown(request): """Download a markdown tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -2992,7 +3009,7 @@ def download_markdown(request): def download_html(request): """Download a pdf tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3010,7 +3027,7 @@ def download_html(request): def download_pdf(request): """Download a pdf tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3028,7 +3045,7 @@ def download_pdf(request): def download_epub(request): """Download an epub tutorial.""" - tutorial = get_object_or_404(Tutorial, pk=request.GET["tutoriel"]) + tutorial = get_object_or_404(PublishableContent, pk=request.GET["tutoriel"]) phy_path = os.path.join( tutorial.get_prod_path(), tutorial.slug + @@ -3281,7 +3298,7 @@ def answer(request): # Retrieve current tutorial. - tutorial = get_object_or_404(Tutorial, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) # Making sure reactioning is allowed @@ -3438,7 +3455,7 @@ def activ_js(request): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - tutorial = get_object_or_404(Tutorial, pk=request.POST["tutorial"]) + tutorial = get_object_or_404(PublishableContent, pk=request.POST["tutorial"]) tutorial.js_support = "js_support" in request.POST tutorial.save() @@ -3457,7 +3474,7 @@ def edit_note(request): note = get_object_or_404(ContentReaction, pk=note_pk) g_tutorial = None if note.position >= 1: - g_tutorial = get_object_or_404(Tutorial, pk=note.related_content.pk) + g_tutorial = get_object_or_404(PublishableContent, pk=note.related_content.pk) # Making sure the user is allowed to do that. Author of the note must to be # the user logged. @@ -3613,42 +3630,3 @@ def dislike_note(request): return HttpResponse(json_writer.dumps(resp)) else: return redirect(note.get_absolute_url()) - - -def help_tutorial(request): - """fetch all tutorials that needs help""" - - # Retrieve type of the help. Default value is any help - type = request.GET.get('type', None) - - if type is not None: - aide = get_object_or_404(HelpWriting, slug=type) - tutos = Tutorial.objects.filter(helps=aide) \ - .all() - else: - tutos = Tutorial.objects.annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - - # Paginator - paginator = Paginator(tutos, settings.ZDS_APP['forum']['topics_per_page']) - page = request.GET.get('page') - - try: - shown_tutos = paginator.page(page) - page = int(page) - except PageNotAnInteger: - shown_tutos = paginator.page(1) - page = 1 - except EmptyPage: - shown_tutos = paginator.page(paginator.num_pages) - page = paginator.num_pages - - aides = HelpWriting.objects.all() - - return render(request, "tutorial/tutorial/help.html", { - "tutorials": shown_tutos, - "helps": aides, - "pages": paginator_range(page, paginator.num_pages), - "nb": page - }) From be556652c489686077bd52ebc12437643b672f2c Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sat, 27 Dec 2014 17:28:33 +0100 Subject: [PATCH 104/887] Separation DB et JSON : partie 2 --- zds/tutorialv2/factories.py | 314 +++++------------- zds/tutorialv2/migrations/0001_initial.py | 89 ++--- ...t__del_field_validation_tutorial__add_f.py | 262 --------------- zds/tutorialv2/models.py | 144 +++++--- zds/tutorialv2/tests/tests_models.py | 46 ++- zds/tutorialv2/views.py | 2 +- zds/utils/tutorialv2.py | 4 +- 7 files changed, 254 insertions(+), 607 deletions(-) delete mode 100644 zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 91fe32412d..e0e8a78330 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -2,285 +2,142 @@ from datetime import datetime from git.repo import Repo -import json as json_writer import os import factory from models import PublishableContent, Container, Extract, ContentReaction,\ - Validation from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory -from zds.utils.tutorials import export_tutorial -from zds.tutorial.views import mep - -content = ( - u'Ceci est un contenu de tutoriel utile et à tester un peu partout\n\n ' - u'Ce contenu ira aussi bien dans les introductions, que dans les conclusions et les extraits \n\n ' - u'le gros intéret étant qu\'il renferme des images pour tester l\'execution coté pandoc \n\n ' - u'Exemple d\'image ![Ma pepite souris](http://blog.science-infuse.fr/public/souris.jpg)\n\n ' - u'\nExemple d\'image ![Image inexistante](http://blog.science-infuse.fr/public/inv_souris.jpg)\n\n ' - u'\nExemple de gif ![](http://corigif.free.fr/oiseau/img/oiseau_004.gif)\n\n ' - u'\nExemple de gif inexistant ![](http://corigif.free.fr/oiseau/img/ironman.gif)\n\n ' - u'Une image de type wikipedia qui fait tomber des tests ![](https://s.qwant.com/thumbr/?u=http%3A%2' - u'F%2Fwww.blogoergosum.com%2Fwp-content%2Fuploads%2F2010%2F02%2Fwikipedia-logo.jpg&h=338&w=600)\n\n ' - u'Image dont le serveur n\'existe pas ![](http://unknown.image.zds)\n\n ' - u'\n Attention les tests ne doivent pas crasher \n\n \n\n \n\n ' - u'qu\'un sujet abandonné !\n\n ') - -content_light = u'Un contenu light pour quand ce n\'est pas vraiment ça qui est testé' - - -class BigTutorialFactory(factory.DjangoModelFactory): - FACTORY_FOR = PublishableContent - title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) - description = factory.Sequence( - lambda n: 'Description du Tutoriel No{0}'.format(n)) - type = 'TUTORIAL' - create_at = datetime.now() - introduction = 'introduction.md' - conclusion = 'conclusion.md' +text_content = u'Ceci est un texte bidon' - @classmethod - def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) - path = tuto.get_path() - real_content = content - if light: - real_content = content_light - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - man = export_tutorial(tuto) - repo = Repo.init(path, bare=False) - repo = Repo(path) - - f = open(os.path.join(path, 'manifest.json'), "w") - f.write(json_writer.dumps(man, indent=4, ensure_ascii=False).encode('utf-8')) - f.close() - f = open(os.path.join(path, tuto.introduction), "w") - f.write(real_content.encode('utf-8')) - f.close() - f = open(os.path.join(path, tuto.conclusion), "w") - f.write(real_content.encode('utf-8')) - f.close() - repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) - cm = repo.index.commit("Init Tuto") - - tuto.sha_draft = cm.hexsha - tuto.sha_beta = None - tuto.gallery = GalleryFactory() - for author in tuto.authors.all(): - UserGalleryFactory(user=author, gallery=tuto.gallery) - return tuto - - -class MiniTutorialFactory(factory.DjangoModelFactory): +class PublishableContentFactory(factory.DjangoModelFactory): FACTORY_FOR = PublishableContent title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) - description = factory.Sequence( - lambda n: 'Description du Tutoriel No{0}'.format(n)) + description = factory.Sequence(lambda n: 'Description du Tutoriel No{0}'.format(n)) + type = 'TUTORIAL' type = 'TUTORIAL' - create_at = datetime.now() - introduction = 'introduction.md' - conclusion = 'conclusion.md' @classmethod def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - tuto = super(MiniTutorialFactory, cls)._prepare(create, **kwargs) - real_content = content - - if light: - real_content = content_light - path = tuto.get_path() + publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) + path = publishable_content.get_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) - man = export_tutorial(tuto) - repo = Repo.init(path, bare=False) + FACTORY_FOR = PublishableContent + type = 'TUTORIAL' + introduction = 'introduction.md' + conclusion = 'conclusion.md' + versioned_content = VersionedContent(None, + publishable_content.pk, + publishable_content.type, + publishable_content.title) + versioned_content.introduction = introduction + versioned_content.conclusion = conclusion + + Repo.init(path, bare=False) repo = Repo(path) - file = open(os.path.join(path, 'manifest.json'), "w") - file.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - file.close() - file = open(os.path.join(path, tuto.introduction), "w") - file.write(real_content.encode('utf-8')) - file.close() - file = open(os.path.join(path, tuto.conclusion), "w") - file.write(real_content.encode('utf-8')) - file.close() - - repo.index.add(['manifest.json', tuto.introduction, tuto.conclusion]) + versioned_content.dump_json() + f = open(os.path.join(path, introduction), "w") + f.write(text_content.encode('utf-8')) + f.close() + f = open(os.path.join(path, conclusion), "w") + f.write(text_content.encode('utf-8')) + f.close() + repo.index.add(['manifest.json', introduction, conclusion]) cm = repo.index.commit("Init Tuto") - tuto.sha_draft = cm.hexsha - tuto.gallery = GalleryFactory() - for author in tuto.authors.all(): - UserGalleryFactory(user=author, gallery=tuto.gallery) - return tuto + publishable_content.sha_draft = cm.hexsha + publishable_content.sha_beta = None + publishable_content.gallery = GalleryFactory() + for author in publishable_content.authors.all(): + UserGalleryFactory(user=author, gallery=publishable_content.gallery) + return publishable_content -class PartFactory(factory.DjangoModelFactory): +class ContainerFactory(factory.Factory): FACTORY_FOR = Container - title = factory.Sequence(lambda n: 'Ma partie No{0}'.format(n)) + title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n+1)) + pk = factory.Sequence(lambda n: n+1) @classmethod def _prepare(cls, create, **kwargs): - light = kwargs.pop('light', False) - part = super(PartFactory, cls)._prepare(create, **kwargs) - parent = kwargs.pop('tutorial', None) - - real_content = content - if light: - real_content = content_light + db_object = kwargs.pop('db_object', None) + container = super(ContainerFactory, cls)._prepare(create, **kwargs) + container.parent.add_container(container) - path = part.get_path() - repo = Repo(part.tutorial.get_path()) + path = container.get_path() + repo = Repo(container.top_container().get_path()) if not os.path.isdir(path): os.makedirs(path, mode=0o777) - part.introduction = os.path.join(part.get_phy_slug(), 'introduction.md') - part.conclusion = os.path.join(part.get_phy_slug(), 'conclusion.md') - part.save() + container.introduction = os.path.join(container.get_path(relative=True), 'introduction.md') + container.conclusion = os.path.join(container.get_path(relative=True), 'conclusion.md') f = open(os.path.join(parent.get_path(), part.introduction), "w") - f.write(real_content.encode('utf-8')) + f.write(text_content.encode('utf-8')) f.close() - repo.index.add([part.introduction]) + repo.index.add([container.introduction]) f = open(os.path.join(parent.get_path(), part.conclusion), "w") - f.write(real_content.encode('utf-8')) + f.write(text_content.encode('utf-8')) f.close() - repo.index.add([part.conclusion]) + repo.index.add([container.conclusion]) if parent: parent.save() + repo.index.add(['manifest.json']) - man = export_tutorial(parent) - f = open(os.path.join(parent.get_path(), 'manifest.json'), "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - - repo.index.add(['manifest.json']) + cm = repo.index.commit("Add container") - cm = repo.index.commit("Init Part") + if db_object: + db_object.sha_draft = cm.hexsha + db_object.save() - if parent: - parent.sha_draft = cm.hexsha - parent.save() + return container - return part +class ExtractFactory(factory.Factory): + FACTORY_FOR = Extract -class ChapterFactory(factory.DjangoModelFactory): - FACTORY_FOR = Container - - title = factory.Sequence(lambda n: 'Mon Chapitre No{0}'.format(n)) + title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n+1)) + pk = factory.Sequence(lambda n: n+1) @classmethod def _prepare(cls, create, **kwargs): + db_object = kwargs.pop('db_object', None) + extract = super(ExtractFactory, cls)._prepare(create, **kwargs) + extract.container.add_extract(extract) - light = kwargs.pop('light', False) - chapter = super(ChapterFactory, cls)._prepare(create, **kwargs) - parent = kwargs.pop('part', None) - - real_content = content - if light: - real_content = content_light - path = chapter.get_path() - - if not os.path.isdir(path): - os.makedirs(path, mode=0o777) - - if parent: - chapter.introduction = os.path.join( - parent.get_phy_slug(), - chapter.get_phy_slug(), - 'introduction.md') - chapter.conclusion = os.path.join( - parent.get_phy_slug(), - chapter.get_phy_slug(), - 'conclusion.md') - chapter.save() - f = open( - os.path.join( - parent.tutorial.get_path(), - chapter.introduction), - "w") - f.write(real_content.encode('utf-8')) - f.close() - f = open( - os.path.join( - parent.tutorial.get_path(), - chapter.conclusion), - "w") - f.write(real_content.encode('utf-8')) - f.close() - parent.tutorial.save() - repo = Repo(parent.tutorial.get_path()) - - man = export_tutorial(parent.tutorial) - f = open( - os.path.join( - parent.parent.get_path(), - 'manifest.json'), - "w") - f.write( - json_writer.dumps( - man, - indent=4, - ensure_ascii=False).encode('utf-8')) - f.close() - - repo.index.add([chapter.introduction, chapter.conclusion]) - repo.index.add(['manifest.json']) - - cm = repo.index.commit("Init Chapter") + extract.text = extract.get_path(relative=True) - if parent: - parent.parent.sha_draft = cm.hexsha - parent.parent.save() - parent.save() - chapter.parent = parent - - return chapter + top_container = extract.container.top_container() + repo = Repo(top_container.get_path()) + f = open(extract.get_path(), 'w') + f.write(text_content.encode('utf-8')) + f.close() + repo.index.add([extract.text]) -class ExtractFactory(factory.DjangoModelFactory): - FACTORY_FOR = Extract + top_container.dump_json() + repo.index.add(['manifest.json']) - title = factory.Sequence(lambda n: 'Mon Extrait No{0}'.format(n)) + cm = repo.index.commit("Add extract") - @classmethod - def _prepare(cls, create, **kwargs): - extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - container = kwargs.pop('container', None) - if container: - if container.parent is PublishableContent: - container.parent.sha_draft = 'EXTRACT-AAAA' - container.parent.save() - elif container.parent.parent is PublishableContent: - container.parent.parent.sha_draft = 'EXTRACT-AAAA' - container.parent.parent.tutorial.save() + if db_object: + db_object.sha_draft = cm.hexsha + db_object.save() return extract -class NoteFactory(factory.DjangoModelFactory): +class ContentReactionFactory(factory.DjangoModelFactory): FACTORY_FOR = ContentReaction ip_address = '192.168.3.1' @@ -288,13 +145,13 @@ class NoteFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): - note = super(NoteFactory, cls)._prepare(create, **kwargs) + note = super(ContentReactionFactory, cls)._prepare(create, **kwargs) note.pubdate = datetime.now() note.save() - tutorial = kwargs.pop('tutorial', None) - if tutorial: - tutorial.last_note = note - tutorial.save() + content = kwargs.pop('tutorial', None) + if content: + content.last_note = note + content.save() return note @@ -321,17 +178,4 @@ def _prepare(cls, create, **kwargs): licence = super(LicenceFactory, cls)._prepare(create, **kwargs) return licence - -class PublishedMiniTutorial(MiniTutorialFactory): - FACTORY_FOR = PublishableContent - - @classmethod - def _prepare(cls, create, **kwargs): - tutorial = super(PublishedMiniTutorial, cls)._prepare(create, **kwargs) - tutorial.pubdate = datetime.now() - tutorial.sha_public = tutorial.sha_draft - tutorial.source = '' - tutorial.sha_validation = None - mep(tutorial, tutorial.sha_draft) - tutorial.save() - return tutorial + FACTORY_FOR = PublishableContent \ No newline at end of file diff --git a/zds/tutorialv2/migrations/0001_initial.py b/zds/tutorialv2/migrations/0001_initial.py index 5dca0239ca..8c4b758aec 100644 --- a/zds/tutorialv2/migrations/0001_initial.py +++ b/zds/tutorialv2/migrations/0001_initial.py @@ -8,22 +8,10 @@ class Migration(SchemaMigration): def forwards(self, orm): - # Adding model 'Container' - db.create_table(u'tutorialv2_container', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), - ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), - ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), - )) - db.send_create_signal(u'tutorialv2', ['Container']) - # Adding model 'PublishableContent' db.create_table(u'tutorialv2_publishablecontent', ( - (u'container_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), ('description', self.gf('django.db.models.fields.CharField')(max_length=200)), ('source', self.gf('django.db.models.fields.CharField')(max_length=200)), ('image', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gallery.Image'], null=True, on_delete=models.SET_NULL, blank=True)), @@ -37,7 +25,7 @@ def forwards(self, orm): ('sha_draft', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), ('licence', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['utils.Licence'], null=True, blank=True)), ('type', self.gf('django.db.models.fields.CharField')(max_length=10, db_index=True)), - ('images', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('relative_images_path', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), ('last_note', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='last_note', null=True, to=orm['tutorialv2.ContentReaction'])), ('is_locked', self.gf('django.db.models.fields.BooleanField')(default=False)), ('js_support', self.gf('django.db.models.fields.BooleanField')(default=False)), @@ -62,6 +50,15 @@ def forwards(self, orm): )) db.create_unique(m2m_table_name, ['publishablecontent_id', 'subcategory_id']) + # Adding M2M table for field helps on 'PublishableContent' + m2m_table_name = db.shorten_name(u'tutorialv2_publishablecontent_helps') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('publishablecontent', models.ForeignKey(orm[u'tutorialv2.publishablecontent'], null=False)), + ('helpwriting', models.ForeignKey(orm[u'utils.helpwriting'], null=False)) + )) + db.create_unique(m2m_table_name, ['publishablecontent_id', 'helpwriting_id']) + # Adding model 'ContentReaction' db.create_table(u'tutorialv2_contentreaction', ( (u'comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['utils.Comment'], unique=True, primary_key=True)), @@ -72,26 +69,16 @@ def forwards(self, orm): # Adding model 'ContentRead' db.create_table(u'tutorialv2_contentread', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), + ('content', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'])), ('note', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.ContentReaction'])), ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_notes_read', to=orm['auth.User'])), )) db.send_create_signal(u'tutorialv2', ['ContentRead']) - # Adding model 'Extract' - db.create_table(u'tutorialv2_extract', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), - ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), - ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - )) - db.send_create_signal(u'tutorialv2', ['Extract']) - # Adding model 'Validation' db.create_table(u'tutorialv2_validation', ( (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('tutorial', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), + ('content', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True)), ('version', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=80, null=True, blank=True)), ('date_proposition', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), ('comment_authors', self.gf('django.db.models.fields.TextField')()), @@ -105,9 +92,6 @@ def forwards(self, orm): def backwards(self, orm): - # Deleting model 'Container' - db.delete_table(u'tutorialv2_container') - # Deleting model 'PublishableContent' db.delete_table(u'tutorialv2_publishablecontent') @@ -117,15 +101,15 @@ def backwards(self, orm): # Removing M2M table for field subcategory on 'PublishableContent' db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_subcategory')) + # Removing M2M table for field helps on 'PublishableContent' + db.delete_table(db.shorten_name(u'tutorialv2_publishablecontent_helps')) + # Deleting model 'ContentReaction' db.delete_table(u'tutorialv2_contentreaction') # Deleting model 'ContentRead' db.delete_table(u'tutorialv2_contentread') - # Deleting model 'Extract' - db.delete_table(u'tutorialv2_extract') - # Deleting model 'Validation' db.delete_table(u'tutorialv2_validation') @@ -187,17 +171,6 @@ def backwards(self, orm): 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, - u'tutorialv2.container': { - 'Meta': {'object_name': 'Container'}, - 'compatibility_pk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'conclusion': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'introduction': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'position_in_parent': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, u'tutorialv2.contentreaction': { 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), @@ -205,39 +178,33 @@ def backwards(self, orm): }, u'tutorialv2.contentread': { 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), - 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) }, - u'tutorialv2.extract': { - 'Meta': {'object_name': 'Extract'}, - 'container': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.Container']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'position_in_container': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), - 'text': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, u'tutorialv2.publishablecontent': { - 'Meta': {'object_name': 'PublishableContent', '_ormbases': [u'tutorialv2.Container']}, + 'Meta': {'object_name': 'PublishableContent'}, 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), - u'container_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['tutorialv2.Container']", 'unique': 'True', 'primary_key': 'True'}), 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'helps': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['utils.HelpWriting']", 'db_index': 'True', 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'images': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, @@ -245,12 +212,12 @@ def backwards(self, orm): 'Meta': {'object_name': 'Validation'}, 'comment_authors': ('django.db.models.fields.TextField', [], {}), 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), - 'tutorial': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) }, @@ -270,6 +237,14 @@ def backwards(self, orm): 'text_html': ('django.db.models.fields.TextField', [], {}), 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) }, + u'utils.helpwriting': { + 'Meta': {'object_name': 'HelpWriting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '20'}), + 'tablelabel': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, u'utils.licence': { 'Meta': {'object_name': 'Licence'}, 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), diff --git a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py b/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py deleted file mode 100644 index a979a2e9df..0000000000 --- a/zds/tutorialv2/migrations/0002_auto__del_container__del_extract__del_field_validation_tutorial__add_f.py +++ /dev/null @@ -1,262 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Deleting model 'Container' - db.delete_table(u'tutorialv2_container') - - # Deleting model 'Extract' - db.delete_table(u'tutorialv2_extract') - - # Deleting field 'Validation.tutorial' - db.delete_column(u'tutorialv2_validation', 'tutorial_id') - - # Adding field 'Validation.content' - db.add_column(u'tutorialv2_validation', 'content', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), - keep_default=False) - - # Deleting field 'PublishableContent.container_ptr' - db.delete_column(u'tutorialv2_publishablecontent', u'container_ptr_id') - - # Deleting field 'PublishableContent.images' - db.delete_column(u'tutorialv2_publishablecontent', 'images') - - # Adding field 'PublishableContent.id' - db.add_column(u'tutorialv2_publishablecontent', u'id', - self.gf('django.db.models.fields.AutoField')(default=0, primary_key=True), - keep_default=False) - - # Adding field 'PublishableContent.title' - db.add_column(u'tutorialv2_publishablecontent', 'title', - self.gf('django.db.models.fields.CharField')(default='', max_length=80), - keep_default=False) - - # Adding field 'PublishableContent.relative_images_path' - db.add_column(u'tutorialv2_publishablecontent', 'relative_images_path', - self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), - keep_default=False) - - # Deleting field 'ContentRead.tutorial' - db.delete_column(u'tutorialv2_contentread', 'tutorial_id') - - # Adding field 'ContentRead.content' - db.add_column(u'tutorialv2_contentread', 'content', - self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['tutorialv2.PublishableContent']), - keep_default=False) - - - def backwards(self, orm): - # Adding model 'Container' - db.create_table(u'tutorialv2_container', ( - ('slug', self.gf('django.db.models.fields.SlugField')(max_length=80)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'], null=True, on_delete=models.SET_NULL, blank=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('introduction', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('compatibility_pk', self.gf('django.db.models.fields.IntegerField')(default=0)), - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('position_in_parent', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('conclusion', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - )) - db.send_create_signal(u'tutorialv2', ['Container']) - - # Adding model 'Extract' - db.create_table(u'tutorialv2_extract', ( - ('container', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.Container'])), - ('title', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('text', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), - ('position_in_container', self.gf('django.db.models.fields.IntegerField')(db_index=True)), - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - )) - db.send_create_signal(u'tutorialv2', ['Extract']) - - # Adding field 'Validation.tutorial' - db.add_column(u'tutorialv2_validation', 'tutorial', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent'], null=True, blank=True), - keep_default=False) - - # Deleting field 'Validation.content' - db.delete_column(u'tutorialv2_validation', 'content_id') - - - # User chose to not deal with backwards NULL issues for 'PublishableContent.container_ptr' - raise RuntimeError("Cannot reverse this migration. 'PublishableContent.container_ptr' and its values cannot be restored.") - - # The following code is provided here to aid in writing a correct migration # Adding field 'PublishableContent.container_ptr' - db.add_column(u'tutorialv2_publishablecontent', u'container_ptr', - self.gf('django.db.models.fields.related.OneToOneField')(to=orm['tutorialv2.Container'], unique=True, primary_key=True), - keep_default=False) - - # Adding field 'PublishableContent.images' - db.add_column(u'tutorialv2_publishablecontent', 'images', - self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True), - keep_default=False) - - # Deleting field 'PublishableContent.id' - db.delete_column(u'tutorialv2_publishablecontent', u'id') - - # Deleting field 'PublishableContent.title' - db.delete_column(u'tutorialv2_publishablecontent', 'title') - - # Deleting field 'PublishableContent.relative_images_path' - db.delete_column(u'tutorialv2_publishablecontent', 'relative_images_path') - - - # User chose to not deal with backwards NULL issues for 'ContentRead.tutorial' - raise RuntimeError("Cannot reverse this migration. 'ContentRead.tutorial' and its values cannot be restored.") - - # The following code is provided here to aid in writing a correct migration # Adding field 'ContentRead.tutorial' - db.add_column(u'tutorialv2_contentread', 'tutorial', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['tutorialv2.PublishableContent']), - keep_default=False) - - # Deleting field 'ContentRead.content' - db.delete_column(u'tutorialv2_contentread', 'content_id') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'gallery.gallery': { - 'Meta': {'object_name': 'Gallery'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'gallery.image': { - 'Meta': {'object_name': 'Image'}, - 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'tutorialv2.contentreaction': { - 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, - u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), - 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) - }, - u'tutorialv2.contentread': { - 'Meta': {'object_name': 'ContentRead'}, - 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) - }, - u'tutorialv2.publishablecontent': { - 'Meta': {'object_name': 'PublishableContent'}, - 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), - 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), - 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), - 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), - 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), - 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), - 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), - 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), - 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'tutorialv2.validation': { - 'Meta': {'object_name': 'Validation'}, - 'comment_authors': ('django.db.models.fields.TextField', [], {}), - 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), - 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), - 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), - 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), - 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) - }, - u'utils.comment': { - 'Meta': {'object_name': 'Comment'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), - 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), - 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), - 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'text': ('django.db.models.fields.TextField', [], {}), - 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), - 'text_html': ('django.db.models.fields.TextField', [], {}), - 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - u'utils.licence': { - 'Meta': {'object_name': 'Licence'}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'description': ('django.db.models.fields.TextField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - }, - u'utils.subcategory': { - 'Meta': {'object_name': 'SubCategory'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), - 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) - } - } - - complete_apps = ['tutorialv2'] \ No newline at end of file diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 16d9ea5d53..46ed6ac433 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -60,7 +60,6 @@ class Container: pk = 0 title = '' - slug = '' introduction = None conclusion = None parent = None @@ -72,7 +71,6 @@ class Container: def __init__(self, pk, title, parent=None, position_in_parent=1): self.pk = pk self.title = title - self.slug = slugify(title) self.parent = parent self.position_in_parent = position_in_parent self.children = [] # even if you want, do NOT remove this line @@ -159,7 +157,13 @@ def get_phy_slug(self): """ :return: the physical slug, used to represent data in filesystem """ - return str(self.pk) + '_' + self.slug + return str(self.pk) + '_' + slugify(self.title) + + def slug(self): + """ + :return: slug of the object, based on title + """ + return slugify(self.title) def update_children(self): """ @@ -302,6 +306,12 @@ def get_phy_slug(self): """ return str(self.pk) + '_' + slugify(self.title) + def slug(self): + """ + :return: slug of the object, based on title + """ + return slugify(self.title) + def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. @@ -318,12 +328,18 @@ def get_prod_path(self): return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' def get_text(self): + """ + :return: versioned text + """ if self.text: return get_blob( self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) def get_text_online(self): + """ + :return: public text of the extract + """ path = self.container.top_container().get_prod_path() + self.text + '.html' if os.path.exists(path): txt = open(path) @@ -346,7 +362,7 @@ class VersionedContent(Container): description = '' type = '' - licence = '' + licence = None # Information from DB sha_draft = None @@ -438,6 +454,29 @@ def dump_json(self, path=None): json_data.close() +def fill_container_from_json(json_sub, parent): + """ + Function which call itself to fill container + :param json_sub: dictionary from "manifest.json" + :param parent: the container to fill + """ + if 'children' in json_sub: + for child in json_sub['children']: + if child['obj_type'] == 'container': + new_container = Container(child['pk'], child['title']) + new_container.introduction = child['introduction'] + new_container.conclusion = child['conclusion'] + parent.add_container(new_container) + if 'children' in child: + fill_container_from_json(child, new_container) + elif child['obj_type'] == 'extract': + new_extract = Extract(child['pk'], child['title']) + new_extract.text = child['text'] + parent.add_extract(new_extract) + else: + raise Exception('Unknown object type'+child['obj_type']) + + class PublishableContent(models.Model): """ A tutorial whatever its size or an article. @@ -450,8 +489,6 @@ class PublishableContent(models.Model): - Public, beta, validation and draft sha, for versioning ; - Comment support ; - Type, which is either "ARTICLE" or "TUTORIAL" - - These are two repositories : draft and online. """ class Meta: verbose_name = 'Contenu' @@ -515,6 +552,24 @@ class Meta: def __unicode__(self): return self.title + def get_phy_slug(self): + """ + :return: physical slug, used for filesystem representation + """ + return str(self.pk) + '_' + self.slug + + def get_path(self, relative=False): + """ + Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. + :return: physical path + """ + if relative: + return '' + else: + # get the full path (with tutorial/article before it) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + def in_beta(self): """ A tutorial is not in beta if sha_beta is `None` or empty @@ -556,21 +611,48 @@ def is_tutorial(self): """ return self.type == 'TUTORIAL' - def load_json_for_public(self, sha=None): + def load_version(self, sha=None, public=False): """ - Fetch the public version of the JSON file for this content. + Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if + `public` is `True`). + Note: for practical reason, the returned object is filled with information form DB. :param sha: version - :return: a dictionary containing the structure of the JSON file. + :param public: if `True`, use `sha_public` instead of `sha_draft` if `sha` is `None` + :return: the versioned content """ + # load the good manifest.json if sha is None: - sha = self.sha_public - repo = Repo(self.get_path()) # should be `get_prod_path()` !?! - mantuto = get_blob(repo.commit(sha).tree, 'manifest.json') - data = json_reader.loads(mantuto) - if 'licence' in data: - data['licence'] = Licence.objects.filter(code=data['licence']).first() - return data - # TODO: mix that with next function + if not public: + sha = self.sha_draft + else: + sha = self.sha_public + path = os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + repo = Repo(path) + data = get_blob(repo.commit(sha).tree, 'manifest.json') + json = json_reader.loads(data) + + # create and fill the container + versioned = VersionedContent(sha, self.pk, self.type, json['title']) + if 'version' in json and json['version'] == 2: + # fill metadata : + versioned.description = json['description'] + if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': + versioned.type = json['type'] + else: + versioned.type = self.type + if 'licence' in json: + versioned.licence = Licence.objects.filter(code=data['licence']).first() + versioned.introduction = json['introduction'] + versioned.conclusion = json['conclusion'] + # then, fill container with children + fill_container_from_json(json, versioned) + # TODO extra metadata from BDD + + else: + raise Exception('Importation of old version is not yet supported') + # TODO so here we can support old version !! + + return versioned def load_dic(self, mandata, sha=None): # TODO should load JSON and store it in VersionedContent @@ -582,12 +664,11 @@ def load_dic(self, mandata, sha=None): # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'is_big', 'is_mini', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'get_path', 'in_beta', - 'in_validation', 'on_line' + 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public' ] attrs = [ - 'pk', 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', + 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' ] @@ -598,30 +679,9 @@ def load_dic(self, mandata, sha=None): mandata[attr] = getattr(self, attr) # general information - mandata['slug'] = slugify(mandata['title']) mandata['is_beta'] = self.in_beta() and self.sha_beta == sha mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_on_line'] = self.in_public() and self.sha_public == sha - - # url: - mandata['get_absolute_url'] = reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, mandata['slug']]) - - if self.in_beta(): - mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorialv2.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) + '?version=' + self.sha_beta - - else: - mandata['get_absolute_url_beta'] = reverse( - 'zds.tutorialv2.views.view_tutorial', - args=[self.pk, mandata['slug']] - ) - - mandata['get_absolute_url_online'] = reverse( - 'zds.tutorialv2.views.view_tutorial_online', - args=[self.pk, mandata['slug']] - ) + mandata['is_public'] = self.in_public() and self.sha_public == sha def save(self, *args, **kwargs): self.slug = slugify(self.title) diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index ca9f27c9ed..193513f183 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -3,12 +3,18 @@ # NOTE : this file is only there for tests purpose, it will be deleted in final version import os +import shutil +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from zds.settings import SITE_ROOT -from zds.tutorialv2.models import Container, Extract, VersionedContent +from zds.member.factories import ProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory +from zds.gallery.factories import GalleryFactory + +# from zds.tutorialv2.models import Container, Extract, VersionedContent @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @@ -18,15 +24,37 @@ class ContentTests(TestCase): def setUp(self): - self.start_version = 'ca5508a' # real version, adapt it ! - self.content = VersionedContent(self.start_version, 1, 'TUTORIAL', 'Mon tutoriel no1') + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + self.mas = ProfileFactory().user + settings.ZDS_APP['member']['bot_account'] = self.mas.username + + self.licence = LicenceFactory() + + self.user_author = ProfileFactory().user + self.tuto = PublishableContentFactory(type='TUTORIAL') + self.tuto.authors.add(self.user_author) + self.tuto.gallery = GalleryFactory() + self.tuto.licence = self.licence + self.tuto.save() - self.container = Container(1, 'Mon chapitre no1') - self.content.add_container(self.container) + self.tuto_draft = self.tuto.load_version() + self.chapter1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) - self.extract = Extract(1, 'Un premier extrait') - self.container.add_extract(self.extract) - self.content.update_children() + self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) def test_workflow_content(self): - print(self.container.position_in_parent) + versioned = self.tuto.load_version() + self.assertEqual(self.tuto_draft.title, versioned.title) + self.assertEqual(self.chapter1.title, versioned.children[0].title) + self.assertEqual(self.extract1.title, versioned.children[0].children[0].title) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): + shutil.rmtree(settings.ZDS_APP['tutorial']['repo_path']) + if os.path.isdir(settings.ZDS_APP['tutorial']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['tutorial']['repo_public_path']) + if os.path.isdir(settings.ZDS_APP['article']['repo_path']): + shutil.rmtree(settings.ZDS_APP['article']['repo_path']) + if os.path.isdir(settings.MEDIA_ROOT): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index c2b875b4c3..61db22d480 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -62,7 +62,7 @@ from django.utils.translation import ugettext as _ from django.views.generic import ListView, DetailView # , UpdateView # until we completely get rid of these, import them : -from zds.tutorial.models import Tutorial, Chapter, Part, HelpWriting +from zds.tutorial.models import Tutorial, Chapter, Part class ArticleList(ListView): diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py index 6959b0c60d..352be20a1c 100644 --- a/zds/utils/tutorialv2.py +++ b/zds/utils/tutorialv2.py @@ -8,6 +8,7 @@ def export_extract(extract): :return: dictionary containing the information """ dct = OrderedDict() + dct['obj_type'] = 'extract' dct['pk'] = extract.pk dct['title'] = extract.title dct['text'] = extract.text @@ -21,9 +22,9 @@ def export_container(container): :return: dictionary containing the information """ dct = OrderedDict() + dct['obj_type'] = "container" dct['pk'] = container.pk dct['title'] = container.title - dct['obj_type'] = "container" dct['introduction'] = container.introduction dct['conclusion'] = container.conclusion dct['children'] = [] @@ -47,6 +48,7 @@ def export_content(content): dct = export_container(content) # append metadata : + dct['version'] = 2 # to recognize old and new version of the content dct['description'] = content.description dct['type'] = content.type if content.licence: From f0c51e3349d0e477e09c75c88d69c107ffc6e69e Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Tue, 30 Dec 2014 20:41:34 +0100 Subject: [PATCH 105/887] MaJ : suppression du pk et ajout de slugs uniques --- zds/tutorial/factories.py | 18 +-- zds/tutorial/tests/tests.py | 44 +++--- zds/tutorial/views.py | 22 +-- zds/tutorialv2/factories.py | 26 ++-- zds/tutorialv2/models.py | 207 +++++++++++++++------------ zds/tutorialv2/tests/tests_models.py | 59 +++++++- zds/tutorialv2/views.py | 58 ++++---- zds/utils/tutorials.py | 6 +- zds/utils/tutorialv2.py | 8 +- 9 files changed, 259 insertions(+), 189 deletions(-) diff --git a/zds/tutorial/factories.py b/zds/tutorial/factories.py index dd9147ee72..3351339dea 100644 --- a/zds/tutorial/factories.py +++ b/zds/tutorial/factories.py @@ -47,7 +47,7 @@ def _prepare(cls, create, **kwargs): light = kwargs.pop('light', False) tuto = super(BigTutorialFactory, cls)._prepare(create, **kwargs) - path = tuto.get_path() + path = tuto.get_repo_path() real_content = content if light: real_content = content_light @@ -97,7 +97,7 @@ def _prepare(cls, create, **kwargs): if light: real_content = content_light - path = tuto.get_path() + path = tuto.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -144,8 +144,8 @@ def _prepare(cls, create, **kwargs): if light: real_content = content_light - path = part.get_path() - repo = Repo(part.tutorial.get_path()) + path = part.get_repo_path() + repo = Repo(part.tutorial.get_repo_path()) if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -202,7 +202,7 @@ def _prepare(cls, create, **kwargs): real_content = content if light: real_content = content_light - path = chapter.get_path() + path = chapter.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -235,25 +235,25 @@ def _prepare(cls, create, **kwargs): chapter.save() f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), chapter.introduction), "w") f.write(real_content.encode('utf-8')) f.close() f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), chapter.conclusion), "w") f.write(real_content.encode('utf-8')) f.close() part.tutorial.save() - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) man = export_tutorial(part.tutorial) f = open( os.path.join( - part.tutorial.get_path(), + part.tutorial.get_repo_path(), 'manifest.json'), "w") f.write( diff --git a/zds/tutorial/tests/tests.py b/zds/tutorial/tests/tests.py index 0ef2404daf..8ecc13fb9d 100644 --- a/zds/tutorial/tests/tests.py +++ b/zds/tutorial/tests/tests.py @@ -1149,8 +1149,8 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", 'msg_commit': u"Mise à jour de la partie", - "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + "last_hash": compute_hash([os.path.join(p2.tutorial.get_repo_path(), p2.introduction), + os.path.join(p2.tutorial.get_repo_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text=u"Partie 2 : edition de titre") @@ -1167,8 +1167,8 @@ def test_workflow_tuto(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"Mise à jour du chapitre", "last_hash": compute_hash([ - os.path.join(c3.get_path(), "introduction.md"), - os.path.join(c3.get_path(), "conclusion.md")]) + os.path.join(c3.get_repo_path(), "introduction.md"), + os.path.join(c3.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertContains(response=result, text=u"Chapitre 3 : edition de titre") @@ -1185,8 +1185,8 @@ def test_workflow_tuto(self): 'introduction': u"Expérimentation : seconde edition d'introduction", 'conclusion': u"C'est terminé : seconde edition de conlusion", 'msg_commit': u"2nd Màj de la partie 2", - "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), - os.path.join(p2.tutorial.get_path(), p2.conclusion)]) + "last_hash": compute_hash([os.path.join(p2.tutorial.get_repo_path(), p2.introduction), + os.path.join(p2.tutorial.get_repo_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text=u"Partie 2 : seconde edition de titre") @@ -1203,8 +1203,8 @@ def test_workflow_tuto(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"MàJ du chapitre 2", "last_hash": compute_hash([ - os.path.join(c2.get_path(), "introduction.md"), - os.path.join(c2.get_path(), "conclusion.md")]) + os.path.join(c2.get_repo_path(), "introduction.md"), + os.path.join(c2.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertContains(response=result, text=u"Chapitre 2 : edition de titre") @@ -1218,7 +1218,7 @@ def test_workflow_tuto(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"Agrume", - "last_hash": compute_hash([os.path.join(e2.get_path())]) + "last_hash": compute_hash([os.path.join(e2.get_repo_path())]) }, follow=True) self.assertContains(response=result, text=u"Extrait 2 : edition de titre") @@ -1665,8 +1665,8 @@ def test_conflict_does_not_destroy(self): }, follow=False) p1 = Part.objects.last() - hash = compute_hash([os.path.join(p1.tutorial.get_path(), p1.introduction), - os.path.join(p1.tutorial.get_path(), p1.conclusion)]) + hash = compute_hash([os.path.join(p1.tutorial.get_repo_path(), p1.introduction), + os.path.join(p1.tutorial.get_repo_path(), p1.conclusion)]) self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk), { @@ -1698,8 +1698,8 @@ def test_conflict_does_not_destroy(self): }, follow=False) c1 = Chapter.objects.last() - hash = compute_hash([os.path.join(c1.get_path(), "introduction.md"), - os.path.join(c1.get_path(), "conclusion.md")]) + hash = compute_hash([os.path.join(c1.get_repo_path(), "introduction.md"), + os.path.join(c1.get_repo_path(), "conclusion.md")]) self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk), { @@ -2675,8 +2675,8 @@ def test_change_update(self): 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", 'msg_commit': u"Changement de la partie", - "last_hash": compute_hash([os.path.join(part.tutorial.get_path(), part.introduction), - os.path.join(part.tutorial.get_path(), part.conclusion)]) + "last_hash": compute_hash([os.path.join(part.tutorial.get_repo_path(), part.introduction), + os.path.join(part.tutorial.get_repo_path(), part.conclusion)]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -2713,8 +2713,8 @@ def test_change_update(self): 'conclusion': u"Edition de conlusion", 'msg_commit': u"MàJ du chapitre 2 : le respect des agrumes sur ZdS", "last_hash": compute_hash([ - os.path.join(chapter.get_path(), "introduction.md"), - os.path.join(chapter.get_path(), "conclusion.md")]) + os.path.join(chapter.get_repo_path(), "introduction.md"), + os.path.join(chapter.get_repo_path(), "conclusion.md")]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -2745,7 +2745,7 @@ def test_change_update(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"On ne torture pas les agrumes !", - "last_hash": compute_hash([os.path.join(extract.get_path())]) + "last_hash": compute_hash([os.path.join(extract.get_repo_path())]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -3134,7 +3134,7 @@ def test_add_extract_named_introduction(self): tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(Extract.objects.all().count(), 1) intro_path = os.path.join(tuto.get_path(), "introduction.md") - extract_path = Extract.objects.first().get_path() + extract_path = Extract.objects.first().get_repo_path() self.assertNotEqual(intro_path, extract_path) self.assertTrue(os.path.isfile(intro_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -3158,7 +3158,7 @@ def test_add_extract_named_conclusion(self): tuto = Tutorial.objects.get(pk=self.minituto.pk) self.assertEqual(Extract.objects.all().count(), 1) ccl_path = os.path.join(tuto.get_path(), "conclusion.md") - extract_path = Extract.objects.first().get_path() + extract_path = Extract.objects.first().get_repo_path() self.assertNotEqual(ccl_path, extract_path) self.assertTrue(os.path.isfile(ccl_path)) self.assertTrue(os.path.isfile(extract_path)) @@ -4094,7 +4094,7 @@ def test_workflow_tuto(self): { 'title': u"Extrait 2 : edition de titre", 'text': u"Edition d'introduction", - "last_hash": compute_hash([e2.get_path()]) + "last_hash": compute_hash([e2.get_repo_path()]) }, follow=True) self.assertEqual(result.status_code, 200) @@ -4557,7 +4557,7 @@ def test_change_update(self): { 'title': u"Un autre titre", 'text': u"j'ai changé d'avis, je vais mettre un sapin synthétique", - "last_hash": compute_hash([extract.get_path()]) + "last_hash": compute_hash([extract.get_repo_path()]) }, follow=True) self.assertEqual(result.status_code, 200) diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index 92f6c7355e..2d95cd958c 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -2340,9 +2340,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2733,7 +2733,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) index = repo.index # update the tutorial last edit date part.tutorial.update = datetime.now() @@ -2753,19 +2753,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = request.user.email @@ -2838,10 +2838,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_path(), + man_path = os.path.join(chapter.part.tutorial.get_repo_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2908,14 +2908,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_path(relative=True)]) + index.add([extract.get_repo_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index e0e8a78330..2937933807 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -7,6 +7,7 @@ import factory from models import PublishableContent, Container, Extract, ContentReaction,\ +from zds.utils import slugify from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory @@ -24,7 +25,7 @@ class PublishableContentFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) - path = publishable_content.get_path() + path = publishable_content.get_repo_path() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -33,9 +34,9 @@ def _prepare(cls, create, **kwargs): introduction = 'introduction.md' conclusion = 'conclusion.md' versioned_content = VersionedContent(None, - publishable_content.pk, publishable_content.type, - publishable_content.title) + publishable_content.title, + slugify(publishable_content.title)) versioned_content.introduction = introduction versioned_content.conclusion = conclusion @@ -64,16 +65,17 @@ class ContainerFactory(factory.Factory): FACTORY_FOR = Container title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n+1)) - pk = factory.Sequence(lambda n: n+1) + slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) container = super(ContainerFactory, cls)._prepare(create, **kwargs) - container.parent.add_container(container) + container.parent.add_container(container, generate_slug=True) path = container.get_path() repo = Repo(container.top_container().get_path()) + top_container = container.top_container() if not os.path.isdir(path): os.makedirs(path, mode=0o777) @@ -81,16 +83,15 @@ def _prepare(cls, create, **kwargs): container.introduction = os.path.join(container.get_path(relative=True), 'introduction.md') container.conclusion = os.path.join(container.get_path(relative=True), 'conclusion.md') - f = open(os.path.join(parent.get_path(), part.introduction), "w") + f = open(os.path.join(top_container.get_path(), container.introduction), "w") f.write(text_content.encode('utf-8')) f.close() - repo.index.add([container.introduction]) - f = open(os.path.join(parent.get_path(), part.conclusion), "w") + f = open(os.path.join(top_container.get_path(), container.conclusion), "w") f.write(text_content.encode('utf-8')) f.close() - repo.index.add([container.conclusion]) + repo.index.add([container.introduction, container.conclusion]) - if parent: + top_container.dump_json() parent.save() repo.index.add(['manifest.json']) @@ -107,16 +108,15 @@ class ExtractFactory(factory.Factory): FACTORY_FOR = Extract title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n+1)) - pk = factory.Sequence(lambda n: n+1) + slug = '' @classmethod def _prepare(cls, create, **kwargs): db_object = kwargs.pop('db_object', None) extract = super(ExtractFactory, cls)._prepare(create, **kwargs) - extract.container.add_extract(extract) + extract.container.add_extract(extract, generate_slug=True) extract.text = extract.get_path(relative=True) - top_container = extract.container.top_container() repo = Repo(top_container.get_path()) f = open(extract.get_path(), 'w') diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 46ed6ac433..1948ca584b 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -4,7 +4,7 @@ import shutil try: import ujson as json_reader -except: +except ImportError: try: import simplejson as json_reader except: @@ -58,22 +58,25 @@ class Container: A container could be either a tutorial/article, a part or a chapter. """ - pk = 0 title = '' + slug = '' introduction = None conclusion = None parent = None position_in_parent = 1 children = [] + children_dict = {} # TODO: thumbnails ? - def __init__(self, pk, title, parent=None, position_in_parent=1): - self.pk = pk + def __init__(self, title, slug='', parent=None, position_in_parent=1): self.title = title + self.slug = slug self.parent = parent self.position_in_parent = position_in_parent + self.children = [] # even if you want, do NOT remove this line + self.children_dict = {} def __unicode__(self): return u''.format(self.title) @@ -127,43 +130,43 @@ def top_container(self): current = current.parent return current - def add_container(self, container): + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. :param container: the new container + :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_extracts(): if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + if generate_slug: + container.slug = self.top_container().get_unique_slug(container.title) + else: + self.top_container().add_slug_to_pool(container.slug) container.parent = self container.position_in_parent = self.get_last_child_position() + 1 self.children.append(container) + self.children_dict[container.slug] = container else: raise InvalidOperationError("Cannot add another level to this content") else: raise InvalidOperationError("Can't add a container if this container contains extracts.") # TODO: limitation if article ? - def add_extract(self, extract): + def add_extract(self, extract, generate_slug=False): """ Add a child container, but only if no container were previously added :param extract: the new extract + :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_sub_containers(): + if generate_slug: + extract.slug = self.top_container().get_unique_slug(extract.title) + else: + self.top_container().add_slug_to_pool(extract.slug) extract.container = self extract.position_in_parent = self.get_last_child_position() + 1 self.children.append(extract) - - def get_phy_slug(self): - """ - :return: the physical slug, used to represent data in filesystem - """ - return str(self.pk) + '_' + slugify(self.title) - - def slug(self): - """ - :return: slug of the object, based on title - """ - return slugify(self.title) + self.children_dict[extract.slug] = extract def update_children(self): """ @@ -190,7 +193,7 @@ def get_path(self, relative=False): base = '' if self.parent: base = self.parent.get_path(relative=relative) - return os.path.join(base, self.get_phy_slug()) + return os.path.join(base, self.slug) def get_prod_path(self): """ @@ -201,7 +204,7 @@ def get_prod_path(self): base = '' if self.parent: base = self.parent.get_prod_path() - return os.path.join(base, self.get_phy_slug()) + return os.path.join(base, self.slug) def get_introduction(self): """ @@ -256,14 +259,14 @@ class Extract: """ title = '' + slug = '' container = None position_in_container = 1 text = None - pk = 0 - def __init__(self, pk, title, container=None, position_in_container=1): - self.pk = pk + def __init__(self, title, slug='', container=None, position_in_container=1): self.title = title + self.slug = slug self.container = container self.position_in_container = position_in_container @@ -300,32 +303,21 @@ def get_absolute_url_beta(self): slugify(self.title) ) - def get_phy_slug(self): - """ - :return: the physical slug - """ - return str(self.pk) + '_' + slugify(self.title) - - def slug(self): - """ - :return: slug of the object, based on title - """ - return slugify(self.title) - def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ - return os.path.join(self.container.get_path(relative=relative), self.get_phy_slug()) + '.md' + return os.path.join(self.container.get_path(relative=relative), self.slug) + '.md' def get_prod_path(self): """ Get the physical path to the public version of a specific version of the extract. :return: physical path """ - return os.path.join(self.container.get_prod_path(), self.get_phy_slug()) + '.md.html' + return os.path.join(self.container.get_prod_path(), self.slug) + '.md.html' + # TODO: should this function exists ? (there will be no public version of a text, all in parent container) def get_text(self): """ @@ -350,7 +342,7 @@ def get_text_online(self): class VersionedContent(Container): """ - This class is used to handle a specific version of a tutorial. + This class is used to handle a specific version of a tutorial.tutorial It is created from the "manifest.json" file, and could dump information in it. @@ -360,11 +352,14 @@ class VersionedContent(Container): current_version = None repository = None + # Metadata from json : description = '' type = '' licence = None - # Information from DB + slug_pool = [] + + # Metadata from DB : sha_draft = None sha_beta = None sha_public = None @@ -372,15 +367,30 @@ class VersionedContent(Container): is_beta = False is_validation = False is_public = False - - # TODO `load_dic()` provide more information, actually - - def __init__(self, current_version, pk, _type, title): - Container.__init__(self, pk, title) + in_beta = False + in_validation = False + in_public = False + + have_markdown = False + have_html = False + have_pdf = False + have_epub = False + + authors = None + subcategory = None + image = None + creation_date = None + pubdate = None + update_date = None + source = None + + def __init__(self, current_version, _type, title, slug): + Container.__init__(self, title, slug) self.current_version = current_version self.type = _type self.repository = Repo(self.get_path()) - # so read JSON ? + + self.slug_pool = ['introduction', 'conclusion', slug] # forbidden slugs def __unicode__(self): return self.title @@ -389,13 +399,13 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.pk, slugify(self.title)]) + return reverse('zds.tutorialv2.views.view_tutorial', args=[self.slug]) def get_absolute_url_online(self): """ :return: the url to access the tutorial when online """ - return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.pk, slugify(self.title)]) + return reverse('zds.tutorialv2.views.view_tutorial_online', args=[self.slug]) def get_absolute_url_beta(self): """ @@ -410,7 +420,33 @@ def get_edit_url(self): """ :return: the url to edit the tutorial """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.pk) + return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) + + def get_unique_slug(self, title): + """ + Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a + "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. + :param title: title from which the slug is generated (with `slugify()`) + :return: the unique slug + """ + new_slug = slugify(title) + if new_slug in self.slug_pool: + num = 1 + while new_slug + '-' + str(num) in self.slug_pool: + num += 1 + new_slug = new_slug + '-' + str(num) + self.slug_pool.append(new_slug) + return new_slug + + def add_slug_to_pool(self, slug): + """ + Add a slug to the slug pool to be taken into account when generate a unique slug + :param slug: the slug to add + """ + if slug not in self.slug_pool: + self.slug_pool.append(slug) + else: + raise Exception('slug {} already in the slug pool !'.format(slug)) def get_path(self, relative=False): """ @@ -422,14 +458,14 @@ def get_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) def get_prod_path(self): """ Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.slug) def get_json(self): """ @@ -454,27 +490,29 @@ def dump_json(self, path=None): json_data.close() -def fill_container_from_json(json_sub, parent): +def fill_containers_from_json(json_sub, parent): """ Function which call itself to fill container :param json_sub: dictionary from "manifest.json" :param parent: the container to fill """ + # TODO should be static function of `VersionedContent` + # TODO should implement fallbacks if 'children' in json_sub: for child in json_sub['children']: - if child['obj_type'] == 'container': - new_container = Container(child['pk'], child['title']) + if child['object'] == 'container': + new_container = Container(child['title'], child['slug']) new_container.introduction = child['introduction'] new_container.conclusion = child['conclusion'] parent.add_container(new_container) if 'children' in child: - fill_container_from_json(child, new_container) - elif child['obj_type'] == 'extract': - new_extract = Extract(child['pk'], child['title']) + fill_containers_from_json(child, new_container) + elif child['object'] == 'extract': + new_extract = Extract(child['title'], child['slug']) new_extract.text = child['text'] parent.add_extract(new_extract) else: - raise Exception('Unknown object type'+child['obj_type']) + raise Exception('Unknown object type'+child['object']) class PublishableContent(models.Model): @@ -552,15 +590,9 @@ class Meta: def __unicode__(self): return self.title - def get_phy_slug(self): - """ - :return: physical slug, used for filesystem representation - """ - return str(self.pk) + '_' + self.slug - - def get_path(self, relative=False): + def get_repo_path(self, relative=False): """ - Get the physical path to the draft version of the Content. + Get the path to the tutorial repository :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ @@ -568,7 +600,7 @@ def get_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) def in_beta(self): """ @@ -596,7 +628,6 @@ def in_public(self): A tutorial is not in on line if sha_public is `None` or empty :return: `True` if the tutorial is on line, `False` otherwise """ - # TODO: for the logic with previous method, why not `in_public()` ? return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_article(self): @@ -626,13 +657,13 @@ def load_version(self, sha=None, public=False): sha = self.sha_draft else: sha = self.sha_public - path = os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.get_phy_slug()) + path = self.get_repo_path() repo = Repo(path) data = get_blob(repo.commit(sha).tree, 'manifest.json') json = json_reader.loads(data) # create and fill the container - versioned = VersionedContent(sha, self.pk, self.type, json['title']) + versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: # fill metadata : versioned.description = json['description'] @@ -642,11 +673,12 @@ def load_version(self, sha=None, public=False): versioned.type = self.type if 'licence' in json: versioned.licence = Licence.objects.filter(code=data['licence']).first() + # TODO must default licence be enforced here ? versioned.introduction = json['introduction'] versioned.conclusion = json['conclusion'] # then, fill container with children - fill_container_from_json(json, versioned) - # TODO extra metadata from BDD + fill_containers_from_json(json, versioned) + self.insert_data_in_versioned(versioned) else: raise Exception('Importation of old version is not yet supported') @@ -654,37 +686,30 @@ def load_version(self, sha=None, public=False): return versioned - def load_dic(self, mandata, sha=None): - # TODO should load JSON and store it in VersionedContent + def insert_data_in_versioned(self, versioned): """ - Fill mandata with information from database model and add 'slug', 'is_beta', 'is_validation', 'is_on_line'. - :param mandata: a dictionary from JSON file - :param sha: current version, used to fill the `is_*` fields by comparison with the corresponding `sha_*` + Insert some additional data from database in a VersionedContent + :param versioned: the VersionedContent to fill """ - # TODO: give it a more explicit name such as `insert_data_in_json()` ? fns = [ - 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public' - ] - - attrs = [ - 'authors', 'subcategory', 'image', 'pubdate', 'update', 'source', 'sha_draft', 'sha_beta', - 'sha_validation', 'sha_public' + 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public', + 'authors', 'subcategory', 'image', 'creation_date', 'pubdate', 'update_date', 'source', 'sha_draft', + 'sha_beta', 'sha_validation', 'sha_public' ] - # load functions and attributs in tree + # load functions and attributs in `versioned` for fn in fns: - mandata[fn] = getattr(self, fn) - for attr in attrs: - mandata[attr] = getattr(self, attr) + setattr(versioned, fn, getattr(self, fn)) # general information - mandata['is_beta'] = self.in_beta() and self.sha_beta == sha - mandata['is_validation'] = self.in_validation() and self.sha_validation == sha - mandata['is_public'] = self.in_public() and self.sha_public == sha + versioned.is_beta = self.in_beta() and self.sha_beta == versioned.current_version + versioned.is_validation = self.in_validation() and self.sha_validation == versioned.current_version + versioned.is_public = self.in_public() and self.sha_public == versioned.current_version def save(self, *args, **kwargs): self.slug = slugify(self.title) + # TODO ensure unique slug here !! super(PublishableContent, self).save(*args, **kwargs) @@ -803,7 +828,7 @@ def delete_entity_and_tree(self): """ Delete the entities and their filesystem counterparts """ - shutil.rmtree(self.get_path(), False) + shutil.rmtree(self.get_repo_path(), False) Validation.objects.filter(tutorial=self).delete() if self.gallery is not None: diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 193513f183..9c2c3b1323 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -16,11 +16,14 @@ # from zds.tutorialv2.models import Container, Extract, VersionedContent +overrided_zds_app = settings.ZDS_APP +overrided_zds_app['tutorial']['repo_path'] = os.path.join(SITE_ROOT, 'tutoriels-private-test') +overrided_zds_app['tutorial']['repo__public_path'] = os.path.join(SITE_ROOT, 'tutoriels-public-test') +overrided_zds_app['article']['repo_path'] = os.path.join(SITE_ROOT, 'article-data-test') + @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) -@override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) -@override_settings(REPO_PATH_PROD=os.path.join(SITE_ROOT, 'tutoriels-public-test')) -@override_settings(REPO_ARTICLE_PATH=os.path.join(SITE_ROOT, 'articles-data-test')) +@override_settings(ZDS_APP=overrided_zds_app) class ContentTests(TestCase): def setUp(self): @@ -38,16 +41,58 @@ def setUp(self): self.tuto.save() self.tuto_draft = self.tuto.load_version() - self.chapter1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) - self.chapter2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto) self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) def test_workflow_content(self): + """ + General tests for a content + """ + # ensure the usability of manifest versioned = self.tuto.load_version() self.assertEqual(self.tuto_draft.title, versioned.title) - self.assertEqual(self.chapter1.title, versioned.children[0].title) - self.assertEqual(self.extract1.title, versioned.children[0].children[0].title) + self.assertEqual(self.part1.title, versioned.children[0].title) + self.assertEqual(self.extract1.title, versioned.children[0].children[0].children[0].title) + + # ensure url resolution project using dictionary : + self.assertTrue(self.part1.slug in versioned.children_dict.keys()) + self.assertTrue(self.chapter1.slug in versioned.children_dict[self.part1.slug].children_dict) + + def test_ensure_unique_slug(self): + """ + Ensure that slugs for a container or extract are always unique + """ + # get draft version + versioned = self.tuto.load_version() + + # forbidden slugs : + slug_to_test = ['introduction', # forbidden slug + 'conclusion', # forbidden slug + self.tuto.slug, # forbidden slug + # slug normally already in the slug pool : + self.part1.slug, + self.chapter1.slug, + self.extract1.slug] + + for slug in slug_to_test: + new_slug = versioned.get_unique_slug(slug) + self.assertNotEqual(slug, new_slug) + self.assertTrue(new_slug in versioned.slug_pool) # ensure new slugs are in slug pool + + # then test with "real" containers and extracts : + new_chapter_1 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) + # now, slug "aa" is forbidden ! + new_chapter_2 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) + self.assertNotEqual(new_chapter_1.slug, new_chapter_2.slug) + new_extract_1 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) + self.assertNotEqual(new_extract_1.slug, new_chapter_2.slug) + self.assertNotEqual(new_extract_1.slug, new_chapter_1.slug) + new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) + self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) + self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) + print(versioned.get_json()) def tearDown(self): if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 61db22d480..c1df7929cd 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -152,7 +152,7 @@ class DisplayContent(DetailView): def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["slug"] = slugify(dictionary["title"]) dictionary["position_in_tutorial"] = cpt_p @@ -167,7 +167,7 @@ def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): def compatibility_chapter(self, content, repo, sha, dictionary): """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["type"] = self.type dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") @@ -175,7 +175,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): cpt = 1 for ext in dictionary["extracts"]: ext["position_in_chapter"] = cpt - ext["path"] = content.get_path() + ext["path"] = content.get_repo_path() ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt += 1 @@ -233,7 +233,7 @@ def get_context_data(self, **kwargs): # Find the good manifest file - repo = Repo(content.get_path()) + repo = Repo(content.get_repo_path()) # Load the tutorial. @@ -293,7 +293,7 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied # open git repo and find diff between displayed version and head - repo = Repo(context[self.context_object_name].get_path()) + repo = Repo(context[self.context_object_name].get_repo_path()) current_version_commit = repo.commit(sha) diff_with_head = current_version_commit.diff("HEAD~1") context["path_add"] = diff_with_head.iter_change_type("A") @@ -319,7 +319,7 @@ def get_forms(self, context, content): def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content - dictionary["path"] = content.get_path() + dictionary["path"] = content.get_repo_path() dictionary["slug"] = slugify(dictionary["title"]) dictionary["position_in_tutorial"] = cpt_p @@ -522,7 +522,7 @@ def history(request, tutorial_pk, tutorial_slug): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) logs = repo.head.reference.log() logs = sorted(logs, key=attrgetter("time"), reverse=True) return render(request, "tutorial/tutorial/history.html", @@ -1162,7 +1162,7 @@ def add_tutorial(request): tutorial.save() maj_repo_tuto( request, - new_slug_path=tutorial.get_path(), + new_slug_path=tutorial.get_repo_path(), tuto=tutorial, introduction=data["introduction"], conclusion=data["conclusion"], @@ -1198,8 +1198,8 @@ def edit_tutorial(request): if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - introduction = os.path.join(tutorial.get_path(), "introduction.md") - conclusion = os.path.join(tutorial.get_path(), "conclusion.md") + introduction = os.path.join(tutorial.get_repo_path(), "introduction.md") + conclusion = os.path.join(tutorial.get_repo_path(), "conclusion.md") if request.method == "POST": form = TutorialForm(request.POST, request.FILES) if form.is_valid(): @@ -1221,7 +1221,7 @@ def edit_tutorial(request): "last_hash": compute_hash([introduction, conclusion]), "new_version": True }) - old_slug = tutorial.get_path() + old_slug = tutorial.get_repo_path() tutorial.title = data["title"] tutorial.description = data["description"] if "licence" in data and data["licence"] != "": @@ -1332,7 +1332,7 @@ def view_part( # find the good manifest file - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) manifest = get_blob(repo.commit(sha).tree, "manifest.json") mandata = json_reader.loads(manifest) tutorial.load_dic(mandata, sha=sha) @@ -1344,7 +1344,7 @@ def view_part( if part_pk == str(part["pk"]): find = True part["tutorial"] = tutorial - part["path"] = tutorial.get_path() + part["path"] = tutorial.get_repo_path() part["slug"] = slugify(part["title"]) part["position_in_tutorial"] = cpt_p part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) @@ -1352,7 +1352,7 @@ def view_part( cpt_c = 1 for chapter in part["chapters"]: chapter["part"] = part - chapter["path"] = tutorial.get_path() + chapter["path"] = tutorial.get_repo_path() chapter["slug"] = slugify(chapter["title"]) chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c @@ -1361,7 +1361,7 @@ def view_part( for ext in chapter["extracts"]: ext["chapter"] = chapter ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() + ext["path"] = tutorial.get_repo_path() cpt_e += 1 cpt_c += 1 final_part = part @@ -1677,7 +1677,7 @@ def view_chapter( # find the good manifest file - repo = Repo(tutorial.get_path()) + repo = Repo(tutorial.get_repo_path()) manifest = get_blob(repo.commit(sha).tree, "manifest.json") mandata = json_reader.loads(manifest) tutorial.load_dic(mandata, sha=sha) @@ -1701,7 +1701,7 @@ def view_chapter( part["tutorial"] = tutorial for chapter in part["chapters"]: chapter["part"] = part - chapter["path"] = tutorial.get_path() + chapter["path"] = tutorial.get_repo_path() chapter["slug"] = slugify(chapter["title"]) chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c @@ -1719,7 +1719,7 @@ def view_chapter( for ext in chapter["extracts"]: ext["chapter"] = chapter ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() + ext["path"] = tutorial.get_repo_path() ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) cpt_e += 1 chapter_tab.append(chapter) @@ -2371,9 +2371,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2763,7 +2763,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_path()) + repo = Repo(part.tutorial.get_repo_path()) index = repo.index if action == "del": shutil.rmtree(old_slug_path) @@ -2781,19 +2781,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = str(request.user.email) @@ -2862,10 +2862,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_path(), + man_path = os.path.join(chapter.part.tutorial.get_repo_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2927,14 +2927,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_path(relative=True)]) + index.add([extract.get_repo_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) diff --git a/zds/utils/tutorials.py b/zds/utils/tutorials.py index 9a8d792250..51bf9ad317 100644 --- a/zds/utils/tutorials.py +++ b/zds/utils/tutorials.py @@ -187,7 +187,7 @@ def export_tutorial_to_md(tutorial, sha=None): cpt_p = 1 for part in parts: part['tutorial'] = tutorial - part['path'] = tutorial.get_path() + part['path'] = tutorial.get_repo_path() part['slug'] = slugify(part['title']) part['position_in_tutorial'] = cpt_p intro = open( @@ -208,7 +208,7 @@ def export_tutorial_to_md(tutorial, sha=None): cpt_c = 1 for chapter in part['chapters']: chapter['part'] = part - chapter['path'] = tutorial.get_path() + chapter['path'] = tutorial.get_repo_path() chapter['slug'] = slugify(chapter['title']) chapter['type'] = 'BIG' chapter['position_in_part'] = cpt_c @@ -230,7 +230,7 @@ def export_tutorial_to_md(tutorial, sha=None): for ext in chapter['extracts']: ext['chapter'] = chapter ext['position_in_chapter'] = cpt_e - ext['path'] = tutorial.get_path() + ext['path'] = tutorial.get_repo_path() text = open( os.path.join( tutorial.get_prod_path(sha), diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py index 352be20a1c..15718e4605 100644 --- a/zds/utils/tutorialv2.py +++ b/zds/utils/tutorialv2.py @@ -8,8 +8,8 @@ def export_extract(extract): :return: dictionary containing the information """ dct = OrderedDict() - dct['obj_type'] = 'extract' - dct['pk'] = extract.pk + dct['object'] = 'extract' + dct['slug'] = extract.slug dct['title'] = extract.title dct['text'] = extract.text return dct @@ -22,8 +22,8 @@ def export_container(container): :return: dictionary containing the information """ dct = OrderedDict() - dct['obj_type'] = "container" - dct['pk'] = container.pk + dct['object'] = "container" + dct['slug'] = container.slug dct['title'] = container.title dct['introduction'] = container.introduction dct['conclusion'] = container.conclusion From cfd27be8037bf09d038ca95b0be9ac9e30f918c7 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 1 Jan 2015 21:11:36 +0100 Subject: [PATCH 106/887] views.py + fallbacks --- templates/tutorialv2/base.html | 104 ++++++ templates/tutorialv2/view.html | 486 +++++++++++++++++++++++++++ zds/tutorialv2/models.py | 79 +++-- zds/tutorialv2/tests/tests_models.py | 1 - zds/tutorialv2/urls.py | 191 +++++------ zds/tutorialv2/views.py | 61 ++-- zds/urls.py | 1 + 7 files changed, 766 insertions(+), 157 deletions(-) create mode 100644 templates/tutorialv2/base.html create mode 100644 templates/tutorialv2/view.html diff --git a/templates/tutorialv2/base.html b/templates/tutorialv2/base.html new file mode 100644 index 0000000000..73ef8519d1 --- /dev/null +++ b/templates/tutorialv2/base.html @@ -0,0 +1,104 @@ +{% extends "base_content_page.html" %} +{% load captureas %} +{% load i18n %} + + +{% block title_base %} + • {% trans "Tutoriels" %} +{% endblock %} + + + +{% block mobile_title %} + {% trans "Tutoriels" %} +{% endblock %} + + + +{% block breadcrumb_base %} + {% if user in tutorial.authors.all %} +
  • {% trans "Mes tutoriels" %}
  • + {% else %} +
  • + +
  • + {% endif %} +{% endblock %} + + + +{% block menu_tutorial %} + current +{% endblock %} + + + +{% block sidebar %} + +{% endblock %} diff --git a/templates/tutorialv2/view.html b/templates/tutorialv2/view.html new file mode 100644 index 0000000000..c53cf43273 --- /dev/null +++ b/templates/tutorialv2/view.html @@ -0,0 +1,486 @@ +{% extends "tutorialv2/base.html" %} +{% load emarkdown %} +{% load repo_reader %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load roman %} +{% load i18n %} + + +{% block title %} + {{ tutorial.title }} +{% endblock %} + + + +{% block breadcrumb %} +
  • {{ tutorial.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if tutorial.licence %} +

    + {{ tutorial.licence }} +

    + {% endif %} + +

    + {% if tutorial.image %} + + {% endif %} + {{ tutorial.title }} +

    + + {% if tutorial.description %} +

    + {{ tutorial.description }} +

    + {% endif %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial add_author=True %} + {% else %} + {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial %} + {% endif %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if tutorial.in_validation %} + {% if validation.version == version %} + {% if validation.is_pending %} +

    + {% trans "Ce tutoriel est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + {% trans "Le tutoriel est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% if validation.comment_authors %} +
    +

    + {% trans "Le message suivant a été laissé à destination des validateurs" %} : +

    + +
    + {{ validation.comment_authors|emarkdown }} +
    +
    + {% endif %} + {% else %} + {% if validation.is_pending %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if tutorial.is_beta %} +
    +
    + {% blocktrans %} + Cette version du tutoriel est en "BÊTA" ! + {% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + +{% block content %} + {% if tutorial.get_introduction and tutorial.get_introduction != "None" %} + {{ tutorial.get_introduction|emarkdown:is_js }} + {% elif not tutorial.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + {% if tutorial.is_mini %} + {# Small tutorial #} + + {% include "tutorial/includes/chapter.part.html" with authors=tutorial.authors.all %} + {% else %} + {# Large tutorial #} + +
    + + {% for part in parts %} +

    + + {{ part.position_in_tutorial|roman }} - {{ part.title }} + +

    + {% include "tutorial/includes/part.part.html" %} + {% empty %} +

    + {% trans "Il n'y a actuellement aucune partie dans ce tutoriel" %}. +

    + {% endfor %} + +
    + + {% endif %} + + {% if tutorial.get_conclusion and tutorial.get_conclusion != "None" %} + {{ tutorial.get_conclusion|emarkdown:is_js }} + {% elif not tutorial.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if tutorial.sha_draft = version %} + {% if not tutorial.is_mini %} + + {% trans "Ajouter une partie" %} + + {% else %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + + + {% trans "Éditer" %} + + {% else %} + + {% trans "Version brouillon" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} +
  • + + {% trans "Ajouter un auteur" %} + + +
  • +
  • + + {% trans "Gérer les auteurs" %} + + +
  • + + {% if tutorial.sha_public %} +
  • + + {% blocktrans %} + Voir la version en ligne + {% endblocktrans %} + +
  • + {% endif %} + + {% if not tutorial.in_beta %} +
  • + + {% trans "Mettre cette version en bêta" %} + + +
  • + {% else %} + {% if not tutorial.is_beta %} +
  • + + {% blocktrans %} + Voir la version en bêta + {% endblocktrans %} + +
  • +
  • + + {% trans "Mettre à jour la bêta avec cette version" %} + + +
  • + {% else %} +
  • + + {% trans "Cette version est déjà en bêta" %} + +
  • + {% endif %} +
  • + + {% trans "Désactiver la bêta" %} + + +
  • + {% endif %} + +
  • + + {% trans "Historique des versions" %} + +
  • + + {% if not tutorial.in_validation %} +
  • + + {% trans "Demander la validation" %} + +
  • + {% else %} + {% if not tutorial.is_validation %} +
  • + + {% trans "Mettre à jour la version en validation" %} + +
  • + {% endif %} +
  • + {% trans "En attente de validation" %} +
  • + {% endif %} + + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {% if perms.tutorial.change_tutorial %} + + {% endif %} + + {% include "tutorial/includes/summary.part.html" %} + + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + + {% endif %} +{% endblock %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 1948ca584b..cd6af0f5ae 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -357,7 +357,7 @@ class VersionedContent(Container): type = '' licence = None - slug_pool = [] + slug_pool = {} # Metadata from DB : sha_draft = None @@ -390,7 +390,7 @@ def __init__(self, current_version, _type, title, slug): self.type = _type self.repository = Repo(self.get_path()) - self.slug_pool = ['introduction', 'conclusion', slug] # forbidden slugs + self.slug_pool = {'introduction': 1, 'conclusion': 1, slug: 1} # forbidden slugs def __unicode__(self): return self.title @@ -399,7 +399,7 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('zds.tutorialv2.views.view_tutorial', args=[self.slug]) + return reverse('view-tutorial-url', args=[self.slug]) def get_absolute_url_online(self): """ @@ -429,13 +429,16 @@ def get_unique_slug(self, title): :param title: title from which the slug is generated (with `slugify()`) :return: the unique slug """ - new_slug = slugify(title) - if new_slug in self.slug_pool: - num = 1 - while new_slug + '-' + str(num) in self.slug_pool: - num += 1 - new_slug = new_slug + '-' + str(num) - self.slug_pool.append(new_slug) + base = slugify(title) + try: + n = self.slug_pool[base] + except KeyError: + new_slug = base + self.slug_pool[base] = 0 + else: + new_slug = base + '-' + str(n) + self.slug_pool[base] += 1 + self.slug_pool[new_slug] = 1 return new_slug def add_slug_to_pool(self, slug): @@ -443,10 +446,12 @@ def add_slug_to_pool(self, slug): Add a slug to the slug pool to be taken into account when generate a unique slug :param slug: the slug to add """ - if slug not in self.slug_pool: - self.slug_pool.append(slug) + try: + self.slug_pool[slug] # test access + except KeyError: + self.slug_pool[slug] = 1 else: - raise Exception('slug {} already in the slug pool !'.format(slug)) + raise Exception('slug "{}" already in the slug pool !'.format(slug)) def get_path(self, relative=False): """ @@ -502,8 +507,10 @@ def fill_containers_from_json(json_sub, parent): for child in json_sub['children']: if child['object'] == 'container': new_container = Container(child['title'], child['slug']) - new_container.introduction = child['introduction'] - new_container.conclusion = child['conclusion'] + if 'introduction' in child: + new_container.introduction = child['introduction'] + if 'conclusion' in child: + new_container.conclusion = child['conclusion'] parent.add_container(new_container) if 'children' in child: fill_containers_from_json(child, new_container) @@ -533,6 +540,7 @@ class Meta: verbose_name_plural = 'Contenus' title = models.CharField('Titre', max_length=80) + slug = models.SlugField(max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -630,6 +638,30 @@ def in_public(self): """ return (self.sha_public is not None) and (self.sha_public.strip() != '') + def is_beta(self, sha): + """ + Is this version of the content the beta version ? + :param sha: version + :return: `True` if the tutorial is in beta, `False` otherwise + """ + return self.in_beta() and sha == self.sha_beta + + def is_validation(self, sha): + """ + Is this version of the content the validation version ? + :param sha: version + :return: `True` if the tutorial is in validation, `False` otherwise + """ + return self.in_validation() and sha == self.sha_validation + + def is_public(self, sha): + """ + Is this version of the content the published version ? + :param sha: version + :return: `True` if the tutorial is in public, `False` otherwise + """ + return self.in_public() and sha == self.sha_public + def is_article(self): """ :return: `True` if article, `False` otherwise @@ -666,16 +698,19 @@ def load_version(self, sha=None, public=False): versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: # fill metadata : - versioned.description = json['description'] + if 'description' in json: + versioned.description = json['description'] if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': versioned.type = json['type'] else: versioned.type = self.type if 'licence' in json: - versioned.licence = Licence.objects.filter(code=data['licence']).first() + versioned.licence = Licence.objects.filter(code=json['licence']).first() # TODO must default licence be enforced here ? - versioned.introduction = json['introduction'] - versioned.conclusion = json['conclusion'] + if 'introduction' in json: + versioned.introduction = json['introduction'] + if 'conclusion' in json: + versioned.conclusion = json['conclusion'] # then, fill container with children fill_containers_from_json(json, versioned) self.insert_data_in_versioned(versioned) @@ -703,9 +738,9 @@ def insert_data_in_versioned(self, versioned): setattr(versioned, fn, getattr(self, fn)) # general information - versioned.is_beta = self.in_beta() and self.sha_beta == versioned.current_version - versioned.is_validation = self.in_validation() and self.sha_validation == versioned.current_version - versioned.is_public = self.in_public() and self.sha_public == versioned.current_version + versioned.is_beta = self.is_beta(versioned.current_version) + versioned.is_validation = self.is_validation(versioned.current_version) + versioned.is_public = self.is_public(versioned.current_version) def save(self, *args, **kwargs): self.slug = slugify(self.title) diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 9c2c3b1323..396e44e65b 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -92,7 +92,6 @@ def test_ensure_unique_slug(self): new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) - print(versioned.get_json()) def tearDown(self): if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 9871095ea8..956c2d78a2 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -18,116 +18,119 @@ url(r'^flux/atom/$', feeds.LastTutorialsFeedATOM(), name='tutorial-feed-atom'), # Current URLs - url(r'^recherche/(?P\d+)/$', - 'zds.tutorial.views.find_tuto'), + # url(r'^recherche/(?P\d+)/$', + # 'zds.tutorialv2.views.find_tuto'), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_chapter', - name="view-chapter-url"), + # url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_chapter', + # name="view-chapter-url"), - url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_part', - name="view-part-url"), + # url(r'^off/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_part', + # name="view-part-url"), - url(r'^tutorial/off/(?P\d+)/(?P.+)/$', - DisplayContent.as_view()), + url(r'^off/tutoriel/(?P.+)/$', + DisplayContent.as_view(), + name='view-tutorial-url'), - url(r'^article/off/(?P\d+)/(?P.+)/$', + url(r'^off/article/(?P.+)/$', DisplayArticle.as_view()), # View online - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_chapter_online', - name="view-chapter-url-online"), - - url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.view_part_online', - name="view-part-url-online"), - - url(r'^tutoriel/(?P\d+)/(?P.+)/$', - DisplayOnlineContent.as_view()), - - url(r'^article/(?P\d+)/(?P.+)/$', - DisplayOnlineArticle.as_view()), + # url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_chapter_online', + # name="view-chapter-url-online"), + # + # url(r'^(?P\d+)/(?P.+)/(?P\d+)/(?P.+)/$', + # 'zds.tutorialv2.views.view_part_online', + # name="view-part-url-online"), + # + # url(r'^tutoriel/(?P\d+)/(?P.+)/$', + # DisplayOnlineContent.as_view()), + # + # url(r'^article/(?P\d+)/(?P.+)/$', + # DisplayOnlineArticle.as_view()), # Editing - url(r'^editer/tutoriel/$', - 'zds.tutorial.views.edit_tutorial'), - url(r'^modifier/tutoriel/$', - 'zds.tutorial.views.modify_tutorial'), - url(r'^modifier/partie/$', - 'zds.tutorial.views.modify_part'), - url(r'^editer/partie/$', - 'zds.tutorial.views.edit_part'), - url(r'^modifier/chapitre/$', - 'zds.tutorial.views.modify_chapter'), - url(r'^editer/chapitre/$', - 'zds.tutorial.views.edit_chapter'), - url(r'^modifier/extrait/$', - 'zds.tutorial.views.modify_extract'), - url(r'^editer/extrait/$', - 'zds.tutorial.views.edit_extract'), + # url(r'^editer/tutoriel/$', + # 'zds.tutorialv2.views.edit_tutorial'), + # url(r'^modifier/tutoriel/$', + # 'zds.tutorialv2.views.modify_tutorial'), + # url(r'^modifier/partie/$', + # 'zds.tutorialv2.views.modify_part'), + # url(r'^editer/partie/$', + # 'zds.tutorialv2.views.edit_part'), + # url(r'^modifier/chapitre/$', + # 'zds.tutorialv2.views.modify_chapter'), + # url(r'^editer/chapitre/$', + # 'zds.tutorialv2.views.edit_chapter'), + # url(r'^modifier/extrait/$', + # 'zds.tutorialv2.views.modify_extract'), + # url(r'^editer/extrait/$', + # 'zds.tutorialv2.views.edit_extract'), # Adding - url(r'^nouveau/tutoriel/$', - 'zds.tutorial.views.add_tutorial'), - url(r'^nouveau/partie/$', - 'zds.tutorial.views.add_part'), - url(r'^nouveau/chapitre/$', - 'zds.tutorial.views.add_chapter'), - url(r'^nouveau/extrait/$', - 'zds.tutorial.views.add_extract'), - - url(r'^$', TutorialList.as_view, name='index-tutorial'), - url(r'^importer/$', 'zds.tutorial.views.import_tuto'), - url(r'^import_local/$', - 'zds.tutorial.views.local_import'), - url(r'^telecharger/$', 'zds.tutorial.views.download'), - url(r'^telecharger/pdf/$', - 'zds.tutorial.views.download_pdf'), - url(r'^telecharger/html/$', - 'zds.tutorial.views.download_html'), - url(r'^telecharger/epub/$', - 'zds.tutorial.views.download_epub'), - url(r'^telecharger/md/$', - 'zds.tutorial.views.download_markdown'), - url(r'^historique/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.history'), - url(r'^comparaison/(?P\d+)/(?P.+)/$', - 'zds.tutorial.views.diff'), + # url(r'^nouveau/tutoriel/$', + # 'zds.tutorialv2.views.add_tutorial'), + # url(r'^nouveau/partie/$', + # 'zds.tutorialv2.views.add_part'), + # url(r'^nouveau/chapitre/$', + # 'zds.tutorialv2.views.add_chapter'), + # url(r'^nouveau/extrait/$', + # 'zds.tutorialv2.views.add_extract'), + + # url(r'^$', TutorialList.as_view, name='index-tutorial'), + # url(r'^importer/$', 'zds.tutorialv2.views.import_tuto'), + # url(r'^import_local/$', + # 'zds.tutorialv2.views.local_import'), + # url(r'^telecharger/$', 'zds.tutorialv2.views.download'), + # url(r'^telecharger/pdf/$', + # 'zds.tutorialv2.views.download_pdf'), + # url(r'^telecharger/html/$', + # 'zds.tutorialv2.views.download_html'), + # url(r'^telecharger/epub/$', + # 'zds.tutorialv2.views.download_epub'), + # url(r'^telecharger/md/$', + # 'zds.tutorialv2.views.download_markdown'), + url(r'^historique/(?P.+)/$', + 'zds.tutorialv2.views.history', + name='view-tutorial-history-url'), + # url(r'^comparaison/(?P.+)/$', + # DisplayDiff.as_view(), + # name='view-tutorial-diff-url'), # user actions - url(r'^suppression/(?P\d+)/$', - 'zds.tutorial.views.delete_tutorial'), + url(r'^suppression/(?P.+)/$', + 'zds.tutorialv2.views.delete_tutorial'), url(r'^validation/tutoriel/$', - 'zds.tutorial.views.ask_validation'), + 'zds.tutorialv2.views.ask_validation'), # Validation - url(r'^validation/$', - 'zds.tutorial.views.list_validation'), - url(r'^validation/reserver/(?P\d+)/$', - 'zds.tutorial.views.reservation'), - url(r'^validation/reject/$', - 'zds.tutorial.views.reject_tutorial'), - url(r'^validation/valid/$', - 'zds.tutorial.views.valid_tutorial'), - url(r'^validation/invalid/(?P\d+)/$', - 'zds.tutorial.views.invalid_tutorial'), - url(r'^validation/historique/(?P\d+)/$', - 'zds.tutorial.views.history_validation'), - url(r'^activation_js/$', - 'zds.tutorial.views.activ_js'), - # Reactions - url(r'^message/editer/$', - 'zds.tutorial.views.edit_note'), - url(r'^message/nouveau/$', 'zds.tutorial.views.answer'), - url(r'^message/like/$', 'zds.tutorial.views.like_note'), - url(r'^message/dislike/$', - 'zds.tutorial.views.dislike_note'), - - # Moderation - url(r'^resolution_alerte/$', - 'zds.tutorial.views.solve_alert'), + # url(r'^validation/$', + # 'zds.tutorialv2.views.list_validation'), + # url(r'^validation/reserver/(?P\d+)/$', + # 'zds.tutorialv2.views.reservation'), + # url(r'^validation/reject/$', + # 'zds.tutorialv2.views.reject_tutorial'), + # url(r'^validation/valid/$', + # 'zds.tutorialv2.views.valid_tutorial'), + # url(r'^validation/invalid/(?P\d+)/$', + # 'zds.tutorialv2.views.invalid_tutorial'), + url(r'^validation/historique/(?P.+)/$', + 'zds.tutorialv2.views.history_validation'), + # url(r'^activation_js/$', + # 'zds.tutorialv2.views.activ_js'), + # # Reactions + # url(r'^message/editer/$', + # 'zds.tutorialv2.views.edit_note'), + # url(r'^message/nouveau/$', 'zds.tutorialv2.views.answer'), + # url(r'^message/like/$', 'zds.tutorialv2.views.like_note'), + # url(r'^message/dislike/$', + # 'zds.tutorialv2.views.dislike_note'), + # + # # Moderation + # url(r'^resolution_alerte/$', + # 'zds.tutorialv2.views.solve_alert'), # Help url(r'^aides/$', diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index c1df7929cd..b512d0307e 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -92,6 +92,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super(ArticleList, self).get_context_data(**kwargs) context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! return context @@ -100,7 +101,7 @@ class TutorialList(ArticleList): context_object_name = 'tutorials' type = "TUTORIAL" - template_name = 'tutorial/index.html' + template_name = 'tutorialv2/index.html' def render_chapter_form(chapter): @@ -119,7 +120,7 @@ class TutorialWithHelp(TutorialList): """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta for more documentation, have a look to ZEP 03 specification (fr)""" context_object_name = 'tutorials' - template_name = 'tutorial/help.html' + template_name = 'tutorialv2/help.html' def get_queryset(self): """get only tutorial that need help and handle filtering if asked""" @@ -141,15 +142,19 @@ def get_context_data(self, **kwargs): context['helps'] = HelpWriting.objects.all() return context +# TODO ArticleWithHelp + class DisplayContent(DetailView): """Base class that can show any content in any state, by default it shows offline tutorials""" model = PublishableContent - template_name = 'tutorial/view.html' + template_name = 'tutorialv2/view.html' type = "TUTORIAL" - is_public = False + online = False + sha = None + # TODO compatibility should be performed into class `PublishableContent.load_version()` ! def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): dictionary["tutorial"] = content dictionary["path"] = content.get_repo_path() @@ -181,7 +186,7 @@ def compatibility_chapter(self, content, repo, sha, dictionary): def get_forms(self, context, content): """get all the auxiliary forms about validation, js fiddle...""" - validation = Validation.objects.filter(tutorial__pk=content.pk)\ + validation = Validation.objects.filter(content__pk=content.pk)\ .order_by("-date_proposition")\ .first() form_js = ActivJsForm(initial={"js_support": content.js_support}) @@ -200,19 +205,20 @@ def get_forms(self, context, content): context["formValid"] = form_valid context["formReject"] = form_reject, - def get_object(self): - return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, slug=self.kwargs['content_slug']) def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" + # TODO: handling public version ! context = super(DisplayContent, self).get_context_data(**kwargs) - content = context[self.context_object_name] + content = context['object'] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't # exist, we take draft version of the content. - try: - sha = self.request.GET.get("version") + sha = self.request.GET["version"] except KeyError: if self.sha is not None: sha = self.sha @@ -220,40 +226,16 @@ def get_context_data(self, **kwargs): sha = content.sha_draft # check that if we ask for beta, we also ask for the sha version - is_beta = (sha == content.sha_beta and content.in_beta()) - # check that if we ask for public version, we also ask for the sha version - is_online = (sha == content.sha_public and content.in_public()) - # Only authors of the tutorial and staff can view tutorial in offline. + is_beta = content.is_beta(sha) - if self.request.user not in content.authors.all() and not is_beta and not is_online: + if self.request.user not in content.authors.all() and not is_beta: # if we are not author of this content or if we did not ask for beta # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Find the good manifest file - - repo = Repo(content.get_repo_path()) - - # Load the tutorial. - - mandata = content.load_json_for_public(sha) - content.load_dic(mandata, sha) - content.load_introduction_and_conclusion(mandata, sha, sha == content.sha_public) - children_tree = {} - - if 'chapter' in mandata: - # compatibility with old "Mini Tuto" - self.compatibility_chapter(content, repo, sha, mandata["chapter"]) - children_tree = mandata['chapter'] - elif 'parts' in mandata: - # compatibility with old "big tuto". - parts = mandata["parts"] - cpt_p = 1 - for part in parts: - self.compatibility_parts(content, repo, sha, part, cpt_p) - cpt_p += 1 - children_tree = parts + # load versioned file + versioned_tutorial = content.load_version(sha) # check whether this tuto support js fiddle if content.js_support: @@ -261,8 +243,7 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = mandata # TODO : change to "content" - context["children"] = children_tree + context["tutorial"] = versioned_tutorial context["version"] = sha self.get_forms(context, content) diff --git a/zds/urls.py b/zds/urls.py index dd33fa9b80..0ffbb375b6 100644 --- a/zds/urls.py +++ b/zds/urls.py @@ -75,6 +75,7 @@ def location(self, article): urlpatterns = patterns('', url(r'^tutoriels/', include('zds.tutorial.urls')), url(r'^articles/', include('zds.article.urls')), + url(r'^contenu/', include('zds.tutorialv2.urls')), url(r'^forums/', include('zds.forum.urls')), url(r'^mp/', include('zds.mp.urls')), url(r'^membres/', include('zds.member.urls')), From 91e8dafea643582ea24249ae5a53905157d08d7e Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sun, 25 Jan 2015 09:02:01 +0100 Subject: [PATCH 107/887] Correction d'une erreur de manipulation sur `tutorial/view.py` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_repo_path()` → `get_path()` --- zds/tutorial/views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index 2d95cd958c..92f6c7355e 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -2340,9 +2340,9 @@ def upload_images(images, tutorial): # download images zfile = zipfile.ZipFile(images, "a") - os.makedirs(os.path.abspath(os.path.join(tutorial.get_repo_path(), "images"))) + os.makedirs(os.path.abspath(os.path.join(tutorial.get_path(), "images"))) for i in zfile.namelist(): - ph_temp = os.path.abspath(os.path.join(tutorial.get_repo_path(), i)) + ph_temp = os.path.abspath(os.path.join(tutorial.get_path(), i)) try: data = zfile.read(i) fp = open(ph_temp, "wb") @@ -2733,7 +2733,7 @@ def maj_repo_part( msg=None, ): - repo = Repo(part.tutorial.get_repo_path()) + repo = Repo(part.tutorial.get_path()) index = repo.index # update the tutorial last edit date part.tutorial.update = datetime.now() @@ -2753,19 +2753,19 @@ def maj_repo_part( msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ .strip() index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(part.tutorial.get_path(), "manifest.json") part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) if introduction is not None: intro = open(os.path.join(new_slug_path, "introduction.md"), "w") intro.write(smart_str(introduction).strip()) intro.close() - index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) + index.add([os.path.join(part.get_path(relative=True), "introduction.md")]) if conclusion is not None: conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") conclu.write(smart_str(conclusion).strip()) conclu.close() - index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" + index.add([os.path.join(part.get_path(relative=True), "conclusion.md" )]) aut_user = str(request.user.pk) aut_email = request.user.email @@ -2838,10 +2838,10 @@ def maj_repo_chapter( # update manifest if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chapter.tutorial.get_path(), "manifest.json") chapter.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chapter.part.tutorial.get_repo_path(), + man_path = os.path.join(chapter.part.tutorial.get_path(), "manifest.json") chapter.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) @@ -2908,14 +2908,14 @@ def maj_repo_extract( ext = open(new_slug_path, "w") ext.write(smart_str(text).strip()) ext.close() - index.add([extract.get_repo_path(relative=True)]) + index.add([extract.get_path(relative=True)]) # update manifest if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chap.tutorial.get_path(), "manifest.json") chap.tutorial.dump_json(path=man_path) else: - man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") + man_path = os.path.join(chap.part.tutorial.get_path(), "manifest.json") chap.part.tutorial.dump_json(path=man_path) index.add(["manifest.json"]) From 60ead75771acd86a9c42a6772ff47c67978893e4 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Thu, 29 Jan 2015 15:14:41 +0100 Subject: [PATCH 108/887] Implemente une partie des specifications + Reecriture d'une partie des modeles (entre autre sur la partie de gestion des slugs) + Creation de nouveaux tests et reecriture des anciens Permet de creer, supprimer et consulter un contenu + Creation des vues correspondantes + Creation de certains tests --- requirements.txt | 2 + templates/tutorialv2/create/content.html | 30 ++ templates/tutorialv2/edit/content.html | 40 ++ .../includes/content_item.part.html | 31 ++ .../includes/tags_authors.part.html | 44 ++ templates/tutorialv2/index.html | 56 ++ templates/tutorialv2/view.html | 486 ------------------ templates/tutorialv2/view/content.html | 206 ++++++++ zds/settings.py | 6 +- zds/tutorialv2/forms.py | 14 +- zds/tutorialv2/models.py | 308 +++++++---- zds/tutorialv2/tests/tests_models.py | 41 +- zds/tutorialv2/tests/tests_views.py | 126 +++++ zds/tutorialv2/url/__init__.py | 1 + zds/tutorialv2/url/url_contents.py | 22 + zds/tutorialv2/urls.py | 12 +- zds/tutorialv2/views.py | 414 +++++++++++---- zds/urls.py | 2 +- 18 files changed, 1123 insertions(+), 718 deletions(-) create mode 100644 templates/tutorialv2/create/content.html create mode 100644 templates/tutorialv2/edit/content.html create mode 100644 templates/tutorialv2/includes/content_item.part.html create mode 100644 templates/tutorialv2/includes/tags_authors.part.html create mode 100644 templates/tutorialv2/index.html delete mode 100644 templates/tutorialv2/view.html create mode 100644 templates/tutorialv2/view/content.html create mode 100644 zds/tutorialv2/url/__init__.py create mode 100644 zds/tutorialv2/url/url_contents.py diff --git a/requirements.txt b/requirements.txt index aaaec71ffc..eccee1c5d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,5 @@ django-oauth-toolkit==0.7.2 drf-extensions==0.2.6 django-rest-swagger==0.2.9 django-cors-headers==1.0.0 + +django-uuslug==1.0.3 diff --git a/templates/tutorialv2/create/content.html b/templates/tutorialv2/create/content.html new file mode 100644 index 0000000000..0ab6c81cdb --- /dev/null +++ b/templates/tutorialv2/create/content.html @@ -0,0 +1,30 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Nouveau contenu" %} +{% endblock %} + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + +{% block breadcrumb %} +
  • {% trans "Nouveau contenu" %}
  • +{% endblock %} + + + +{% block headline %} +

    + {% trans "Nouveau contenu" %} +

    +{% endblock %} + + + +{% block content %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html new file mode 100644 index 0000000000..9d4a2cc26c --- /dev/null +++ b/templates/tutorialv2/edit/content.html @@ -0,0 +1,40 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load i18n %} + +{% block title %} + {% trans "Éditer le contenu" %} +{% endblock %} + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + +{% block breadcrumb %} +
  • {{ content.title }}
  • +
  • {% trans "Éditer le contenu" %}
  • +{% endblock %} + +{% block headline %} +

    + {% if content.image %} + + {% endif %} + {% trans "Éditer" %} : {{ content.title }} +

    +{% endblock %} + +{% block headline_sub %} + {{ content.description }} +{% endblock %} + + +{% block content %} + {% if new_version %} +

    + {% trans "Une nouvelle version a été postée avant que vous ne validiez" %}. +

    + {% endif %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/includes/content_item.part.html b/templates/tutorialv2/includes/content_item.part.html new file mode 100644 index 0000000000..f5b98891e1 --- /dev/null +++ b/templates/tutorialv2/includes/content_item.part.html @@ -0,0 +1,31 @@ +{% load thumbnail %} +{% load date %} +{% load i18n %} + + \ No newline at end of file diff --git a/templates/tutorialv2/includes/tags_authors.part.html b/templates/tutorialv2/includes/tags_authors.part.html new file mode 100644 index 0000000000..5fb924fd4e --- /dev/null +++ b/templates/tutorialv2/includes/tags_authors.part.html @@ -0,0 +1,44 @@ +{% load date %} +{% load i18n %} + + + +{% if content.subcategory.all|length > 0 %} + +{% endif %} + + + +{% if content.update %} + + {% trans "Dernière mise à jour" %} : + + +{% endif %} + +{% include "misc/zen_button.part.html" %} + +
    + {% trans "Auteur" %}{{ content.authors.all|pluralize }} : +
      + {% for member in content.authors.all %} +
    • + {% include "misc/member_item.part.html" with avatar=True author=True %} +
    • + {% endfor %} + + {% if add_author == True %} +
    • + + {% trans "Ajouter un auteur" %} + +
    • + {% endif %} +
    +
    diff --git a/templates/tutorialv2/index.html b/templates/tutorialv2/index.html new file mode 100644 index 0000000000..ce4a752428 --- /dev/null +++ b/templates/tutorialv2/index.html @@ -0,0 +1,56 @@ +{% extends "tutorialv2/base.html" %} +{% load date %} +{% load i18n %} + + +{% block title %} + {% trans "Mes contenus" %} +{% endblock %} + + + +{% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • +{% endblock %} + + + +{% block content_out %} +
    +

    + {% block headline %} + {% trans "Mes contenus" %} + {% endblock %} +

    + + {% if tutorials %} +

    {% trans "Mes tutoriels" %}

    +
    + {% for tutorial in tutorials %} + {% include "tutorialv2/includes/content_item.part.html" with content=tutorial %} + {% endfor %} +
    + {% endif %} + + {% if articles %} +

    {% trans "Mes articles" %}

    +
    + {% for article in articles %} + {% include "tutorialv2/includes/content_item.part.html" with content=article %} + {% endfor %} +
    + {% endif %} + + {% if not articles and not tutorials %} +

    {% trans "Vous n'avez encore créé aucun contenu" %}

    + {% endif %} +
    +{% endblock %} + + + +{% block sidebar_new %} + + {% trans "Nouveau contenu" %} + +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/view.html b/templates/tutorialv2/view.html deleted file mode 100644 index c53cf43273..0000000000 --- a/templates/tutorialv2/view.html +++ /dev/null @@ -1,486 +0,0 @@ -{% extends "tutorialv2/base.html" %} -{% load emarkdown %} -{% load repo_reader %} -{% load crispy_forms_tags %} -{% load thumbnail %} -{% load roman %} -{% load i18n %} - - -{% block title %} - {{ tutorial.title }} -{% endblock %} - - - -{% block breadcrumb %} -
  • {{ tutorial.title }}
  • -{% endblock %} - - - -{% block headline %} - {% if tutorial.licence %} -

    - {{ tutorial.licence }} -

    - {% endif %} - -

    - {% if tutorial.image %} - - {% endif %} - {{ tutorial.title }} -

    - - {% if tutorial.description %} -

    - {{ tutorial.description }} -

    - {% endif %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial add_author=True %} - {% else %} - {% include 'tutorial/includes/tags_authors.part.html' with tutorial=tutorial %} - {% endif %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if tutorial.in_validation %} - {% if validation.version == version %} - {% if validation.is_pending %} -

    - {% trans "Ce tutoriel est en attente d'un validateur" %} -

    - {% elif validation.is_pending_valid %} -

    - {% trans "Le tutoriel est en cours de validation par" %} - {% include "misc/member_item.part.html" with member=validation.validator %} -

    - {% endif %} - {% if validation.comment_authors %} -
    -

    - {% trans "Le message suivant a été laissé à destination des validateurs" %} : -

    - -
    - {{ validation.comment_authors|emarkdown }} -
    -
    - {% endif %} - {% else %} - {% if validation.is_pending %} -

    - - {% trans "Une autre version de ce tutoriel" %} - {% trans "est en attente d'un validateur" %} -

    - {% elif validation.is_pending_valid %} -

    - - {% trans "Une autre version de ce tutoriel" %} - {% trans "est en cours de validation par" %} - {% include "misc/member_item.part.html" with member=validation.validator %} -

    - {% endif %} - {% endif %} - {% endif %} - {% endif %} - - {% if tutorial.is_beta %} -
    -
    - {% blocktrans %} - Cette version du tutoriel est en "BÊTA" ! - {% endblocktrans %} -
    -
    - {% endif %} -{% endblock %} - - -{% block content %} - {% if tutorial.get_introduction and tutorial.get_introduction != "None" %} - {{ tutorial.get_introduction|emarkdown:is_js }} - {% elif not tutorial.is_beta %} -

    - {% trans "Il n'y a pas d'introduction" %}. -

    - {% endif %} - - {% if tutorial.is_mini %} - {# Small tutorial #} - - {% include "tutorial/includes/chapter.part.html" with authors=tutorial.authors.all %} - {% else %} - {# Large tutorial #} - -
    - - {% for part in parts %} -

    - - {{ part.position_in_tutorial|roman }} - {{ part.title }} - -

    - {% include "tutorial/includes/part.part.html" %} - {% empty %} -

    - {% trans "Il n'y a actuellement aucune partie dans ce tutoriel" %}. -

    - {% endfor %} - -
    - - {% endif %} - - {% if tutorial.get_conclusion and tutorial.get_conclusion != "None" %} - {{ tutorial.get_conclusion|emarkdown:is_js }} - {% elif not tutorial.is_beta %} -

    - {% trans "Il n'y a pas de conclusion" %}. -

    - {% endif %} -{% endblock %} - - - -{% block sidebar_new %} - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if tutorial.sha_draft = version %} - {% if not tutorial.is_mini %} - - {% trans "Ajouter une partie" %} - - {% else %} - - {% trans "Ajouter un extrait" %} - - {% endif %} - - - {% trans "Éditer" %} - - {% else %} - - {% trans "Version brouillon" %} - - {% endif %} - {% endif %} -{% endblock %} - - - -{% block sidebar_actions %} - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} -
  • - - {% trans "Ajouter un auteur" %} - - -
  • -
  • - - {% trans "Gérer les auteurs" %} - - -
  • - - {% if tutorial.sha_public %} -
  • - - {% blocktrans %} - Voir la version en ligne - {% endblocktrans %} - -
  • - {% endif %} - - {% if not tutorial.in_beta %} -
  • - - {% trans "Mettre cette version en bêta" %} - - -
  • - {% else %} - {% if not tutorial.is_beta %} -
  • - - {% blocktrans %} - Voir la version en bêta - {% endblocktrans %} - -
  • -
  • - - {% trans "Mettre à jour la bêta avec cette version" %} - - -
  • - {% else %} -
  • - - {% trans "Cette version est déjà en bêta" %} - -
  • - {% endif %} -
  • - - {% trans "Désactiver la bêta" %} - - -
  • - {% endif %} - -
  • - - {% trans "Historique des versions" %} - -
  • - - {% if not tutorial.in_validation %} -
  • - - {% trans "Demander la validation" %} - -
  • - {% else %} - {% if not tutorial.is_validation %} -
  • - - {% trans "Mettre à jour la version en validation" %} - -
  • - {% endif %} -
  • - {% trans "En attente de validation" %} -
  • - {% endif %} - - {% endif %} -{% endblock %} - - - -{% block sidebar_blocks %} - {% if perms.tutorial.change_tutorial %} - - {% endif %} - - {% include "tutorial/includes/summary.part.html" %} - - {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - - {% endif %} -{% endblock %} diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html new file mode 100644 index 0000000000..66daa60f08 --- /dev/null +++ b/templates/tutorialv2/view/content.html @@ -0,0 +1,206 @@ +{% extends "tutorialv2/base.html" %} +{% load emarkdown %} +{% load repo_reader %} +{% load crispy_forms_tags %} +{% load thumbnail %} +{% load roman %} +{% load i18n %} + + +{% block title %} + {{ content.title }} +{% endblock %} + + +{% if user in content.authors.all or perms.content.change_content %} + {% block breadcrumb_base %} +
  • {% trans "Mes contenus" %}
  • + {% endblock %} +{% endif %} + + +{% block breadcrumb %} +
  • {{ content.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if content.licence %} +

    + {{ content.licence }} +

    + {% endif %} + +

    + {% if content.image %} + + {% endif %} + {{ content.title }} +

    + + {% if content.description %} +

    + {{ content.description }} +

    + {% endif %} + + {% if user in content.authors.all or perms.content.change_content %} + {% include 'tutorialv2/includes/tags_authors.part.html' with content=content add_author=True %} + {% else %} + {% include 'tutorialv2/includes/tags_authors.part.html' with content=content %} + {% endif %} + + {% if user in content.authors.all or perms.content.change_content %} + {% if content.in_validation %} + {% if validation.version == version %} + {% if validation.is_pending %} +

    + {% trans "Ce tutoriel est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + {% trans "Le tutoriel est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% if validation.comment_authors %} +
    +

    + {% trans "Le message suivant a été laissé à destination des validateurs" %} : +

    + +
    + {{ validation.comment_authors|emarkdown }} +
    +
    + {% endif %} + {% else %} + {% if validation.is_pending %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en attente d'un validateur" %} +

    + {% elif validation.is_pending_valid %} +

    + + {% trans "Une autre version de ce tutoriel" %} + {% trans "est en cours de validation par" %} + {% include "misc/member_item.part.html" with member=validation.validator %} +

    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if content.is_beta %} +
    +
    + {% blocktrans %} + Cette version est en "BÊTA" ! + {% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + +{% block content %} + {% if content.get_introduction and content.get_introduction != "None" %} + {{ content.get_introduction|emarkdown:is_js }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + + +
    + + {% for child in content.childreen %} + {# include stuff #} + {% empty %} +

    + {% trans "Ce contenu est actuelement vide" %}. +

    + {% endfor %} + +
    + + {% if content.get_conclusion and content.get_conclusion != "None" %} + {{ content.get_conclusion|emarkdown:is_js }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in content.authors.all or perms.content.change_content %} + {% if content.sha_draft == version %} + + {% trans "Éditer" %} + + + {% else %} + + {% trans "Version brouillon" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in content.authors.all or perms.content.change_content %} + {# other action (valid, beta, ...) #} + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {% if perms.content.change_content %} + {# more actions ?!? #} + {% endif %} + + {# include "content/includes/summary.part.html" #} + + {% if user in content.authors.all or perms.content.change_content %} + + {% endif %} +{% endblock %} diff --git a/zds/settings.py b/zds/settings.py index 90f40d7651..2a5a440d45 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -453,10 +453,14 @@ 'default_license_pk': 7, 'home_number': 5, 'helps_per_page': 20, - 'max_tree_depth': 3, 'content_per_page': 50, 'feed_length': 5 }, + 'content': { + 'repo_private_path': os.path.join(SITE_ROOT, 'contents-private'), + 'repo_public_path': os.path.join(SITE_ROOT, 'contents-public'), + 'max_tree_depth': 3 + }, 'forum': { 'posts_per_page': 21, 'topics_per_page': 21, diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index e2001098e8..7bd5236838 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -10,14 +10,14 @@ from zds.utils.forms import CommonLayoutModalText, CommonLayoutEditor, CommonLayoutVersionEditor from zds.utils.models import SubCategory, Licence -from zds.tutorial.models import Tutorial, TYPE_CHOICES, HelpWriting +from zds.tutorialv2.models import PublishableContent, TYPE_CHOICES, HelpWriting from django.utils.translation import ugettext_lazy as _ class FormWithTitle(forms.Form): title = forms.CharField( label=_(u'Titre'), - max_length=Tutorial._meta.get_field('title').max_length, + max_length=PublishableContent._meta.get_field('title').max_length, widget=forms.TextInput( attrs={ 'required': 'required', @@ -39,11 +39,11 @@ def clean(self): return cleaned_data -class TutorialForm(FormWithTitle): +class ContentForm(FormWithTitle): description = forms.CharField( label=_(u'Description'), - max_length=Tutorial._meta.get_field('description').max_length, + max_length=PublishableContent._meta.get_field('description').max_length, required=False, ) @@ -123,7 +123,7 @@ class TutorialForm(FormWithTitle): ) def __init__(self, *args, **kwargs): - super(TutorialForm, self).__init__(*args, **kwargs) + super(ContentForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' @@ -414,7 +414,7 @@ class ImportArchiveForm(forms.Form): tutorial = forms.ModelChoiceField( label=_(u"Tutoriel vers lequel vous souhaitez importer votre archive"), - queryset=Tutorial.objects.none(), + queryset=PublishableContent.objects.none(), required=True ) @@ -423,7 +423,7 @@ def __init__(self, user, *args, **kwargs): self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' - self.fields['tutorial'].queryset = Tutorial.objects.filter(authors__in=[user]) + self.fields['tutorial'].queryset = PublishableContent.objects.filter(authors__in=[user]) self.helper.layout = Layout( Field('file'), diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cd6af0f5ae..63cf578acf 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -28,6 +28,8 @@ from zds.settings import ZDS_APP from zds.utils.models import HelpWriting +from uuslug import uuslug + TYPE_CHOICES = ( ('TUTORIAL', 'Tutoriel'), @@ -66,6 +68,7 @@ class Container: position_in_parent = 1 children = [] children_dict = {} + slug_pool = {} # TODO: thumbnails ? @@ -78,6 +81,8 @@ def __init__(self, title, slug='', parent=None, position_in_parent=1): self.children = [] # even if you want, do NOT remove this line self.children_dict = {} + self.slug_pool = {'introduction': 1, 'conclusion': 1} # forbidden slugs + def __unicode__(self): return u''.format(self.title) @@ -130,18 +135,50 @@ def top_container(self): current = current.parent return current + def get_unique_slug(self, title): + """ + Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a + "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. + :param title: title from which the slug is generated (with `slugify()`) + :return: the unique slug + """ + base = slugify(title) + try: + n = self.slug_pool[base] + except KeyError: + new_slug = base + self.slug_pool[base] = 0 + else: + new_slug = base + '-' + str(n) + self.slug_pool[base] += 1 + self.slug_pool[new_slug] = 1 + return new_slug + + def add_slug_to_pool(self, slug): + """ + Add a slug to the slug pool to be taken into account when generate a unique slug + :param slug: the slug to add + """ + try: + self.slug_pool[slug] # test access + except KeyError: + self.slug_pool[slug] = 1 + else: + raise Exception('slug "{}" already in the slug pool !'.format(slug)) + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. + Note: this function will also raise an Exception if article, because it cannot contain child container :param container: the new container :param generate_slug: if `True`, ask the top container an unique slug for this object """ if not self.has_extracts(): - if self.get_tree_depth() < ZDS_APP['tutorial']['max_tree_depth']: + if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth'] and self.top_container().type != 'ARTICLE': if generate_slug: - container.slug = self.top_container().get_unique_slug(container.title) + container.slug = self.get_unique_slug(container.title) else: - self.top_container().add_slug_to_pool(container.slug) + self.add_slug_to_pool(container.slug) container.parent = self container.position_in_parent = self.get_last_child_position() + 1 self.children.append(container) @@ -149,8 +186,7 @@ def add_container(self, container, generate_slug=False): else: raise InvalidOperationError("Cannot add another level to this content") else: - raise InvalidOperationError("Can't add a container if this container contains extracts.") - # TODO: limitation if article ? + raise InvalidOperationError("Can't add a container if this container already contains extracts.") def add_extract(self, extract, generate_slug=False): """ @@ -160,18 +196,20 @@ def add_extract(self, extract, generate_slug=False): """ if not self.has_sub_containers(): if generate_slug: - extract.slug = self.top_container().get_unique_slug(extract.title) + extract.slug = self.get_unique_slug(extract.title) else: - self.top_container().add_slug_to_pool(extract.slug) + self.add_slug_to_pool(extract.slug) extract.container = self extract.position_in_parent = self.get_last_child_position() + 1 self.children.append(extract) self.children_dict[extract.slug] = extract + else: + raise InvalidOperationError("Can't add an extract if this container already contains containers.") def update_children(self): """ Update the path for introduction and conclusion for the container and all its children. If the children is an - extract, update the path to the text instead. This function is useful when `self.pk` or `self.title` has + extract, update the path to the text instead. This function is useful when `self.slug` has changed. Note : this function does not account for a different arrangement of the files. """ @@ -214,18 +252,6 @@ def get_introduction(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.introduction) - def get_introduction_online(self): - """ - Get introduction content of the public version - :return: the introduction - """ - path = self.top_container().get_prod_path() + self.introduction + '.html' - if os.path.exists(path): - intro = open(path) - intro_content = intro.read() - intro.close() - return intro_content.decode('utf-8') - def get_conclusion(self): """ :return: the conclusion from the file in `self.conclusion` @@ -234,17 +260,48 @@ def get_conclusion(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.conclusion) - def get_conclusion_online(self): + def repo_update(self, title, introduction, conclusion, commit_message=''): """ - Get conclusion content of the public version - :return: the conclusion + Update the container information and commit them into the repository + :param title: the new title + :param introduction: the new introduction text + :param conclusion: the new conclusion text + :param commit_message: commit message that will be used instead of the default one + :return : commit sha """ - path = self.top_container().get_prod_path() + self.conclusion + '.html' - if os.path.exists(path): - conclusion = open(path) - conclusion_content = conclusion.read() - conclusion.close() - return conclusion_content.decode('utf-8') + + # update title + if title != self.title: + self.title = title + if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed + self.slug = self.top_container().get_unique_slug(title) + self.update_children() + # TODO : and move() !!! + + # update introduction and conclusion + if self.introduction is None: + self.introduction = self.get_path(relative=True) + 'introduction.md' + if self.conclusion is None: + self.conclusion = self.get_path(relative=True) + 'conclusion.md' + + path = self.top_container().get_path() + f = open(os.path.join(path, self.introduction), "w") + f.write(introduction.encode('utf-8')) + f.close() + f = open(os.path.join(path, self.conclusion), "w") + f.write(conclusion.encode('utf-8')) + f.close() + + self.top_container().dump_json() + + repo = self.top_container().repository + repo.index.add(['manifest.json', self.introduction, self.conclusion]) + + if commit_message == '': + commit_message = u'Mise à jour de « ' + self.title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha # TODO: # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) @@ -311,14 +368,6 @@ def get_path(self, relative=False): """ return os.path.join(self.container.get_path(relative=relative), self.slug) + '.md' - def get_prod_path(self): - """ - Get the physical path to the public version of a specific version of the extract. - :return: physical path - """ - return os.path.join(self.container.get_prod_path(), self.slug) + '.md.html' - # TODO: should this function exists ? (there will be no public version of a text, all in parent container) - def get_text(self): """ :return: versioned text @@ -360,6 +409,7 @@ class VersionedContent(Container): slug_pool = {} # Metadata from DB : + pk = 0 sha_draft = None sha_beta = None sha_public = None @@ -390,8 +440,6 @@ def __init__(self, current_version, _type, title, slug): self.type = _type self.repository = Repo(self.get_path()) - self.slug_pool = {'introduction': 1, 'conclusion': 1, slug: 1} # forbidden slugs - def __unicode__(self): return self.title @@ -399,7 +447,7 @@ def get_absolute_url(self): """ :return: the url to access the tutorial when offline """ - return reverse('view-tutorial-url', args=[self.slug]) + return reverse('content:view', args=[self.pk, self.slug]) def get_absolute_url_online(self): """ @@ -422,37 +470,6 @@ def get_edit_url(self): """ return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) - def get_unique_slug(self, title): - """ - Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a - "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. - :param title: title from which the slug is generated (with `slugify()`) - :return: the unique slug - """ - base = slugify(title) - try: - n = self.slug_pool[base] - except KeyError: - new_slug = base - self.slug_pool[base] = 0 - else: - new_slug = base + '-' + str(n) - self.slug_pool[base] += 1 - self.slug_pool[new_slug] = 1 - return new_slug - - def add_slug_to_pool(self, slug): - """ - Add a slug to the slug pool to be taken into account when generate a unique slug - :param slug: the slug to add - """ - try: - self.slug_pool[slug] # test access - except KeyError: - self.slug_pool[slug] = 1 - else: - raise Exception('slug "{}" already in the slug pool !'.format(slug)) - def get_path(self, relative=False): """ Get the physical path to the draft version of the Content. @@ -462,15 +479,14 @@ def get_path(self, relative=False): if relative: return '' else: - # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) def get_prod_path(self): """ Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_public_path'], self.slug) + return os.path.join(settings.ZDS_APP['contents']['repo_public_path'], self.slug) def get_json(self): """ @@ -494,6 +510,28 @@ def dump_json(self, path=None): json_data.write(self.get_json().encode('utf-8')) json_data.close() + def repo_update_top_container(self, title, slug, introduction, conclusion, commit_message=''): + """ + Update the top container information and commit them into the repository. + Note that this is slightly different from the `repo_update()` function, because slug is generated using DB + :param title: the new title + :param slug: the new slug, according to title (choose using DB!!) + :param introduction: the new introduction text + :param conclusion: the new conclusion text + :param commit_message: commit message that will be used instead of the default one + :return : commit sha + """ + + if slug != self.slug: + # move repository + old_path = self.get_path() + self.slug = slug + new_path = self.get_path() + shutil.move(old_path, new_path) + self.repository = Repo(new_path) + + return self.repo_update(title, introduction, conclusion, commit_message) + def fill_containers_from_json(json_sub, parent): """ @@ -501,27 +539,101 @@ def fill_containers_from_json(json_sub, parent): :param json_sub: dictionary from "manifest.json" :param parent: the container to fill """ - # TODO should be static function of `VersionedContent` - # TODO should implement fallbacks + # TODO should be static function of `VersionedContent` ?!? if 'children' in json_sub: for child in json_sub['children']: if child['object'] == 'container': - new_container = Container(child['title'], child['slug']) + slug = '' + try: + slug = child['slug'] + except KeyError: + pass + new_container = Container(child['title'], slug) if 'introduction' in child: new_container.introduction = child['introduction'] if 'conclusion' in child: new_container.conclusion = child['conclusion'] - parent.add_container(new_container) + parent.add_container(new_container, generate_slug=(slug != '')) if 'children' in child: fill_containers_from_json(child, new_container) elif child['object'] == 'extract': - new_extract = Extract(child['title'], child['slug']) + slug = '' + try: + slug = child['slug'] + except KeyError: + pass + new_extract = Extract(child['title'], slug) new_extract.text = child['text'] - parent.add_extract(new_extract) + parent.add_extract(new_extract, generate_slug=(slug != '')) else: raise Exception('Unknown object type'+child['object']) +def init_new_repo(db_object, introduction_text, conclusion_text, commit_message=''): + """ + Create a new repository in `settings.ZDS_APP['contents']['private_repo']` to store the files for a new content. + Note that `db_object.sha_draft` will be set to the good value + :param db_object: `PublishableContent` (WARNING: should have a valid `slug`, so previously saved) + :param introduction_text: introduction from form + :param conclusion_text: conclusion from form + :param commit_message : set a commit message instead of the default one + :return: `VersionedContent` object + """ + # TODO: should be a static function of an object (I don't know which one yet) + + # create directory + path = db_object.get_repo_path() + print(path) + if not os.path.isdir(path): + os.makedirs(path, mode=0o777) + + introduction = 'introduction.md' + conclusion = 'conclusion.md' + versioned_content = VersionedContent(None, + db_object.type, + db_object.title, + db_object.slug) + + # fill some information that are missing : + versioned_content.licence = db_object.licence + versioned_content.description = db_object.description + versioned_content.introduction = introduction + versioned_content.conclusion = conclusion + + # init repo: + Repo.init(path, bare=False) + repo = Repo(path) + + # fill intro/conclusion: + f = open(os.path.join(path, introduction), "w") + f.write(introduction_text.encode('utf-8')) + f.close() + f = open(os.path.join(path, conclusion), "w") + f.write(conclusion_text.encode('utf-8')) + f.close() + + versioned_content.dump_json() + + # commit change: + if commit_message == '': + commit_message = u'Création du contenu' + repo.index.add(['manifest.json', introduction, conclusion]) + cm = repo.index.commit(commit_message) + + # update sha: + db_object.sha_draft = cm.hexsha + db_object.sha_beta = None + db_object.sha_public = None + db_object.sha_validation = None + + db_object.save() + + versioned_content.current_version = cm.hexsha + versioned_content.repository = repo + + return versioned_content + + class PublishableContent(models.Model): """ A tutorial whatever its size or an article. @@ -540,7 +652,7 @@ class Meta: verbose_name_plural = 'Contenus' title = models.CharField('Titre', max_length=80) - slug = models.SlugField(max_length=80) + slug = models.CharField('Slug', max_length=80) description = models.CharField('Description', max_length=200) source = models.CharField('Source', max_length=200) authors = models.ManyToManyField(User, verbose_name='Auteurs', db_index=True) @@ -598,6 +710,13 @@ class Meta: def __unicode__(self): return self.title + def save(self, *args, **kwargs): + """ + Rewrite the `save()` function to handle slug uniqueness + """ + self.slug = uuslug(self.title, instance=self, max_length=80) + super(PublishableContent, self).save(*args, **kwargs) + def get_repo_path(self, relative=False): """ Get the path to the tutorial repository @@ -608,7 +727,7 @@ def get_repo_path(self, relative=False): return '' else: # get the full path (with tutorial/article before it) - return os.path.join(settings.ZDS_APP[self.type.lower()]['repo_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) def in_beta(self): """ @@ -697,20 +816,27 @@ def load_version(self, sha=None, public=False): # create and fill the container versioned = VersionedContent(sha, self.type, json['title'], json['slug']) if 'version' in json and json['version'] == 2: + # fill metadata : if 'description' in json: versioned.description = json['description'] - if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': - versioned.type = json['type'] + + if 'type' in json: + if json['type'] == 'ARTICLE' or json['type'] == 'TUTORIAL': + versioned.type = json['type'] else: versioned.type = self.type + if 'licence' in json: versioned.licence = Licence.objects.filter(code=json['licence']).first() - # TODO must default licence be enforced here ? + else: + versioned.licence = Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + if 'introduction' in json: versioned.introduction = json['introduction'] if 'conclusion' in json: versioned.conclusion = json['conclusion'] + # then, fill container with children fill_containers_from_json(json, versioned) self.insert_data_in_versioned(versioned) @@ -728,6 +854,7 @@ def insert_data_in_versioned(self, versioned): """ fns = [ + 'pk', 'have_markdown', 'have_html', 'have_pdf', 'have_epub', 'in_beta', 'in_validation', 'in_public', 'authors', 'subcategory', 'image', 'creation_date', 'pubdate', 'update_date', 'source', 'sha_draft', 'sha_beta', 'sha_validation', 'sha_public' @@ -742,12 +869,6 @@ def insert_data_in_versioned(self, versioned): versioned.is_validation = self.is_validation(versioned.current_version) versioned.is_public = self.is_public(versioned.current_version) - def save(self, *args, **kwargs): - self.slug = slugify(self.title) - # TODO ensure unique slug here !! - - super(PublishableContent, self).save(*args, **kwargs) - def get_note_count(self): """ :return : umber of notes in the tutorial. @@ -859,18 +980,17 @@ def have_epub(self): """ return os.path.isfile(os.path.join(self.get_prod_path(), self.slug + ".epub")) - def delete_entity_and_tree(self): + def repo_delete(self): """ Delete the entities and their filesystem counterparts """ shutil.rmtree(self.get_repo_path(), False) - Validation.objects.filter(tutorial=self).delete() + Validation.objects.filter(content=self).delete() if self.gallery is not None: self.gallery.delete() if self.in_public(): shutil.rmtree(self.get_prod_path()) - self.delete() class ContentReaction(Comment): diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 396e44e65b..fe13d94dc2 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -1,7 +1,5 @@ # coding: utf-8 -# NOTE : this file is only there for tests purpose, it will be deleted in final version - import os import shutil @@ -10,16 +8,15 @@ from django.test.utils import override_settings from zds.settings import SITE_ROOT -from zds.member.factories import ProfileFactory +from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory from zds.gallery.factories import GalleryFactory # from zds.tutorialv2.models import Container, Extract, VersionedContent overrided_zds_app = settings.ZDS_APP -overrided_zds_app['tutorial']['repo_path'] = os.path.join(SITE_ROOT, 'tutoriels-private-test') -overrided_zds_app['tutorial']['repo__public_path'] = os.path.join(SITE_ROOT, 'tutoriels-public-test') -overrided_zds_app['article']['repo_path'] = os.path.join(SITE_ROOT, 'article-data-test') +overrided_zds_app['content']['repo_private_path'] = os.path.join(SITE_ROOT, 'contents-private-test') +overrided_zds_app['content']['repo_public_path'] = os.path.join(SITE_ROOT, 'contents-public-test') @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @@ -34,6 +31,8 @@ def setUp(self): self.licence = LicenceFactory() self.user_author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.tuto = PublishableContentFactory(type='TUTORIAL') self.tuto.authors.add(self.user_author) self.tuto.gallery = GalleryFactory() @@ -68,13 +67,7 @@ def test_ensure_unique_slug(self): versioned = self.tuto.load_version() # forbidden slugs : - slug_to_test = ['introduction', # forbidden slug - 'conclusion', # forbidden slug - self.tuto.slug, # forbidden slug - # slug normally already in the slug pool : - self.part1.slug, - self.chapter1.slug, - self.extract1.slug] + slug_to_test = ['introduction', 'conclusion'] for slug in slug_to_test: new_slug = versioned.get_unique_slug(slug) @@ -83,22 +76,22 @@ def test_ensure_unique_slug(self): # then test with "real" containers and extracts : new_chapter_1 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) - # now, slug "aa" is forbidden ! new_chapter_2 = ContainerFactory(title='aa', parent=versioned, db_object=self.tuto) self.assertNotEqual(new_chapter_1.slug, new_chapter_2.slug) + new_extract_1 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) - self.assertNotEqual(new_extract_1.slug, new_chapter_2.slug) - self.assertNotEqual(new_extract_1.slug, new_chapter_1.slug) + self.assertEqual(new_extract_1.slug, new_chapter_1.slug) # different level can have the same slug ! + new_extract_2 = ExtractFactory(title='aa', container=new_chapter_2, db_object=self.tuto) - self.assertNotEqual(new_extract_2.slug, new_extract_1.slug) - self.assertNotEqual(new_extract_2.slug, new_chapter_1.slug) + self.assertEqual(new_extract_2.slug, new_extract_1.slug) # not the same parent, so allowed + + new_extract_3 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) + self.assertNotEqual(new_extract_3.slug, new_extract_1.slug) # same parent, forbidden def tearDown(self): - if os.path.isdir(settings.ZDS_APP['tutorial']['repo_path']): - shutil.rmtree(settings.ZDS_APP['tutorial']['repo_path']) - if os.path.isdir(settings.ZDS_APP['tutorial']['repo_public_path']): - shutil.rmtree(settings.ZDS_APP['tutorial']['repo_public_path']) - if os.path.isdir(settings.ZDS_APP['article']['repo_path']): - shutil.rmtree(settings.ZDS_APP['article']['repo_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_public_path']) if os.path.isdir(settings.MEDIA_ROOT): shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index e69de29bb2..178b9d0368 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -0,0 +1,126 @@ +# coding: utf-8 + +import os +import shutil + +from django.conf import settings +from django.test import TestCase +from django.test.utils import override_settings +from django.core.urlresolvers import reverse + +from zds.settings import SITE_ROOT +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory +from zds.tutorialv2.models import PublishableContent +from zds.gallery.factories import GalleryFactory + +overrided_zds_app = settings.ZDS_APP +overrided_zds_app['content']['repo_private_path'] = os.path.join(SITE_ROOT, 'contents-private-test') +overrided_zds_app['content']['repo_public_path'] = os.path.join(SITE_ROOT, 'contents-public-test') + + +@override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) +@override_settings(ZDS_APP=overrided_zds_app) +class ContentTests(TestCase): + + def setUp(self): + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + self.mas = ProfileFactory().user + settings.ZDS_APP['member']['bot_account'] = self.mas.username + + self.licence = LicenceFactory() + + self.user_author = ProfileFactory().user + self.staff = StaffProfileFactory().user + + self.tuto = PublishableContentFactory(type='TUTORIAL') + self.tuto.authors.add(self.user_author) + self.tuto.gallery = GalleryFactory() + self.tuto.licence = self.licence + self.tuto.save() + + self.tuto_draft = self.tuto.load_version() + self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto) + + self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + + def test_ensure_access(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access for user + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + self.client.logout() + + # check access for public (get 302, login) + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 302) + + # login with staff + self.assertEqual( + self.client.login( + username=self.staff.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access for staff (get 200) + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + def test_deletion(self): + """Ensure deletion behavior""" + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # create a new tutorial + tuto = PublishableContentFactory(type='TUTORIAL') + tuto.authors.add(self.user_author) + tuto.gallery = GalleryFactory() + tuto.licence = self.licence + tuto.save() + + versioned = tuto.load_version() + path = versioned.get_path() + + # delete it + result = self.client.get( + reverse('content:delete', args=[tuto.pk, tuto.slug]), + follow=True) + self.assertEqual(result.status_code, 405) # get method is not allowed for deleting + + result = self.client.post( + reverse('content:delete', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 302) + + self.assertFalse(os.path.isfile(path)) # deletion get right ;) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_public_path']) + if os.path.isdir(settings.MEDIA_ROOT): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/url/__init__.py b/zds/tutorialv2/url/__init__.py new file mode 100644 index 0000000000..a170a28c86 --- /dev/null +++ b/zds/tutorialv2/url/__init__.py @@ -0,0 +1 @@ +__author__ = 'pbeaujea' diff --git a/zds/tutorialv2/url/url_contents.py b/zds/tutorialv2/url/url_contents.py new file mode 100644 index 0000000000..58718bb534 --- /dev/null +++ b/zds/tutorialv2/url/url_contents.py @@ -0,0 +1,22 @@ +# coding: utf-8 + +from django.conf.urls import patterns, url + +from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent + +urlpatterns = patterns('', + url(r'^$', ListContent.as_view(), name='index'), + + # view: + url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(), name='view'), + + # create: + url(r'^nouveau/$', CreateContent.as_view(), name='create'), + + # edit: + url(r'^editer/(?P\d+)/(?P.+)/$', EditContent.as_view(), name='edit'), + + # delete: + url(r'^supprimer/(?P\d+)/(?P.+)/$', DeleteContent.as_view(), name='delete'), + + ) diff --git a/zds/tutorialv2/urls.py b/zds/tutorialv2/urls.py index 956c2d78a2..f53f428b03 100644 --- a/zds/tutorialv2/urls.py +++ b/zds/tutorialv2/urls.py @@ -1,12 +1,12 @@ # coding: utf-8 -from django.conf.urls import patterns, url - -from . import views -from . import feeds -from .views import * +from django.conf.urls import patterns, include, url urlpatterns = patterns('', + url(r'^contenus/', include('zds.tutorialv2.url.url_contents', namespace='content')) +) + +"""urlpatterns = patterns('', # viewing articles url(r'^articles/$', ArticleList.as_view(), name="index-article"), url(r'^articles/flux/rss/$', feeds.LastArticlesFeedRSS(), name='article-feed-rss'), @@ -136,4 +136,4 @@ url(r'^aides/$', TutorialWithHelp.as_view()), ) - +""" diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index b512d0307e..4a9ef3cd02 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -25,6 +25,7 @@ from PIL import Image as ImagePIL from django.conf import settings from django.contrib import messages +from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied @@ -40,9 +41,9 @@ from git import Repo, Actor from lxml import etree -from forms import TutorialForm, PartForm, ChapterForm, EmbdedChapterForm, \ +from forms import ContentForm, PartForm, ChapterForm, EmbdedChapterForm, \ ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm -from models import PublishableContent, Container, Extract, Validation, ContentReaction # , ContentRead +from models import PublishableContent, Container, Extract, Validation, ContentReaction, init_new_repo from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now @@ -60,129 +61,137 @@ from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive from zds.utils.misc import compute_hash, content_has_changed from django.utils.translation import ugettext as _ -from django.views.generic import ListView, DetailView # , UpdateView +from django.views.generic import ListView, DetailView, FormView, DeleteView # until we completely get rid of these, import them : from zds.tutorial.models import Tutorial, Chapter, Part +from zds.tutorial.forms import TutorialForm -class ArticleList(ListView): +class ListContent(ListView): """ - Displays the list of published articles. + Displays the list of offline contents (written by user) """ - context_object_name = 'articles' - paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type = "ARTICLE" - template_name = 'article/index.html' - tag = None + context_object_name = 'contents' + template_name = 'tutorialv2/index.html' + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(ListContent, self).dispatch(*args, **kwargs) def get_queryset(self): """ - Filter the content to obtain the list of only articles. If tag parameter is provided, only articles - which have this category will be listed. + Filter the content to obtain the list of content written by current user :return: list of articles """ - if self.request.GET.get('tag') is not None: - self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) - query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ - .exclude(sha_public='') - if self.tag is not None: - query_set = query_set.filter(subcategory__in=[self.tag]) - return query_set.order_by('-pubdate') + query_set = PublishableContent.objects.all().filter(authors__in=[self.request.user]) + return query_set def get_context_data(self, **kwargs): - context = super(ArticleList, self).get_context_data(**kwargs) - context['tag'] = self.tag - # TODO in database, the information concern the draft, so we have to make stuff here ! + """Separate articles and tutorials""" + context = super(ListContent, self).get_context_data(**kwargs) + context['articles'] = [] + context['tutorials'] = [] + for content in self.get_queryset(): + versioned = content.load_version() + if content.type == 'ARTICLE': + context['articles'].append(versioned) + else: + context['tutorials'].append(versioned) return context -class TutorialList(ArticleList): - """Displays the list of published tutorials.""" +class CreateContent(FormView): + template_name = 'tutorialv2/create/content.html' + model = PublishableContent + form_class = ContentForm + content = None - context_object_name = 'tutorials' - type = "TUTORIAL" - template_name = 'tutorialv2/index.html' + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(CreateContent, self).dispatch(*args, **kwargs) + def form_valid(self, form): + # create the object: + self.content = PublishableContent() + self.content.title = form.cleaned_data['title'] + self.content.description = form.cleaned_data["description"] + self.content.type = form.cleaned_data["type"] + self.content.licence = form.cleaned_data["licence"] -def render_chapter_form(chapter): - if chapter.part: - return ChapterForm({"title": chapter.title, - "introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) - else: + self.content.creation_date = datetime.now() - return \ - EmbdedChapterForm({"introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) + # Creating the gallery + gal = Gallery() + gal.title = form.cleaned_data["title"] + gal.slug = slugify(form.cleaned_data["title"]) + gal.pubdate = datetime.now() + gal.save() + # Attach user to gallery + userg = UserGallery() + userg.gallery = gal + userg.mode = "W" # write mode + userg.user = self.request.user + userg.save() + self.content.gallery = gal -class TutorialWithHelp(TutorialList): - """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta - for more documentation, have a look to ZEP 03 specification (fr)""" - context_object_name = 'tutorials' - template_name = 'tutorialv2/help.html' + # create image: + if "image" in self.request.FILES: + img = Image() + img.physical = self.request.FILES["image"] + img.gallery = gal + img.title = self.request.FILES["image"] + img.slug = slugify(self.request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + self.content.image = img - def get_queryset(self): - """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects\ - .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - try: - type_filter = self.request.GET.get('type') - query_set = query_set.filter(helps_title__in=[type_filter]) - except KeyError: - # if no filter, no need to change - pass - return query_set + self.content.save() - def get_context_data(self, **kwargs): - """Add all HelpWriting objects registered to the context so that the template can use it""" - context = super(TutorialWithHelp, self).get_context_data(**kwargs) - context['helps'] = HelpWriting.objects.all() - return context + # We need to save the tutorial before changing its author list since it's a many-to-many relationship + self.content.authors.add(self.request.user) -# TODO ArticleWithHelp + # Add subcategories on tutorial + for subcat in form.cleaned_data["subcategory"]: + self.content.subcategory.add(subcat) + + # Add helps if needed + for helpwriting in form.cleaned_data["helps"]: + self.content.helps.add(helpwriting) + + self.content.save() + + # create a new repo : + init_new_repo(self.content, + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) + + return super(CreateContent, self).form_valid(form) + + def get_success_url(self): + if self.content: + return reverse('content:view', args=[self.content.pk, self.content.slug]) + else: + return reverse('content:index') class DisplayContent(DetailView): - """Base class that can show any content in any state, by default it shows offline tutorials""" + """Base class that can show any content in any state""" model = PublishableContent - template_name = 'tutorialv2/view.html' - type = "TUTORIAL" + template_name = 'tutorialv2/view/content.html' online = False sha = None - # TODO compatibility should be performed into class `PublishableContent.load_version()` ! - def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): - dictionary["tutorial"] = content - dictionary["path"] = content.get_repo_path() - dictionary["slug"] = slugify(dictionary["title"]) - dictionary["position_in_tutorial"] = cpt_p - - cpt_c = 1 - for chapter in dictionary["chapters"]: - chapter["part"] = dictionary - chapter["slug"] = slugify(chapter["title"]) - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - self.compatibility_chapter(content, repo, sha, chapter) - cpt_c += 1 - - def compatibility_chapter(self, content, repo, sha, dictionary): - """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_repo_path() - dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = get_blob(repo.commit(sha).tree, "introduction.md") - dictionary["conclu"] = get_blob(repo.commit(sha).tree, "conclusion.md") - cpt = 1 - for ext in dictionary["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = content.get_repo_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt += 1 + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(DisplayContent, self).dispatch(*args, **kwargs) def get_forms(self, context, content): """get all the auxiliary forms about validation, js fiddle...""" @@ -206,11 +215,11 @@ def get_forms(self, context, content): context["formReject"] = form_reject, def get_object(self, queryset=None): - return get_object_or_404(PublishableContent, slug=self.kwargs['content_slug']) + # TODO : check slug ? + return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" - # TODO: handling public version ! context = super(DisplayContent, self).get_context_data(**kwargs) content = context['object'] @@ -234,6 +243,10 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied + # check if slug is good: + if self.kwargs['slug'] != content.slug: + raise Http404 + # load versioned file versioned_tutorial = content.load_version(sha) @@ -243,13 +256,208 @@ def get_context_data(self, **kwargs): else: is_js = "" context["is_js"] = is_js - context["tutorial"] = versioned_tutorial + context["content"] = versioned_tutorial context["version"] = sha self.get_forms(context, content) return context +class EditContent(FormView): + template_name = 'tutorialv2/edit/content.html' + model = PublishableContent + form_class = ContentForm + content = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(EditContent, self).dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + # TODO: check slug ? + return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + versioned = context['content'] + initial = super(EditContent, self).get_initial() + + initial['title'] = versioned.title + initial['description'] = versioned.description + initial['type'] = versioned.type + initial['introduction'] = versioned.get_introduction() + initial['conclusion'] = versioned.get_conclusion() + initial['licence'] = versioned.licence + initial['subcategory'] = self.content.subcategory.all() + initial['helps'] = self.content.helps.all() + + return initial + + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditContent, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + + return context + + def form_valid(self, form): + # TODO: tutorial <-> article + context = self.get_context_data() + versioned = context['content'] + + # first, update DB (in order to get a new slug if needed) + self.content.title = form.cleaned_data['title'] + self.content.description = form.cleaned_data["description"] + self.content.licence = form.cleaned_data["licence"] + + self.content.update_date = datetime.now() + + # update gallery and image: + gal = Gallery.objects.filter(pk=self.content.gallery.pk) + gal.update(title=self.content.title) + gal.update(slug=slugify(self.content.title)) + gal.update(update=datetime.now()) + + if "image" in self.request.FILES: + img = Image() + img.physical = self.request.FILES["image"] + img.gallery = self.content.gallery + img.title = self.request.FILES["image"] + img.slug = slugify(self.request.FILES["image"]) + img.pubdate = datetime.now() + img.save() + self.content.image = img + + self.content.save() + + # now, update the versioned information + versioned.description = form.cleaned_data['description'] + versioned.licence = form.cleaned_data['licence'] + + sha = versioned.repo_update_top_container(form.cleaned_data['title'], + self.content.slug, + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) + + # update relationships : + self.content.sha_draft = sha + + self.content.subcategory.clear() + for subcat in form.cleaned_data["subcategory"]: + self.content.subcategory.add(subcat) + + self.content.helps.clear() + for help in form.cleaned_data["helps"]: + self.content.helps.add(help) + + self.content.save() + + return super(EditContent, self).form_valid(form) + + def get_success_url(self): + return reverse('content:view', args=[self.content.pk, self.content.slug]) + + +class DeleteContent(DeleteView): + model = PublishableContent + template_name = None + http_method_names = [u'delete', u'post'] + object = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + """rewrite this method to ensure decoration""" + return super(DeleteContent, self).dispatch(*args, **kwargs) + + def get_queryset(self): + # TODO: check slug ? + qs = super(DeleteContent, self).get_queryset() + return qs.filter(pk=self.kwargs['pk']) + + def delete(self, request, *args, **kwargs): + """rewrite delete() function to ensure repository deletion""" + self.object = self.get_object() + self.object.repo_delete() + + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('content:index') + + +class ArticleList(ListView): + """ + Displays the list of published articles. + """ + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type = "ARTICLE" + template_name = 'article/index.html' + tag = None + + def get_queryset(self): + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') + + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! + return context + + +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" + + context_object_name = 'tutorials' + type = "TUTORIAL" + template_name = 'tutorialv2/index.html' + + +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorialv2/help.html' + + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set + + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context + +# TODO ArticleWithHelp + + class DisplayDiff(DetailView): """Display the difference between two version of a content. Reference is always HEAD and compared version is a GET query parameter named sha @@ -284,10 +492,6 @@ def get_context_data(self, **kwargs): return context -class DisplayArticle(DisplayContent): - type = "ARTICLE" - - class DisplayOnlineContent(DisplayContent): """Display online tutorial""" type = "TUTORIAL" @@ -399,6 +603,18 @@ class DisplayOnlineArticle(DisplayOnlineContent): type = "ARTICLE" +def render_chapter_form(chapter): + if chapter.part: + return ChapterForm({"title": chapter.title, + "introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + else: + + return \ + EmbdedChapterForm({"introduction": chapter.get_introduction(), + "conclusion": chapter.get_conclusion()}) + + # Staff actions. diff --git a/zds/urls.py b/zds/urls.py index 0ffbb375b6..c061eecd17 100644 --- a/zds/urls.py +++ b/zds/urls.py @@ -75,7 +75,7 @@ def location(self, article): urlpatterns = patterns('', url(r'^tutoriels/', include('zds.tutorial.urls')), url(r'^articles/', include('zds.article.urls')), - url(r'^contenu/', include('zds.tutorialv2.urls')), + url(r'^', include('zds.tutorialv2.urls')), url(r'^forums/', include('zds.forum.urls')), url(r'^mp/', include('zds.mp.urls')), url(r'^membres/', include('zds.member.urls')), From eb458afb436efafd41da229891d559b45117be06 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 10:47:17 +0100 Subject: [PATCH 109/887] Ajout d'un fichier de migration oublie --- ...auto__add_field_publishablecontent_slug.py | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py diff --git a/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py b/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py new file mode 100644 index 0000000000..3d836c0669 --- /dev/null +++ b/zds/tutorialv2/migrations/0002_auto__add_field_publishablecontent_slug.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'PublishableContent.slug' + db.add_column(u'tutorialv2_publishablecontent', 'slug', + self.gf('django.db.models.fields.CharField')(default='', max_length=80), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PublishableContent.slug' + db.delete_column(u'tutorialv2_publishablecontent', 'slug') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'gallery.gallery': { + 'Meta': {'object_name': 'Gallery'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'gallery.image': { + 'Meta': {'object_name': 'Image'}, + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'legend': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'physical': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.contentreaction': { + 'Meta': {'object_name': 'ContentReaction', '_ormbases': [u'utils.Comment']}, + u'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['utils.Comment']", 'unique': 'True', 'primary_key': 'True'}), + 'related_content': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'related_content_note'", 'to': u"orm['tutorialv2.PublishableContent']"}) + }, + u'tutorialv2.contentread': { + 'Meta': {'object_name': 'ContentRead'}, + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.ContentReaction']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_notes_read'", 'to': u"orm['auth.User']"}) + }, + u'tutorialv2.publishablecontent': { + 'Meta': {'object_name': 'PublishableContent'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'gallery': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Gallery']", 'null': 'True', 'blank': 'True'}), + 'helps': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['utils.HelpWriting']", 'db_index': 'True', 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gallery.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'is_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'js_support': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_note': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_note'", 'null': 'True', 'to': u"orm['tutorialv2.ContentReaction']"}), + 'licence': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['utils.Licence']", 'null': 'True', 'blank': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'relative_images_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'sha_beta': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_draft': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_public': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'sha_validation': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'subcategory': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['utils.SubCategory']", 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'db_index': 'True'}), + 'update_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'tutorialv2.validation': { + 'Meta': {'object_name': 'Validation'}, + 'comment_authors': ('django.db.models.fields.TextField', [], {}), + 'comment_validator': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['tutorialv2.PublishableContent']", 'null': 'True', 'blank': 'True'}), + 'date_proposition': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'date_reserve': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'date_validation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '10'}), + 'validator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'author_content_validations'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}) + }, + u'utils.comment': { + 'Meta': {'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': u"orm['auth.User']"}), + 'dislike': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'editor': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments-editor'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '39'}), + 'is_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'like': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'position': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'pubdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'text_hidden': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80'}), + 'text_html': ('django.db.models.fields.TextField', [], {}), + 'update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + u'utils.helpwriting': { + 'Meta': {'object_name': 'HelpWriting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '20'}), + 'tablelabel': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + u'utils.licence': { + 'Meta': {'object_name': 'Licence'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + }, + u'utils.subcategory': { + 'Meta': {'object_name': 'SubCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '80'}), + 'subtitle': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '80'}) + } + } + + complete_apps = ['tutorialv2'] \ No newline at end of file From e2a787364ac5f3864f5577eb1c11e897e3902a41 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 11:11:31 +0100 Subject: [PATCH 110/887] Force le slug a etre correct --- zds/tutorialv2/models.py | 3 ++- zds/tutorialv2/views.py | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 63cf578acf..7f383f9daf 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -830,7 +830,8 @@ def load_version(self, sha=None, public=False): if 'licence' in json: versioned.licence = Licence.objects.filter(code=json['licence']).first() else: - versioned.licence = Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) + versioned.licence = \ + Licence.objects.filter(pk=settings.ZDS_APP['tutorial']['default_license_pk']).first() if 'introduction' in json: versioned.introduction = json['introduction'] diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4a9ef3cd02..8a44cdd1a9 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -111,7 +111,6 @@ class CreateContent(FormView): @method_decorator(login_required) @method_decorator(can_write_and_read_now) def dispatch(self, *args, **kwargs): - """rewrite this method to ensure decoration""" return super(CreateContent, self).dispatch(*args, **kwargs) def form_valid(self, form): @@ -190,7 +189,6 @@ class DisplayContent(DetailView): @method_decorator(login_required) def dispatch(self, *args, **kwargs): - """rewrite this method to ensure decoration""" return super(DisplayContent, self).dispatch(*args, **kwargs) def get_forms(self, context, content): @@ -212,11 +210,13 @@ def get_forms(self, context, content): context["formAskValidation"] = form_ask_validation context["formJs"] = form_js context["formValid"] = form_valid - context["formReject"] = form_reject, + context["formReject"] = form_reject def get_object(self, queryset=None): - # TODO : check slug ? - return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" @@ -243,10 +243,6 @@ def get_context_data(self, **kwargs): if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # check if slug is good: - if self.kwargs['slug'] != content.slug: - raise Http404 - # load versioned file versioned_tutorial = content.load_version(sha) @@ -276,8 +272,10 @@ def dispatch(self, *args, **kwargs): return super(EditContent, self).dispatch(*args, **kwargs) def get_object(self, queryset=None): - # TODO: check slug ? - return get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_initial(self): """rewrite function to pre-populate form""" @@ -374,10 +372,11 @@ def dispatch(self, *args, **kwargs): """rewrite this method to ensure decoration""" return super(DeleteContent, self).dispatch(*args, **kwargs) - def get_queryset(self): - # TODO: check slug ? - qs = super(DeleteContent, self).get_queryset() - return qs.filter(pk=self.kwargs['pk']) + def get_object(self, queryset=None): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def delete(self, request, *args, **kwargs): """rewrite delete() function to ensure repository deletion""" From ed1e444de8a5e991a4114da818eab57aa802e4ec Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 30 Jan 2015 21:56:06 +0100 Subject: [PATCH 111/887] Ajoute de features Possibilite d'ajouter, d'editer et de supprimer des conteneurs et extraits !! --- templates/tutorialv2/base.html | 8 +- templates/tutorialv2/create/container.html | 38 + templates/tutorialv2/create/content.html | 4 - templates/tutorialv2/create/extract.html | 43 + templates/tutorialv2/edit/container.html | 39 + templates/tutorialv2/edit/content.html | 4 - templates/tutorialv2/edit/extract.html | 45 + templates/tutorialv2/includes/child.part.html | 42 + .../tutorialv2/includes/delete.part.html | 16 + .../includes/tags_authors.part.html | 6 +- templates/tutorialv2/view/container.html | 202 + templates/tutorialv2/view/content.html | 23 +- zds/tutorialv2/forms.py | 120 +- zds/tutorialv2/models.py | 327 +- zds/tutorialv2/url/url_contents.py | 59 +- zds/tutorialv2/views.py | 3296 ++++------------- 16 files changed, 1594 insertions(+), 2678 deletions(-) create mode 100644 templates/tutorialv2/create/container.html create mode 100644 templates/tutorialv2/create/extract.html create mode 100644 templates/tutorialv2/edit/container.html create mode 100644 templates/tutorialv2/edit/extract.html create mode 100644 templates/tutorialv2/includes/child.part.html create mode 100644 templates/tutorialv2/includes/delete.part.html create mode 100644 templates/tutorialv2/view/container.html diff --git a/templates/tutorialv2/base.html b/templates/tutorialv2/base.html index 73ef8519d1..2c8ba30155 100644 --- a/templates/tutorialv2/base.html +++ b/templates/tutorialv2/base.html @@ -4,20 +4,20 @@ {% block title_base %} - • {% trans "Tutoriels" %} + • {% trans "Mes contenus" %} {% endblock %} {% block mobile_title %} - {% trans "Tutoriels" %} + {% trans "Mes contenus" %} {% endblock %} {% block breadcrumb_base %} - {% if user in tutorial.authors.all %} -
  • {% trans "Mes tutoriels" %}
  • + {% if user in content.authors.all %} +
  • {% trans "Mes contenus" %}
  • {% else %}
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Nouveau conteneur" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/create/content.html b/templates/tutorialv2/create/content.html index 0ab6c81cdb..5643d67e23 100644 --- a/templates/tutorialv2/create/content.html +++ b/templates/tutorialv2/create/content.html @@ -7,10 +7,6 @@ {% trans "Nouveau contenu" %} {% endblock %} -{% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • -{% endblock %} - {% block breadcrumb %}
  • {% trans "Nouveau contenu" %}
  • {% endblock %} diff --git a/templates/tutorialv2/create/extract.html b/templates/tutorialv2/create/extract.html new file mode 100644 index 0000000000..5c267b9f01 --- /dev/null +++ b/templates/tutorialv2/create/extract.html @@ -0,0 +1,43 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Nouvel extrait" %} +{% endblock %} + + + +{% block headline %} +

    + {% trans "Nouvel extrait" %} +

    +{% endblock %} + + + +{% block breadcrumb %} + + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Nouvel extrait" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} + + {% if form.text.value %} + {% include "misc/previsualization.part.html" with text=form.text.value %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/container.html b/templates/tutorialv2/edit/container.html new file mode 100644 index 0000000000..d9e771fb3f --- /dev/null +++ b/templates/tutorialv2/edit/container.html @@ -0,0 +1,39 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Editer un conteneur" %} +{% endblock %} + +{% block breadcrumb %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + +
  • {% trans "Editer le conteneur" %}
  • +{% endblock %} + +{% block headline %} +

    + {% trans "Éditer" %} : {{ container.title }} +

    +{% endblock %} + + + +{% block content %} + {% if new_version %} +

    + {% trans "Une nouvelle version a été postée avant que vous ne validiez" %}. +

    + {% endif %} + {% crispy form %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html index 9d4a2cc26c..2f05ddcdae 100644 --- a/templates/tutorialv2/edit/content.html +++ b/templates/tutorialv2/edit/content.html @@ -7,10 +7,6 @@ {% trans "Éditer le contenu" %} {% endblock %} -{% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • -{% endblock %} - {% block breadcrumb %}
  • {{ content.title }}
  • {% trans "Éditer le contenu" %}
  • diff --git a/templates/tutorialv2/edit/extract.html b/templates/tutorialv2/edit/extract.html new file mode 100644 index 0000000000..65c5a37773 --- /dev/null +++ b/templates/tutorialv2/edit/extract.html @@ -0,0 +1,45 @@ +{% extends "tutorialv2/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + + +{% block title %} + {% trans "Éditer l'extrait" %} +{% endblock %} + + + +{% block headline %} +

    + {% trans "Éditer l'extrait" %} +

    +{% endblock %} + + + +{% block breadcrumb %} + + {% with container=extract.container %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • + {% endwith %} + +
  • {% trans "Éditer l'extrait" %}
  • +{% endblock %} + + + +{% block content %} + {% crispy form %} + + {% if form.text.value %} + {% include "misc/previsualization.part.html" with text=form.text.value %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html new file mode 100644 index 0000000000..683fea4a72 --- /dev/null +++ b/templates/tutorialv2/includes/child.part.html @@ -0,0 +1,42 @@ +{% load emarkdown %} +{% load i18n %} + + +

    + + {{ child.title }} + +

    + +{% if user in content.authors.all or perms.tutorial.change_tutorial %} +
    + + {% trans "Éditer" %} + + + {% include "tutorialv2/includes/delete.part.html" with object=child additional_classes="ico-after cross btn btn-grey" %} +
    +{% endif %} + +{% if child.text %} + {# child is an extract #} + + {{ child.get_text|emarkdown }} + +{% else %} + {# child is a container #} + + {% if child.children %} +
      + {% for subchild in child.children %} +
    1. + {{ subchild.title }} +
    2. + {% endfor %} +
    + {% else %} +

    + {% trans "Ce conteneur est actuelement vide" %}. +

    + {% endif %} +{% endif %} diff --git a/templates/tutorialv2/includes/delete.part.html b/templates/tutorialv2/includes/delete.part.html new file mode 100644 index 0000000000..f42b4caf18 --- /dev/null +++ b/templates/tutorialv2/includes/delete.part.html @@ -0,0 +1,16 @@ +{% load i18n %} + +{# note : ico-after cross btn btn-grey #} + +{% trans "Supprimer" %} + \ No newline at end of file diff --git a/templates/tutorialv2/includes/tags_authors.part.html b/templates/tutorialv2/includes/tags_authors.part.html index 5fb924fd4e..319f275022 100644 --- a/templates/tutorialv2/includes/tags_authors.part.html +++ b/templates/tutorialv2/includes/tags_authors.part.html @@ -13,11 +13,11 @@ -{% if content.update %} +{% if content.update_date %} {% trans "Dernière mise à jour" %} : - {% endif %} diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html new file mode 100644 index 0000000000..13cb0e5ca7 --- /dev/null +++ b/templates/tutorialv2/view/container.html @@ -0,0 +1,202 @@ +{% extends "tutorialv2/base.html" %} +{% load set %} +{% load thumbnail %} +{% load emarkdown %} +{% load i18n %} + + +{% block title %} + {{ container.title }} - {{ content.title }} +{% endblock %} + + + +{% block breadcrumb %} + {% if container.parent.parent %} +
  • {{ container.parent.parent.title }}
  • + {% endif %} + + {% if container.parent %} +
  • {{ container.parent.title }}
  • + {% endif %} + +
  • {{ container.title }}
  • +{% endblock %} + + + +{% block headline %} + {% if content.licence %} +

    + {{ content.licence }} +

    + {% endif %} + +

    + {{ container.title }} +

    + + {% include 'tutorialv2/includes/tags_authors.part.html' %} + + {% if content.is_beta %} +
    +
    + {% blocktrans %}Cette version du tutoriel est en BÊTA !{% endblocktrans %} +
    +
    + {% endif %} +{% endblock %} + + + +{% block content %} + {% if container.get_introduction and container.get_introduction != "None" %} + {{ container.get_introduction|emarkdown }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas d'introduction" %}. +

    + {% endif %} + + + +
    + + {% for child in container.children %} + {% include "tutorialv2/includes/child.part.html" with child=child %} + {% empty %} +

    + {% trans "Ce conteneur est actuelement vide" %}. +

    + {% endfor %} + +
    + + {% if container.get_conclusion and container.get_conclusion != "None" %} + {{ container.get_conclusion|emarkdown }} + {% elif not content.is_beta %} +

    + {% trans "Il n'y a pas de conclusion" %}. +

    + {% endif %} +{% endblock %} + + + +{% block sidebar_new %} + {% if user in content.authors.all or perms.tutorial.change_tutorial %} + + + {% trans "Éditer" %} + + + + {% if container.can_add_container %} + + {% trans "Ajouter un conteneur" %} + + {% endif %} + + {% if container.can_add_extract %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_actions %} + {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if chapter.part %} +
  • + + {% blocktrans %} + Déplacer le chapitre + {% endblocktrans %} + + +
  • + {% endif %} + {% endif %} +{% endblock %} + + + +{% block sidebar_blocks %} + {# include "tutorial/includes/summary.part.html" with tutorial=tutorial chapter_current=chapter #} + + {% if user in content.authors.all or perms.content.change_content %} + + {% endif %} +{% endblock %} diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 66daa60f08..20ed95446c 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -12,13 +12,6 @@ {% endblock %} -{% if user in content.authors.all or perms.content.change_content %} - {% block breadcrumb_base %} -
  • {% trans "Mes contenus" %}
  • - {% endblock %} -{% endif %} - - {% block breadcrumb %}
  • {{ content.title }}
  • {% endblock %} @@ -119,8 +112,8 @@


    - {% for child in content.childreen %} - {# include stuff #} + {% for child in content.children %} + {% include "tutorialv2/includes/child.part.html" with child=child %} {% empty %}

    {% trans "Ce contenu est actuelement vide" %}. @@ -147,6 +140,18 @@

    {% trans "Éditer" %} + {% if content.can_add_container %} + + {% trans "Ajouter un conteneur" %} + + {% endif %} + + {% if content.can_add_extract %} + + {% trans "Ajouter un extrait" %} + + {% endif %} + {% else %} {% trans "Version brouillon" %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 7bd5236838..beeb1ffdd9 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -4,7 +4,7 @@ from crispy_forms.bootstrap import StrictButton from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Layout, Fieldset, Submit, Field, \ +from crispy_forms.layout import HTML, Layout, Submit, Field, \ ButtonHolder, Hidden from django.core.urlresolvers import reverse @@ -157,7 +157,7 @@ def __init__(self, *args, **kwargs): disabled=True) -class PartForm(FormWithTitle): +class ContainerForm(FormWithTitle): introduction = forms.CharField( label=_(u"Introduction"), @@ -191,7 +191,7 @@ class PartForm(FormWithTitle): ) def __init__(self, *args, **kwargs): - super(PartForm, self).__init__(*args, **kwargs) + super(ContainerForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' @@ -206,122 +206,8 @@ def __init__(self, *args, **kwargs): StrictButton( _(u'Valider'), type='submit'), - StrictButton( - _(u'Ajouter et continuer'), - type='submit', - name='submit_continue'), - ) - ) - - -class ChapterForm(FormWithTitle): - - image = forms.ImageField( - label=_(u'Selectionnez le logo du tutoriel ' - u'(max. {0} Ko)').format(str(settings.ZDS_APP['gallery']['image_max_size'] / 1024)), - required=False - ) - - introduction = forms.CharField( - label=_(u'Introduction'), - required=False, - widget=forms.Textarea( - attrs={ - 'placeholder': _(u'Votre message au format Markdown.') - } - ) - ) - - conclusion = forms.CharField( - label=_(u'Conclusion'), - required=False, - widget=forms.Textarea( - attrs={ - 'placeholder': _(u'Votre message au format Markdown.') - } - ) - ) - - msg_commit = forms.CharField( - label=_(u"Message de suivi"), - max_length=80, - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': _(u'Un résumé de vos ajouts et modifications') - } - ) - ) - - def __init__(self, *args, **kwargs): - super(ChapterForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_class = 'content-wrapper' - self.helper.form_method = 'post' - - self.helper.layout = Layout( - Field('title'), - Field('image'), - Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor'), - Field('msg_commit'), - Hidden('last_hash', '{{ last_hash }}'), - ButtonHolder( - StrictButton( - _(u'Valider'), - type='submit'), - StrictButton( - _(u'Ajouter et continuer'), - type='submit', - name='submit_continue'), - )) - - -class EmbdedChapterForm(forms.Form): - introduction = forms.CharField( - required=False, - widget=forms.Textarea - ) - - image = forms.ImageField( - label=_(u'Sélectionnez une image'), - required=False) - - conclusion = forms.CharField( - required=False, - widget=forms.Textarea - ) - - msg_commit = forms.CharField( - label=_(u'Message de suivi'), - max_length=80, - required=False, - widget=forms.TextInput( - attrs={ - 'placeholder': _(u'Un résumé de vos ajouts et modifications') - } - ) - ) - - def __init__(self, *args, **kwargs): - self.helper = FormHelper() - self.helper.form_class = 'content-wrapper' - self.helper.form_method = 'post' - - self.helper.layout = Layout( - Fieldset( - _(u'Contenu'), - Field('image'), - Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor'), - Field('msg_commit'), - Hidden('last_hash', '{{ last_hash }}'), - ), - ButtonHolder( - Submit('submit', _(u'Valider')) ) ) - super(EmbdedChapterForm, self).__init__(*args, **kwargs) class ExtractForm(FormWithTitle): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 7f383f9daf..21e03de321 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -19,6 +19,7 @@ from django.db import models from datetime import datetime from git.repo import Repo +from django.core.exceptions import PermissionDenied from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user @@ -166,6 +167,25 @@ def add_slug_to_pool(self, slug): else: raise Exception('slug "{}" already in the slug pool !'.format(slug)) + def can_add_container(self): + """ + :return: True if this container accept child container, false otherwise + """ + if not self.has_extracts(): + if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth']-1: + if self.top_container().type != 'ARTICLE': + return True + return False + + def can_add_extract(self): + """ + :return: True if this container accept child extract, false otherwise + """ + if not self.has_sub_containers(): + if self.get_tree_depth() <= ZDS_APP['content']['max_tree_depth']: + return True + return False + def add_container(self, container, generate_slug=False): """ Add a child Container, but only if no extract were previously added and tree depth is < 2. @@ -173,20 +193,17 @@ def add_container(self, container, generate_slug=False): :param container: the new container :param generate_slug: if `True`, ask the top container an unique slug for this object """ - if not self.has_extracts(): - if self.get_tree_depth() < ZDS_APP['content']['max_tree_depth'] and self.top_container().type != 'ARTICLE': - if generate_slug: - container.slug = self.get_unique_slug(container.title) - else: - self.add_slug_to_pool(container.slug) - container.parent = self - container.position_in_parent = self.get_last_child_position() + 1 - self.children.append(container) - self.children_dict[container.slug] = container + if self.can_add_container(): + if generate_slug: + container.slug = self.get_unique_slug(container.title) else: - raise InvalidOperationError("Cannot add another level to this content") + self.add_slug_to_pool(container.slug) + container.parent = self + container.position_in_parent = self.get_last_child_position() + 1 + self.children.append(container) + self.children_dict[container.slug] = container else: - raise InvalidOperationError("Can't add a container if this container already contains extracts.") + raise InvalidOperationError("Cannot add another level to this container") def add_extract(self, extract, generate_slug=False): """ @@ -194,7 +211,7 @@ def add_extract(self, extract, generate_slug=False): :param extract: the new extract :param generate_slug: if `True`, ask the top container an unique slug for this object """ - if not self.has_sub_containers(): + if self.can_add_extract(): if generate_slug: extract.slug = self.get_unique_slug(extract.title) else: @@ -213,6 +230,7 @@ def update_children(self): changed. Note : this function does not account for a different arrangement of the files. """ + # TODO : path comparison instead of pure rewritring ? self.introduction = os.path.join(self.get_path(relative=True), "introduction.md") self.conclusion = os.path.join(self.get_path(relative=True), "conclusion.md") for child in self.children: @@ -244,6 +262,42 @@ def get_prod_path(self): base = self.parent.get_prod_path() return os.path.join(base, self.slug) + def get_absolute_url(self): + """ + :return: url to access the container + """ + return self.top_container().get_absolute_url() + self.get_path(relative=True) + '/' + + def get_edit_url(self): + """ + :return: url to edit the container + """ + slugs = [self.slug] + parent = self.parent + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.top_container().pk] + args.extend(slugs) + + return reverse('content:edit-container', args=args) + + def get_delete_url(self): + """ + :return: url to edit the container + """ + slugs = [self.slug] + parent = self.parent + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.top_container().pk] + args.extend(slugs) + + return reverse('content:delete', args=args) + def get_introduction(self): """ :return: the introduction from the file in `self.introduction` @@ -270,19 +324,26 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): :return : commit sha """ + repo = self.top_container().repository + # update title if title != self.title: self.title = title if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed + old_path = self.get_path(relative=True) self.slug = self.top_container().get_unique_slug(title) + new_path = self.get_path(relative=True) + repo.index.move([old_path, new_path]) + + # update manifest self.update_children() - # TODO : and move() !!! # update introduction and conclusion + rel_path = self.get_path(relative=True) if self.introduction is None: - self.introduction = self.get_path(relative=True) + 'introduction.md' + self.introduction = os.path.join(rel_path, 'introduction.md') if self.conclusion is None: - self.conclusion = self.get_path(relative=True) + 'conclusion.md' + self.conclusion = os.path.join(rel_path, 'conclusion.md') path = self.top_container().get_path() f = open(os.path.join(path, self.introduction), "w") @@ -293,8 +354,6 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): f.close() self.top_container().dump_json() - - repo = self.top_container().repository repo.index.add(['manifest.json', self.introduction, self.conclusion]) if commit_message == '': @@ -303,9 +362,110 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): return cm.hexsha - # TODO: - # - get_absolute_url_*() stuffs (harder than it seems, because they cannot be written in a recursive way) - # - the `maj_repo_*()` stuffs should probably be into the model ? + def repo_add_container(self, title, introduction, conclusion, commit_message=''): + """ + :param title: title of the new container + :param introduction: text of its introduction + :param conclusion: text of its conclusion + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + subcontainer = Container(title) + + # can a subcontainer be added ? + try: + self.add_container(subcontainer, generate_slug=True) + except Exception: + raise PermissionDenied + + # create directory + repo = self.top_container().repository + path = self.top_container().get_path() + rel_path = subcontainer.get_path(relative=True) + os.makedirs(os.path.join(path, rel_path), mode=0o777) + + repo.index.add([rel_path]) + + # create introduction and conclusion + subcontainer.introduction = os.path.join(rel_path, 'introduction.md') + subcontainer.conclusion = os.path.join(rel_path, 'conclusion.md') + + f = open(os.path.join(path, subcontainer.introduction), "w") + f.write(introduction.encode('utf-8')) + f.close() + f = open(os.path.join(path, subcontainer.conclusion), "w") + f.write(conclusion.encode('utf-8')) + f.close() + + # commit + self.top_container().dump_json() + repo.index.add(['manifest.json', subcontainer.introduction, subcontainer.conclusion]) + + if commit_message == '': + commit_message = u'Création du conteneur « ' + title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_add_extract(self, title, text, commit_message=''): + """ + :param title: title of the new extract + :param text: text of the new extract + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + extract = Extract(title) + + # can an extract be added ? + try: + self.add_extract(extract, generate_slug=True) + except Exception: + raise PermissionDenied + + # create text + repo = self.top_container().repository + path = self.top_container().get_path() + + extract.text = extract.get_path(relative=True) + f = open(os.path.join(path, extract.text), "w") + f.write(text.encode('utf-8')) + f.close() + + # commit + self.top_container().dump_json() + repo.index.add(['manifest.json', extract.text]) + + if commit_message == '': + commit_message = u'Création de l\'extrait « ' + title + u' »' + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_delete(self, commit_message=''): + """ + :param commit_message: commit message used instead of default one if provided + :return: commit sha + """ + path = self.get_path(relative=True) + repo = self.top_container().repository + repo.index.remove([path], r=True) + shutil.rmtree(self.get_path()) # looks like removing from git is not enough !! + + # now, remove from manifest + # work only if slug is correct + top = self.top_container() + top.children_dict.pop(self.slug) + top.children.pop(top.children.index(self)) + + # commit + top.dump_json() + repo.index.add(['manifest.json']) + + if commit_message == '': + commit_message = u'Suppression du conteneur « {} »'.format(self.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha class Extract: @@ -318,14 +478,14 @@ class Extract: title = '' slug = '' container = None - position_in_container = 1 + position_in_parent = 1 text = None - def __init__(self, title, slug='', container=None, position_in_container=1): + def __init__(self, title, slug='', container=None, position_in_parent=1): self.title = title self.slug = slug self.container = container - self.position_in_container = position_in_container + self.position_in_parent = position_in_parent def __unicode__(self): return u''.format(self.title) @@ -336,8 +496,8 @@ def get_absolute_url(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) def get_absolute_url_online(self): @@ -346,8 +506,8 @@ def get_absolute_url_online(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_online(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) def get_absolute_url_beta(self): @@ -356,10 +516,40 @@ def get_absolute_url_beta(self): """ return '{0}#{1}-{2}'.format( self.container.get_absolute_url_beta(), - self.position_in_container, - slugify(self.title) + self.position_in_parent, + self.slug ) + def get_edit_url(self): + """ + :return: url to edit the extract + """ + slugs = [self.slug] + parent = self.container + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.container.top_container().pk] + args.extend(slugs) + + return reverse('content:edit-extract', args=args) + + def get_delete_url(self): + """ + :return: url to delete the extract + """ + slugs = [self.slug] + parent = self.container + while parent is not None: + slugs.append(parent.slug) + parent = parent.parent + slugs.reverse() + args = [self.container.top_container().pk] + args.extend(slugs) + + return reverse('content:delete', args=args) + def get_path(self, relative=False): """ Get the physical path to the draft version of the extract. @@ -377,16 +567,69 @@ def get_text(self): self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) - def get_text_online(self): + def repo_update(self, title, text, commit_message=''): + """ + :param title: new title of the extract + :param text: new text of the extract + :param commit_message: commit message that will be used instead of the default one + :return: commit sha + """ + + repo = self.container.top_container().repository + + if title != self.title: + # get a new slug + old_path = self.get_path(relative=True) + self.title = title + self.slug = self.container.get_unique_slug(title) + new_path = self.get_path(relative=True) + # move file + repo.index.move([old_path, new_path]) + + # edit text + path = self.container.top_container().get_path() + + self.text = self.get_path(relative=True) + f = open(os.path.join(path, self.text), "w") + f.write(text.encode('utf-8')) + f.close() + + # commit + self.container.top_container().dump_json() + repo.index.add(['manifest.json', self.text]) + + if commit_message == '': + commit_message = u'Modification de l\'extrait « {} », situé dans le conteneur « {} »'\ + .format(self.title, self.container.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha + + def repo_delete(self, commit_message=''): """ - :return: public text of the extract + :param commit_message: commit message used instead of default one if provided + :return: commit sha """ - path = self.container.top_container().get_prod_path() + self.text + '.html' - if os.path.exists(path): - txt = open(path) - txt_content = txt.read() - txt.close() - return txt_content.decode('utf-8') + path = self.get_path(relative=True) + repo = self.container.top_container().repository + repo.index.remove([path]) + os.remove(self.get_path()) # looks like removing from git is not enough + + # now, remove from manifest + # work only if slug is correct!! + top = self.container + top.children_dict.pop(self.slug, None) + top.children.pop(top.children.index(self)) + + # commit + top.top_container().dump_json() + repo.index.add(['manifest.json']) + + if commit_message == '': + commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) + cm = repo.index.commit(commit_message) + + return cm.hexsha class VersionedContent(Container): @@ -464,12 +707,6 @@ def get_absolute_url_beta(self): else: return self.get_absolute_url() - def get_edit_url(self): - """ - :return: the url to edit the tutorial - """ - return reverse('zds.tutorialv2.views.modify_tutorial') + '?tutorial={0}'.format(self.slug) - def get_path(self, relative=False): """ Get the physical path to the draft version of the Content. @@ -486,7 +723,7 @@ def get_prod_path(self): Get the physical path to the public version of the content :return: physical path """ - return os.path.join(settings.ZDS_APP['contents']['repo_public_path'], self.slug) + return os.path.join(settings.ZDS_APP['content']['repo_public_path'], self.slug) def get_json(self): """ diff --git a/zds/tutorialv2/url/url_contents.py b/zds/tutorialv2/url/url_contents.py index 58718bb534..e22c50492d 100644 --- a/zds/tutorialv2/url/url_contents.py +++ b/zds/tutorialv2/url/url_contents.py @@ -2,21 +2,78 @@ from django.conf.urls import patterns, url -from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent +from zds.tutorialv2.views import ListContent, DisplayContent, CreateContent, EditContent, DeleteContent,\ + CreateContainer, DisplayContainer, EditContainer, CreateExtract, EditExtract, DeleteContainerOrExtract urlpatterns = patterns('', url(r'^$', ListContent.as_view(), name='index'), # view: + url(r'^(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + DisplayContainer.as_view(), + name='view-container'), + url(r'^(?P\d+)/(?P.+)/(?P.+)/$', + DisplayContainer.as_view(), + name='view-container'), + url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(), name='view'), # create: url(r'^nouveau/$', CreateContent.as_view(), name='create'), + url(r'^nouveau-conteneur/(?P\d+)/(?P.+)/(?P.+)/$', + CreateContainer.as_view(), + name='create-container'), + url(r'^nouveau-conteneur/(?P\d+)/(?P.+)/$', + CreateContainer.as_view(), + name='create-container'), + + + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + url(r'^nouvel-extrait/(?P\d+)/(?P.+)/$', + CreateExtract.as_view(), + name='create-extract'), + # edit: + url(r'^editer-conteneur/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + EditContainer.as_view(), + name='edit-container'), + url(r'^editer-conteneur/(?P\d+)/(?P.+)/(?P.+)/$', + EditContainer.as_view(), + name='edit-container'), + + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/' + r'(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer-extrait/(?P\d+)/(?P.+)/(?P.+)/$', + EditExtract.as_view(), + name='edit-extract'), + url(r'^editer/(?P\d+)/(?P.+)/$', EditContent.as_view(), name='edit'), # delete: + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/' + r'(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/(?P.+)/$', + DeleteContainerOrExtract.as_view(), + name='delete'), + url(r'^supprimer/(?P\d+)/(?P.+)/$', DeleteContent.as_view(), name='delete'), ) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8a44cdd1a9..814f39ecdb 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -4,7 +4,6 @@ from datetime import datetime from operator import attrgetter from urllib import urlretrieve -from django.contrib.humanize.templatetags.humanize import naturaltime from urlparse import urlparse, parse_qs try: import ujson as json_reader @@ -38,33 +37,26 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.encoding import smart_str from django.views.decorators.http import require_POST -from git import Repo, Actor -from lxml import etree +from git import Repo -from forms import ContentForm, PartForm, ChapterForm, EmbdedChapterForm, \ - ExtractForm, ImportForm, ImportArchiveForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm +from forms import ContentForm, ContainerForm, \ + ExtractForm, NoteForm, AskValidationForm, ValidForm, RejectForm, ActivJsForm from models import PublishableContent, Container, Extract, Validation, ContentReaction, init_new_repo from utils import never_read, mark_read from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now -from zds.member.models import get_info_old_tuto, Profile from zds.member.views import get_client_ip -from zds.forum.models import Forum, Topic from zds.utils import slugify from zds.utils.models import Alert -from zds.utils.models import Category, Licence, CommentLike, CommentDislike, \ +from zds.utils.models import Category, CommentLike, CommentDislike, \ SubCategory, HelpWriting from zds.utils.mps import send_mp -from zds.utils.forums import create_topic, send_post, lock_topic, unlock_topic from zds.utils.paginator import paginator_range from zds.utils.templatetags.emarkdown import emarkdown -from zds.utils.tutorials import get_blob, export_tutorial_to_md, move, get_sep, get_text_is_empty, import_archive -from zds.utils.misc import compute_hash, content_has_changed +from zds.utils.tutorials import get_blob, export_tutorial_to_md from django.utils.translation import ugettext as _ from django.views.generic import ListView, DetailView, FormView, DeleteView -# until we completely get rid of these, import them : from zds.tutorial.models import Tutorial, Chapter, Part -from zds.tutorial.forms import TutorialForm class ListContent(ListView): @@ -173,10 +165,7 @@ def form_valid(self, form): return super(CreateContent, self).form_valid(form) def get_success_url(self): - if self.content: - return reverse('content:view', args=[self.content.pk, self.content.slug]) - else: - return reverse('content:index') + return reverse('content:view', args=[self.content.pk, self.content.slug]) class DisplayContent(DetailView): @@ -389,2144 +378,1023 @@ def get_success_url(self): return reverse('content:index') -class ArticleList(ListView): - """ - Displays the list of published articles. - """ - context_object_name = 'articles' - paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] - type = "ARTICLE" - template_name = 'article/index.html' - tag = None +class CreateContainer(FormView): + template_name = 'tutorialv2/create/container.html' + form_class = ContainerForm + content = None - def get_queryset(self): - """ - Filter the content to obtain the list of only articles. If tag parameter is provided, only articles - which have this category will be listed. - :return: list of articles - """ - if self.request.GET.get('tag') is not None: - self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) - query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ - .exclude(sha_public='') - if self.tag is not None: - query_set = query_set.filter(subcategory__in=[self.tag]) - return query_set.order_by('-pubdate') + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(CreateContainer, self).dispatch(*args, **kwargs) - def get_context_data(self, **kwargs): - context = super(ArticleList, self).get_context_data(**kwargs) - context['tag'] = self.tag - # TODO in database, the information concern the draft, so we have to make stuff here ! - return context + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(CreateContainer, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() -class TutorialList(ArticleList): - """Displays the list of published tutorials.""" + # get the container: + if 'container_slug' in self.kwargs: + try: + container = context['content'].children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + context['container'] = container + else: + context['container'] = context['content'] + return context - context_object_name = 'tutorials' - type = "TUTORIAL" - template_name = 'tutorialv2/index.html' + def form_valid(self, form): + context = self.get_context_data() + parent = context['container'] + sha = parent.repo_add_container(form.cleaned_data['title'], + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) -class TutorialWithHelp(TutorialList): - """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta - for more documentation, have a look to ZEP 03 specification (fr)""" - context_object_name = 'tutorials' - template_name = 'tutorialv2/help.html' + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - def get_queryset(self): - """get only tutorial that need help and handle filtering if asked""" - query_set = PublishableContent.objects\ - .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ - .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ - .all() - try: - type_filter = self.request.GET.get('type') - query_set = query_set.filter(helps_title__in=[type_filter]) - except KeyError: - # if no filter, no need to change - pass - return query_set + self.success_url = parent.get_absolute_url() - def get_context_data(self, **kwargs): - """Add all HelpWriting objects registered to the context so that the template can use it""" - context = super(TutorialWithHelp, self).get_context_data(**kwargs) - context['helps'] = HelpWriting.objects.all() - return context + return super(CreateContainer, self).form_valid(form) -# TODO ArticleWithHelp +class DisplayContainer(DetailView): + """Base class that can show any content in any state""" -class DisplayDiff(DetailView): - """Display the difference between two version of a content. - Reference is always HEAD and compared version is a GET query parameter named sha - this class has no reason to be adapted to any content type""" model = PublishableContent - template_name = "tutorial/diff.html" - context_object_name = "tutorial" + template_name = 'tutorialv2/view/container.html' + online = False + sha = None + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(DisplayContainer, self).dispatch(*args, **kwargs) def get_object(self, queryset=None): - return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj def get_context_data(self, **kwargs): + """Show the given tutorial if exists.""" - context = super(DisplayDiff, self).get_context_data(**kwargs) + context = super(DisplayContainer, self).get_context_data(**kwargs) + content = context['object'] + # Retrieve sha given by the user. This sha must to be exist. If it doesn't + # exist, we take draft version of the content. try: - sha = self.request.GET.get("sha") + sha = self.request.GET["version"] except KeyError: - sha = self.get_object().sha_draft + if self.sha is not None: + sha = self.sha + else: + sha = content.sha_draft - if self.request.user not in context[self.context_object_name].authors.all(): + # check that if we ask for beta, we also ask for the sha version + is_beta = content.is_beta(sha) + + if self.request.user not in content.authors.all() and not is_beta: + # if we are not author of this content or if we did not ask for beta + # the only members that can display and modify the tutorial are validators if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # open git repo and find diff between displayed version and head - repo = Repo(context[self.context_object_name].get_repo_path()) - current_version_commit = repo.commit(sha) - diff_with_head = current_version_commit.diff("HEAD~1") - context["path_add"] = diff_with_head.iter_change_type("A") - context["path_ren"] = diff_with_head.iter_change_type("R") - context["path_del"] = diff_with_head.iter_change_type("D") - context["path_maj"] = diff_with_head.iter_change_type("M") - return context - - -class DisplayOnlineContent(DisplayContent): - """Display online tutorial""" - type = "TUTORIAL" - template_name = "tutorial/view_online.html" - - def get_forms(self, context, content): - - # Build form to send a note for the current tutorial. - context['form'] = NoteForm(content, self.request.user) - - def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): - dictionary["tutorial"] = content - dictionary["path"] = content.get_repo_path() - dictionary["slug"] = slugify(dictionary["title"]) - dictionary["position_in_tutorial"] = cpt_p - cpt_c = 1 - for chapter in dictionary["chapters"]: - chapter["part"] = dictionary - chapter["slug"] = slugify(chapter["title"]) - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - self.compatibility_chapter(content, repo, sha, chapter) - cpt_c += 1 + # load versioned file + context['content'] = content.load_version(sha) + container = context['content'] - def compatibility_chapter(self, content, repo, sha, dictionary): - """enable compatibility with old version of mini tutorial and chapter implementations""" - dictionary["path"] = content.get_prod_path() - dictionary["type"] = self.type - dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name - dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") - dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") - # load extracts - cpt = 1 - for ext in dictionary["extracts"]: - ext["position_in_chapter"] = cpt - ext["path"] = content.get_prod_path() - text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") - ext["txt"] = text.read() - cpt += 1 + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - def get_context_data(self, **kwargs): - content = self.get_object() - # If the tutorial isn't online, we raise 404 error. - if not content.in_public(): + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: raise Http404 - self.sha = content.sha_public - context = super(DisplayOnlineContent, self).get_context_data(**kwargs) - - context["tutorial"]["update"] = content.update - context["tutorial"]["get_note_count"] = content.get_note_count() - if self.request.user.is_authenticated(): - # If the user is authenticated, he may want to tell the world how cool the content is - # We check if he can post a not or not with - # antispam filter. - context['tutorial']['antispam'] = content.antispam() + context['container'] = container - # If the user has never read this before, we mark this tutorial read. - if never_read(content): - mark_read(content) + return context - # Find all notes of the tutorial. - notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() +class EditContainer(FormView): + template_name = 'tutorialv2/edit/container.html' + form_class = ContainerForm + content = None - # Retrieve pk of the last note. If there aren't notes for the tutorial, we - # initialize this last note at 0. + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(EditContainer, self).dispatch(*args, **kwargs) - last_note_pk = 0 - if content.last_note: - last_note_pk = content.last_note.pk + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj - # Handle pagination + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditContainer, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] - paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) - try: - page_nbr = int(self.request.GET.get("page")) - except KeyError: - page_nbr = 1 - except ValueError: - raise Http404 + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - try: - notes = paginator.page(page_nbr) - except PageNotAnInteger: - notes = paginator.page(1) - except EmptyPage: + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: raise Http404 - res = [] - if page_nbr != 1: + context['container'] = container - # Show the last note of the previous page + return context - last_page = paginator.page(page_nbr - 1).object_list - last_note = last_page[len(last_page) - 1] - res.append(last_note) - for note in notes: - res.append(note) + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + container = context['container'] + initial = super(EditContainer, self).get_initial() - context['notes'] = res - context['last_note_pk'] = last_note_pk - context['pages'] = paginator_range(page_nbr, paginator.num_pages) - context['nb'] = page_nbr + initial['title'] = container.title + initial['introduction'] = container.get_introduction() + initial['conclusion'] = container.get_conclusion() + return initial -class DisplayOnlineArticle(DisplayOnlineContent): - type = "ARTICLE" + def form_valid(self, form): + context = self.get_context_data() + container = context['container'] + sha = container.repo_update(form.cleaned_data['title'], + form.cleaned_data['introduction'], + form.cleaned_data['conclusion'], + form.cleaned_data['msg_commit']) -def render_chapter_form(chapter): - if chapter.part: - return ChapterForm({"title": chapter.title, - "introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) - else: + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - return \ - EmbdedChapterForm({"introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) + self.success_url = container.get_absolute_url() + return super(EditContainer, self).form_valid(form) -# Staff actions. +class CreateExtract(FormView): + template_name = 'tutorialv2/create/extract.html' + form_class = ExtractForm + content = None -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -def list_validation(request): - """Display tutorials list in validation.""" - - # Retrieve type of the validation. Default value is all validations. - - try: - type = request.GET["type"] - except KeyError: - type = None - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - - # Orphan validation. There aren't validator attached to the validations. - - if type == "orphan": - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=True, - status="PENDING").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=True, - status="PENDING", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - elif type == "reserved": - - # Reserved validation. There are a validator attached to the - # validations. - - if subcategory is None: - validations = Validation.objects.filter( - validator__isnull=False, - status="PENDING_V").order_by("date_proposition").all() - else: - validations = Validation.objects.filter(validator__isnull=False, - status="PENDING_V", - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition") \ - .all() - else: - - # Default, we display all validations. - - if subcategory is None: - validations = Validation.objects.filter( - Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() - else: - validations = Validation.objects.filter(Q(status="PENDING") - | Q(status="PENDING_V" - )).filter(tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition")\ - .all() - return render(request, "tutorial/validation/index.html", - {"validations": validations}) - - -@permission_required("tutorial.change_tutorial", raise_exception=True) -@login_required -@require_POST -def reservation(request, validation_pk): - """Display tutorials list in validation.""" - - validation = get_object_or_404(Validation, pk=validation_pk) - if validation.validator: - validation.validator = None - validation.date_reserve = None - validation.status = "PENDING" - validation.save() - messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) - return redirect(reverse("zds.tutorial.views.list_validation")) - else: - validation.validator = request.user - validation.date_reserve = datetime.now() - validation.status = "PENDING_V" - validation.save() - messages.info(request, - _(u"Le tutoriel a bien été \ - réservé par {0}.").format(request.user.username)) - return redirect( - validation.content.get_absolute_url() + - "?version=" + validation.version - ) - - -@login_required -def history(request, tutorial_pk, tutorial_slug): - """History of the tutorial.""" - - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - - repo = Repo(tutorial.get_repo_path()) - logs = repo.head.reference.log() - logs = sorted(logs, key=attrgetter("time"), reverse=True) - return render(request, "tutorial/tutorial/history.html", - {"tutorial": tutorial, "logs": logs}) - - -@login_required -@permission_required("tutorial.change_tutorial", raise_exception=True) -def history_validation(request, tutorial_pk): - """History of the validation of a tutorial.""" - - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - - # Get subcategory to filter validations. - - try: - subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) - except (KeyError, Http404): - subcategory = None - if subcategory is None: - validations = \ - Validation.objects.filter(tutorial__pk=tutorial_pk) \ - .order_by("date_proposition" - ).all() - else: - validations = Validation.objects.filter(tutorial__pk=tutorial_pk, - tutorial__subcategory__in=[subcategory]) \ - .order_by("date_proposition" - ).all() - return render(request, "tutorial/validation/history.html", - {"validations": validations, "tutorial": tutorial}) - - -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def reject_tutorial(request): - """Staff reject tutorial of an author.""" - - # Retrieve current tutorial; - - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") - - if request.user == validation.validator: - validation.comment_validator = request.POST["text"] - validation.status = "REJECT" - validation.date_validation = datetime.now() - validation.save() - - # Remove sha_validation because we rejected this version of the tutorial. - - tutorial.sha_validation = None - tutorial.pubdate = None - tutorial.save() - messages.info(request, _(u"Le tutoriel a bien été refusé.")) - comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) - # send feedback - msg = ( - _(u'Désolé, le zeste **{0}** n\'a malheureusement ' - u'pas passé l’étape de validation. Mais ne désespère pas, ' - u'certaines corrections peuvent surement être faite pour ' - u'l’améliorer et repasser la validation plus tard. ' - u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' - u'N\'hésite pas a lui envoyer un petit message pour discuter ' - u'de la décision ou demander plus de détail si tout cela te ' - u'semble injuste ou manque de clarté.') - .format(tutorial.title, - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), - comment_reject)) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Refus de Validation : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le refuser.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - - -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def valid_tutorial(request): - """Staff valid tutorial of an author.""" - - # Retrieve current tutorial; - - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") - - if request.user == validation.validator: - (output, err) = mep(tutorial, tutorial.sha_validation) - messages.info(request, output) - messages.error(request, err) - validation.comment_validator = request.POST["text"] - validation.status = "ACCEPT" - validation.date_validation = datetime.now() - validation.save() - - # Update sha_public with the sha of validation. We don't update sha_draft. - # So, the user can continue to edit his tutorial in offline. - - if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': - tutorial.pubdate = datetime.now() - tutorial.sha_public = validation.version - tutorial.source = request.POST["source"] - tutorial.sha_validation = None - tutorial.save() - messages.success(request, _(u"Le tutoriel a bien été validé.")) - - # send feedback - - msg = ( - _(u'Félicitations ! Le zeste [{0}]({1}) ' - u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' - u'peuvent venir l\'éplucher et réagir a son sujet. ' - u'Je te conseille de rester a leur écoute afin ' - u'd\'apporter des corrections/compléments.' - u'Un Tutoriel vivant et a jour est bien plus lu ' - u'qu\'un sujet abandonné !') - .format(tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Publication : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le valider.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(CreateExtract, self).dispatch(*args, **kwargs) + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj -@can_write_and_read_now -@login_required -@permission_required("tutorial.change_tutorial", raise_exception=True) -@require_POST -def invalid_tutorial(request, tutorial_pk): - """Staff invalid tutorial of an author.""" + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(CreateExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] - # Retrieve current tutorial + # get the container: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - un_mep(tutorial) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_public).latest("date_proposition") - validation.status = "PENDING" - validation.date_validation = None - validation.save() + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - # Only update sha_validation because contributors can contribute on - # rereading version. + context['container'] = container - tutorial.sha_public = None - tutorial.sha_validation = validation.version - tutorial.pubdate = None - tutorial.save() - messages.success(request, _(u"Le tutoriel a bien été dépublié.")) - return redirect(tutorial.get_absolute_url() + "?version=" - + validation.version) + return context + def form_valid(self, form): + context = self.get_context_data() + parent = context['container'] -# User actions on tutorial. + sha = parent.repo_add_extract(form.cleaned_data['title'], + form.cleaned_data['text'], + form.cleaned_data['msg_commit']) -@can_write_and_read_now -@login_required -@require_POST -def ask_validation(request): - """User ask validation for his tutorial.""" + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - # Retrieve current tutorial; + self.success_url = parent.get_absolute_url() - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + return super(CreateExtract, self).form_valid(form) - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied +class EditExtract(FormView): + template_name = 'tutorialv2/edit/extract.html' + form_class = ExtractForm + content = None - old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, - status__in=['PENDING_V']).first() - if old_validation is not None: - old_validator = old_validation.validator - else: - old_validator = None - # delete old pending validation - Validation.objects.filter(tutorial__pk=tutorial_pk, - status__in=['PENDING', 'PENDING_V'])\ - .delete() - # We create and save validation object of the tutorial. + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(EditExtract, self).dispatch(*args, **kwargs) - validation = Validation() - validation.content = tutorial - validation.date_proposition = datetime.now() - validation.comment_authors = request.POST["text"] - validation.version = request.POST["version"] - if old_validator is not None: - validation.validator = old_validator - validation.date_reserve - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - msg = \ - (_(u'Bonjour {0},' - u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' - u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' - u'consulter l\'historique des versions' - u'\n\n> Merci').format(old_validator.username, tutorial.title)) - send_mp( - bot, - [old_validator], - _(u"Mise à jour de tuto : {0}").format(tutorial.title), - _(u"En validation"), - msg, - False, - ) - validation.save() - validation.content.source = request.POST["source"] - validation.content.sha_validation = request.POST["version"] - validation.content.save() - messages.success(request, - _(u"Votre demande de validation a été envoyée à l'équipe.")) - return redirect(tutorial.get_absolute_url()) + def get_object(self): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(EditExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] -@can_write_and_read_now -@login_required -@require_POST -def delete_tutorial(request, tutorial_pk): - """User would like delete his tutorial.""" + # get the extract: + if 'parent_container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 - # Retrieve current tutorial + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(container, Container): + raise Http404 + else: + raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + extract = None + if 'extract_slug' in self.kwargs: + try: + extract = container.children_dict[self.kwargs['extract_slug']] + except KeyError: + raise Http404 + else: + if not isinstance(extract, Extract): + raise Http404 - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: + context['extract'] = extract - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied + return context + + def get_initial(self): + """rewrite function to pre-populate form""" + context = self.get_context_data() + extract = context['extract'] + initial = super(EditExtract, self).get_initial() - # when author is alone we can delete definitively tutorial + initial['title'] = extract.title + initial['text'] = extract.get_text() - if tutorial.authors.count() == 1: + return initial - # user can access to gallery + def form_valid(self, form): + context = self.get_context_data() + extract = context['extract'] - try: - ug = UserGallery.objects.filter(user=request.user, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - - # Delete the tutorial on the repo and on the database. - - old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - maj_repo_tuto(request, old_slug_path=old_slug, tuto=tutorial, - action="del") - messages.success(request, - _(u'Le tutoriel {0} a bien ' - u'été supprimé.').format(tutorial.title)) - tutorial.delete() - else: - tutorial.authors.remove(request.user) + sha = extract.repo_update(form.cleaned_data['title'], + form.cleaned_data['text'], + form.cleaned_data['msg_commit']) + + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - # user can access to gallery + self.success_url = extract.get_absolute_url() - try: - ug = UserGallery.objects.filter( - user=request.user, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - tutorial.save() - messages.success(request, - _(u'Vous ne faites plus partie des rédacteurs de ce ' - u'tutoriel.')) - return redirect(reverse("zds.tutorial.views.index")) + return super(EditExtract, self).form_valid(form) -@can_write_and_read_now -@require_POST -def modify_tutorial(request): - tutorial_pk = request.POST["tutorial"] - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # User actions - - if request.user in tutorial.authors.all() or request.user.has_perm("tutorial.change_tutorial"): - if "add_author" in request.POST: - redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ - tutorial.pk, - tutorial.slug, - ]) - author_username = request.POST["author"] - author = None - try: - author = User.objects.get(username=author_username) - except User.DoesNotExist: - return redirect(redirect_url) - tutorial.authors.add(author) - tutorial.save() - - # share gallery - - ug = UserGallery() - ug.user = author - ug.gallery = tutorial.gallery - ug.mode = "W" - ug.save() - messages.success(request, - _(u'L\'auteur {0} a bien été ajouté à la rédaction ' - u'du tutoriel.').format(author.username)) - - # send msg to new author - - msg = ( - _(u'Bonjour **{0}**,\n\n' - u'Tu as été ajouté comme auteur du tutoriel [{1}]({2}).\n' - u'Tu peux retrouver ce tutoriel en [cliquant ici]({3}), ou *via* le lien "En rédaction" du menu ' - u'"Tutoriels" sur la page de ton profil.\n\n' - u'Tu peux maintenant commencer à rédiger !').format( - author.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url(), - settings.ZDS_APP['site']['url'] + reverse("zds.member.views.tutorials")) - ) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - [author], - _(u"Ajout en tant qu'auteur : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - - return redirect(redirect_url) - elif "remove_author" in request.POST: - redirect_url = reverse("zds.tutorial.views.view_tutorial", args=[ - tutorial.pk, - tutorial.slug, - ]) - - # Avoid orphan tutorials - - if tutorial.authors.all().count() <= 1: - raise Http404 - author_pk = request.POST["author"] - author = get_object_or_404(User, pk=author_pk) - tutorial.authors.remove(author) +class DeleteContainerOrExtract(DeleteView): + model = PublishableContent + template_name = None + http_method_names = [u'delete', u'post'] + object = None + content = None + + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + return super(DeleteContainerOrExtract, self).dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + if obj.slug != self.kwargs['slug']: + raise Http404 + return obj - # user can access to gallery + def get_context_data(self, **kwargs): + self.content = self.get_object() + context = super(DeleteContainerOrExtract, self).get_context_data(**kwargs) + context['content'] = self.content.load_version() + container = context['content'] + # get the extract: + if 'parent_container_slug' in self.kwargs: try: - ug = UserGallery.objects.filter(user=author, - gallery=tutorial.gallery) - ug.delete() - except: - ug = None - tutorial.save() - messages.success(request, - _(u"L'auteur {0} a bien été retiré du tutoriel.") - .format(author.username)) - - # send msg to removed author - - msg = ( - _(u'Bonjour **{0}**,\n\n' - u'Tu as été supprimé des auteurs du tutoriel [{1}]({2}). Tant qu\'il ne sera pas publié, tu ne ' - u'pourra plus y accéder.\n').format( - author.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url()) - ) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - [author], - _(u"Suppression des auteurs : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - - return redirect(redirect_url) - elif "activ_beta" in request.POST: - if "version" in request.POST: - tutorial.sha_beta = request.POST['version'] - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id'])\ - .first() - msg = \ - (_(u'Bonjour à tous,\n\n' - u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' - u'J\'aimerais obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' - u'sur la forme, afin de proposer en validation un texte de qualité.' - u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' - u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' - u'\n\nMerci d\'avance pour votre aide').format( - naturaltime(tutorial.create_at), - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) - if topic is None: - forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) - - create_topic(author=request.user, - forum=forum, - title=_(u"[beta][tutoriel]{0}").format(tutorial.title), - subtitle=u"{}".format(tutorial.description), - text=msg, - key=tutorial.pk - ) - tp = Topic.objects.get(key=tutorial.pk) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - private_mp = \ - (_(u'Bonjour {},\n\n' - u'Vous venez de mettre votre tutoriel **{}** en beta. La communauté ' - u'pourra le consulter afin de vous faire des retours ' - u'constructifs avant sa soumission en validation.\n\n' - u'Un sujet dédié pour la beta de votre tutoriel a été ' - u'crée dans le forum et est accessible [ici]({})').format( - request.user.username, - tutorial.title, - settings.ZDS_APP['site']['url'] + tp.get_absolute_url())) - send_mp( - bot, - [request.user], - _(u"Tutoriel en beta : {0}").format(tutorial.title), - "", - private_mp, - False, - ) - else: - msg_up = \ - (_(u'Bonjour,\n\n' - u'La beta du tutoriel est de nouveau active.' - u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' - u'\n\nMerci pour vos relectures').format(tutorial.title, - settings.ZDS_APP['site']['url'] - + tutorial.get_absolute_url_beta())) - unlock_topic(topic, msg) - send_post(topic, msg_up) - - messages.success(request, _(u"La BETA sur ce tutoriel est bien activée.")) + container = container.children_dict[self.kwargs['parent_container_slug']] + except KeyError: + raise Http404 else: - messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être activée.")) - return redirect(tutorial.get_absolute_url_beta()) - elif "update_beta" in request.POST: - if "version" in request.POST: - tutorial.sha_beta = request.POST['version'] - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, - forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() - msg = \ - (_(u'Bonjour à tous,\n\n' - u'J\'ai commencé ({0}) la rédaction d\'un tutoriel dont l\'intitulé est **{1}**.\n\n' - u'J\'aimerai obtenir un maximum de retour sur celui-ci, sur le fond ainsi que ' - u'sur la forme, afin de proposer en validation un texte de qualité.' - u'\n\nSi vous êtes intéressé, cliquez ci-dessous ' - u'\n\n-> [Lien de la beta du tutoriel : {1}]({2}) <-\n\n' - u'\n\nMerci d\'avance pour votre aide').format( - naturaltime(tutorial.create_at), - tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_beta())) - if topic is None: - forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) - - create_topic(author=request.user, - forum=forum, - title=u"[beta][tutoriel]{0}".format(tutorial.title), - subtitle=u"{}".format(tutorial.description), - text=msg, - key=tutorial.pk - ) - else: - msg_up = \ - (_(u'Bonjour, !\n\n' - u'La beta du tutoriel a été mise à jour.' - u'\n\n-> [Lien de la beta du tutoriel : {0}]({1}) <-\n\n' - u'\n\nMerci pour vos relectures').format(tutorial.title, - settings.ZDS_APP['site']['url'] - + tutorial.get_absolute_url_beta())) - unlock_topic(topic, msg) - send_post(topic, msg_up) - messages.success(request, _(u"La BETA sur ce tutoriel a bien été mise à jour.")) + if not isinstance(container, Container): + raise Http404 + + if 'container_slug' in self.kwargs: + try: + container = container.children_dict[self.kwargs['container_slug']] + except KeyError: + raise Http404 else: - messages.error(request, _(u"La BETA sur ce tutoriel n'a malheureusement pas pu être mise à jour.")) - return redirect(tutorial.get_absolute_url_beta()) - elif "desactiv_beta" in request.POST: - tutorial.sha_beta = None - tutorial.save() - topic = Topic.objects.filter(key=tutorial.pk, forum__pk=settings.ZDS_APP['forum']['beta_forum_id']).first() - if topic is not None: - msg = \ - (_(u'Désactivation de la beta du tutoriel **{}**' - u'\n\nPour plus d\'informations envoyez moi un message privé.').format(tutorial.title)) - lock_topic(topic) - send_post(topic, msg) - messages.info(request, _(u"La BETA sur ce tutoriel a bien été désactivée.")) + if not isinstance(container, Container): + raise Http404 - return redirect(tutorial.get_absolute_url()) + to_delete = None + if 'object_slug' in self.kwargs: + try: + to_delete = container.children_dict[self.kwargs['object_slug']] + except KeyError: + raise Http404 - # No action performed, raise 403 + context['to_delete'] = to_delete - raise PermissionDenied + return context + def delete(self, request, *args, **kwargs): + """delete any object, either Extract or Container""" -@can_write_and_read_now -@login_required -def add_tutorial(request): - """'Adds a tutorial.""" + context = self.get_context_data() + to_delete = context['to_delete'] - if request.method == "POST": - form = TutorialForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - - # Creating a tutorial - - tutorial = PublishableContent() - tutorial.title = data["title"] - tutorial.description = data["description"] - tutorial.type = data["type"] - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - tutorial.images = "images" - if "licence" in data and data["licence"] != "": - lc = Licence.objects.filter(pk=data["licence"]).all()[0] - tutorial.licence = lc - else: - tutorial.licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) + sha = to_delete.repo_delete() - # add create date + # then save + self.content.sha_draft = sha + self.content.update_date = datetime.now() + self.content.save() - tutorial.create_at = datetime.now() - tutorial.pubdate = datetime.now() + success_url = '' + if isinstance(to_delete, Extract): + success_url = to_delete.container.get_absolute_url() + else: + success_url = to_delete.parent.get_absolute_url() - # Creating the gallery - - gal = Gallery() - gal.title = data["title"] - gal.slug = slugify(data["title"]) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = gal - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - tutorial.image = img - tutorial.save() - - # Add subcategories on tutorial - - for subcat in form.cleaned_data["subcategory"]: - tutorial.subcategory.add(subcat) - - # Add helps if needed - for helpwriting in form.cleaned_data["helps"]: - tutorial.helps.add(helpwriting) - - # We need to save the tutorial before changing its author list - # since it's a many-to-many relationship - - tutorial.authors.add(request.user) - - # If it's a small tutorial, create its corresponding chapter - - if tutorial.type == "MINI": - chapter = Container() - chapter.tutorial = tutorial - chapter.save() - tutorial.save() - maj_repo_tuto( - request, - new_slug_path=tutorial.get_repo_path(), - tuto=tutorial, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - return redirect(tutorial.get_absolute_url()) - else: - form = TutorialForm( - initial={ - 'licence': Licence.objects.get(pk=settings.ZDS_APP['tutorial']['default_license_pk']) - } - ) - return render(request, "tutorial/tutorial/new.html", {"form": form}) + return redirect(success_url) -@can_write_and_read_now -@login_required -def edit_tutorial(request): - """Edit a tutorial.""" +class ArticleList(ListView): + """ + Displays the list of published articles. + """ + context_object_name = 'articles' + paginate_by = settings.ZDS_APP['tutorial']['content_per_page'] + type = "ARTICLE" + template_name = 'article/index.html' + tag = None - # Retrieve current tutorial; + def get_queryset(self): + """ + Filter the content to obtain the list of only articles. If tag parameter is provided, only articles + which have this category will be listed. + :return: list of articles + """ + if self.request.GET.get('tag') is not None: + self.tag = get_object_or_404(SubCategory, title=self.request.GET.get('tag')) + query_set = PublishableContent.objects.filter(type=self.type).filter(sha_public__isnull=False)\ + .exclude(sha_public='') + if self.tag is not None: + query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') - try: - tutorial_pk = request.GET["tutoriel"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + def get_context_data(self, **kwargs): + context = super(ArticleList, self).get_context_data(**kwargs) + context['tag'] = self.tag + # TODO in database, the information concern the draft, so we have to make stuff here ! + return context - # If the user isn't an author of the tutorial or isn't in the staff, he - # hasn't permission to execute this method: - if request.user not in tutorial.authors.all(): - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - introduction = os.path.join(tutorial.get_repo_path(), "introduction.md") - conclusion = os.path.join(tutorial.get_repo_path(), "conclusion.md") - if request.method == "POST": - form = TutorialForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = TutorialForm(initial={ - "title": tutorial.title, - "type": tutorial.type, - "licence": tutorial.licence, - "description": tutorial.description, - "subcategory": tutorial.subcategory.all(), - "introduction": tutorial.get_introduction(), - "conclusion": tutorial.get_conclusion(), - "helps": tutorial.helps.all(), - }) - return render(request, "tutorial/tutorial/edit.html", - { - "tutorial": tutorial, "form": form, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True - }) - old_slug = tutorial.get_repo_path() - tutorial.title = data["title"] - tutorial.description = data["description"] - if "licence" in data and data["licence"] != "": - lc = Licence.objects.filter(pk=data["licence"]).all()[0] - tutorial.licence = lc - else: - tutorial.licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) - - # add MAJ date - - tutorial.update = datetime.now() - - # MAJ gallery - - gal = Gallery.objects.filter(pk=tutorial.gallery.pk) - gal.update(title=data["title"]) - gal.update(slug=slugify(data["title"])) - gal.update(update=datetime.now()) - - # MAJ image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = tutorial.gallery - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - tutorial.image = img - tutorial.save() - tutorial.update_children() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - - maj_repo_tuto( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - tuto=tutorial, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - - tutorial.subcategory.clear() - for subcat in form.cleaned_data["subcategory"]: - tutorial.subcategory.add(subcat) - - tutorial.helps.clear() - for help in form.cleaned_data["helps"]: - tutorial.helps.add(help) - - tutorial.save() - return redirect(tutorial.get_absolute_url()) - else: - json = tutorial.load_json_for_public(tutorial.sha_draft) - if "licence" in json: - licence = json['licence'] - else: - licence = Licence.objects.get( - pk=settings.ZDS_APP['tutorial']['default_license_pk'] - ) - form = TutorialForm(initial={ - "title": json["title"], - "type": json["type"], - "licence": licence, - "description": json["description"], - "subcategory": tutorial.subcategory.all(), - "introduction": tutorial.get_introduction(), - "conclusion": tutorial.get_conclusion(), - "helps": tutorial.helps.all(), - }) - return render(request, "tutorial/tutorial/edit.html", - {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])}) +class TutorialList(ArticleList): + """Displays the list of published tutorials.""" -# Parts. + context_object_name = 'tutorials' + type = "TUTORIAL" + template_name = 'tutorialv2/index.html' -@login_required -def view_part( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, -): - """Display a part.""" +class TutorialWithHelp(TutorialList): + """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta + for more documentation, have a look to ZEP 03 specification (fr)""" + context_object_name = 'tutorials' + template_name = 'tutorialv2/help.html' - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - try: - sha = request.GET["version"] - except KeyError: - sha = tutorial.sha_draft + def get_queryset(self): + """get only tutorial that need help and handle filtering if asked""" + query_set = PublishableContent.objects\ + .annotate(total=Count('helps'), shasize=Count('sha_beta')) \ + .filter((Q(sha_beta__isnull=False) & Q(shasize__gt=0)) | Q(total__gt=0)) \ + .all() + try: + type_filter = self.request.GET.get('type') + query_set = query_set.filter(helps_title__in=[type_filter]) + except KeyError: + # if no filter, no need to change + pass + return query_set - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() + def get_context_data(self, **kwargs): + """Add all HelpWriting objects registered to the context so that the template can use it""" + context = super(TutorialWithHelp, self).get_context_data(**kwargs) + context['helps'] = HelpWriting.objects.all() + return context - # Only authors of the tutorial and staff can view tutorial in offline. +# TODO ArticleWithHelp - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - final_part = None +class DisplayDiff(DetailView): + """Display the difference between two version of a content. + Reference is always HEAD and compared version is a GET query parameter named sha + this class has no reason to be adapted to any content type""" + model = PublishableContent + template_name = "tutorial/diff.html" + context_object_name = "tutorial" + + def get_object(self, queryset=None): + return get_object_or_404(PublishableContent, pk=self.kwargs['content_pk']) - # find the good manifest file + def get_context_data(self, **kwargs): - repo = Repo(tutorial.get_repo_path()) - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha=sha) - - parts = mandata["parts"] - find = False - cpt_p = 1 - for part in parts: - if part_pk == str(part["pk"]): - find = True - part["tutorial"] = tutorial - part["path"] = tutorial.get_repo_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - part["intro"] = get_blob(repo.commit(sha).tree, part["introduction"]) - part["conclu"] = get_blob(repo.commit(sha).tree, part["conclusion"]) - cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_repo_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_repo_path() - cpt_e += 1 - cpt_c += 1 - final_part = part - break - cpt_p += 1 - - # if part can't find - if not find: - raise Http404 + context = super(DisplayDiff, self).get_context_data(**kwargs) - if tutorial.js_support: - is_js = "js" - else: - is_js = "" + try: + sha = self.request.GET.get("sha") + except KeyError: + sha = self.get_object().sha_draft + + if self.request.user not in context[self.context_object_name].authors.all(): + if not self.request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied + # open git repo and find diff between displayed version and head + repo = Repo(context[self.context_object_name].get_repo_path()) + current_version_commit = repo.commit(sha) + diff_with_head = current_version_commit.diff("HEAD~1") + context["path_add"] = diff_with_head.iter_change_type("A") + context["path_ren"] = diff_with_head.iter_change_type("R") + context["path_del"] = diff_with_head.iter_change_type("D") + context["path_maj"] = diff_with_head.iter_change_type("M") + return context - return render(request, "tutorial/part/view.html", - {"tutorial": mandata, - "part": final_part, - "version": sha, - "is_js": is_js}) +class DisplayOnlineContent(DisplayContent): + """Display online tutorial""" + type = "TUTORIAL" + template_name = "tutorial/view_online.html" -def view_part_online( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, -): - """Display a part.""" + def get_forms(self, context, content): - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if not tutorial.in_public(): - raise Http404 + # Build form to send a note for the current tutorial. + context['form'] = NoteForm(content, self.request.user) + + def compatibility_parts(self, content, repo, sha, dictionary, cpt_p): + dictionary["tutorial"] = content + dictionary["path"] = content.get_repo_path() + dictionary["slug"] = slugify(dictionary["title"]) + dictionary["position_in_tutorial"] = cpt_p - # find the good manifest file - - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - mandata["update"] = tutorial.update - - mandata["get_parts"] = mandata["parts"] - parts = mandata["parts"] - cpt_p = 1 - final_part = None - find = False - for part in parts: - part["tutorial"] = mandata - part["path"] = tutorial.get_path() - part["slug"] = slugify(part["title"]) - part["position_in_tutorial"] = cpt_p - if part_pk == str(part["pk"]): - find = True - intro = open(os.path.join(tutorial.get_prod_path(), - part["introduction"] + ".html"), "r") - part["intro"] = intro.read() - intro.close() - conclu = open(os.path.join(tutorial.get_prod_path(), - part["conclusion"] + ".html"), "r") - part["conclu"] = conclu.read() - conclu.close() - final_part = part cpt_c = 1 - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_path() + for chapter in dictionary["chapters"]: + chapter["part"] = dictionary chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c chapter["position_in_tutorial"] = cpt_c * cpt_p - if part_slug == slugify(part["title"]): - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_prod_path() - cpt_e += 1 + self.compatibility_chapter(content, repo, sha, chapter) cpt_c += 1 - part["get_chapters"] = part["chapters"] - cpt_p += 1 - - # if part can't find - if not find: - raise Http404 - - return render(request, "tutorial/part/view_online.html", {"part": final_part}) + def compatibility_chapter(self, content, repo, sha, dictionary): + """enable compatibility with old version of mini tutorial and chapter implementations""" + dictionary["path"] = content.get_prod_path() + dictionary["type"] = self.type + dictionary["pk"] = Container.objects.get(parent=content).pk # TODO : find better name + dictionary["intro"] = open(os.path.join(content.get_prod_path(), "introduction.md" + ".html"), "r") + dictionary["conclu"] = open(os.path.join(content.get_prod_path(), "conclusion.md" + ".html"), "r") + # load extracts + cpt = 1 + for ext in dictionary["extracts"]: + ext["position_in_chapter"] = cpt + ext["path"] = content.get_prod_path() + text = open(os.path.join(content.get_prod_path(), ext["text"] + ".html"), "r") + ext["txt"] = text.read() + cpt += 1 -@can_write_and_read_now -@login_required -def add_part(request): - """Add a new part.""" - - try: - tutorial_pk = request.GET["tutoriel"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + def get_context_data(self, **kwargs): + content = self.get_object() + # If the tutorial isn't online, we raise 404 error. + if not content.in_public(): + raise Http404 + self.sha = content.sha_public + context = super(DisplayOnlineContent, self).get_context_data(**kwargs) - # Make sure it's a big tutorial, just in case + context["tutorial"]["update"] = content.update + context["tutorial"]["get_note_count"] = content.get_note_count() - if not tutorial.type == "BIG": - raise Http404 + if self.request.user.is_authenticated(): + # If the user is authenticated, he may want to tell the world how cool the content is + # We check if he can post a not or not with + # antispam filter. + context['tutorial']['antispam'] = content.antispam() - # Make sure the user belongs to the author list + # If the user has never read this before, we mark this tutorial read. + if never_read(content): + mark_read(content) - if request.user not in tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = PartForm(request.POST) - if form.is_valid(): - data = form.data - part = Container() - part.tutorial = tutorial - part.title = data["title"] - part.position_in_tutorial = tutorial.get_parts().count() + 1 - part.save() - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part.save() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - - maj_repo_part( - request, - new_slug_path=new_slug, - part=part, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - if "submit_continue" in request.POST: - form = PartForm() - messages.success(request, - _(u'La partie « {0} » a été ajoutée ' - u'avec succès.').format(part.title)) - else: - return redirect(part.get_absolute_url()) - else: - form = PartForm() - return render(request, "tutorial/part/new.html", {"tutorial": tutorial, - "form": form}) + # Find all notes of the tutorial. + notes = ContentReaction.objects.filter(related_content__pk=content.pk).order_by("position").all() -@can_write_and_read_now -@login_required -def modify_part(request): - """Modifiy the given part.""" + # Retrieve pk of the last note. If there aren't notes for the tutorial, we + # initialize this last note at 0. - if not request.method == "POST": - raise Http404 - part_pk = request.POST["part"] - part = get_object_or_404(Container, pk=part_pk) + last_note_pk = 0 + if content.last_note: + last_note_pk = content.last_note.pk - # Make sure the user is allowed to do that + # Handle pagination - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if "move" in request.POST: + paginator = Paginator(notes, settings.ZDS_APP['forum']['posts_per_page']) try: - new_pos = int(request.POST["move_target"]) + page_nbr = int(self.request.GET.get("page")) + except KeyError: + page_nbr = 1 except ValueError: - # Invalid conversion, maybe the user played with the move button - return redirect(part.tutorial.get_absolute_url()) - - move(part, new_pos, "position_in_tutorial", "tutorial", "get_parts") - part.save() - - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) - - maj_repo_tuto(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - tuto=part.tutorial, - action="maj", - msg=_(u"Déplacement de la partie {} ").format(part.title)) - elif "delete" in request.POST: - # Delete all chapters belonging to the part - - Container.objects.all().filter(part=part).delete() - - # Move other parts - - old_pos = part.position_in_tutorial - for tut_p in part.tutorial.get_parts(): - if old_pos <= tut_p.position_in_tutorial: - tut_p.position_in_tutorial = tut_p.position_in_tutorial - 1 - tut_p.save() - old_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - maj_repo_part(request, old_slug_path=old_slug, part=part, action="del") - - new_slug_tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], part.tutorial.get_phy_slug()) - # Actually delete the part - part.delete() - - maj_repo_tuto(request, - old_slug_path=new_slug_tuto_path, - new_slug_path=new_slug_tuto_path, - tuto=part.tutorial, - action="maj", - msg=_(u"Suppression de la partie {} ").format(part.title)) - return redirect(part.tutorial.get_absolute_url()) + raise Http404 + + try: + notes = paginator.page(page_nbr) + except PageNotAnInteger: + notes = paginator.page(1) + except EmptyPage: + raise Http404 + res = [] + if page_nbr != 1: -@can_write_and_read_now -@login_required -def edit_part(request): - """Edit the given part.""" + # Show the last note of the previous page - try: - part_pk = int(request.GET["partie"]) - except KeyError: - raise Http404 - except ValueError: - raise Http404 + last_page = paginator.page(page_nbr - 1).object_list + last_note = last_page[len(last_page) - 1] + res.append(last_note) + for note in notes: + res.append(note) - part = get_object_or_404(Container, pk=part_pk) - introduction = os.path.join(part.get_path(), "introduction.md") - conclusion = os.path.join(part.get_path(), "conclusion.md") - # Make sure the user is allowed to do that + context['notes'] = res + context['last_note_pk'] = last_note_pk + context['pages'] = paginator_range(page_nbr, paginator.num_pages) + context['nb'] = page_nbr - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = PartForm(request.POST) - if form.is_valid(): - data = form.data - # avoid collision - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = PartForm({"title": part.title, - "introduction": part.get_introduction(), - "conclusion": part.get_conclusion()}) - return render(request, "tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True, - "form": form - }) - # Update title and his slug. - - part.title = data["title"] - old_slug = part.get_path() - part.save() - - # Update path for introduction and conclusion. - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part.save() - part.update_children() - - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - - maj_repo_part( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - part=part, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(part.get_absolute_url()) - else: - form = PartForm({"title": part.title, - "introduction": part.get_introduction(), - "conclusion": part.get_conclusion()}) - return render(request, "tutorial/part/edit.html", - { - "part": part, - "last_hash": compute_hash([introduction, conclusion]), - "form": form - }) + +class DisplayOnlineArticle(DisplayOnlineContent): + type = "ARTICLE" -# Containers. +# Staff actions. +@permission_required("tutorial.change_tutorial", raise_exception=True) @login_required -def view_chapter( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, - chapter_pk, - chapter_slug, -): - """View chapter.""" +def list_validation(request): + """Display tutorials list in validation.""" - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + # Retrieve type of the validation. Default value is all validations. try: - sha = request.GET["version"] + type = request.GET["type"] except KeyError: - sha = tutorial.sha_draft - - is_beta = sha == tutorial.sha_beta and tutorial.in_beta() - - # Only authors of the tutorial and staff can view tutorial in offline. + type = None - if request.user not in tutorial.authors.all() and not is_beta: - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied + # Get subcategory to filter validations. - # find the good manifest file + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None - repo = Repo(tutorial.get_repo_path()) - manifest = get_blob(repo.commit(sha).tree, "manifest.json") - mandata = json_reader.loads(manifest) - tutorial.load_dic(mandata, sha=sha) - - parts = mandata["parts"] - cpt_p = 1 - final_chapter = None - chapter_tab = [] - final_position = 0 - find = False - for part in parts: - cpt_c = 1 - part["slug"] = slugify(part["title"]) - part["get_absolute_url"] = reverse( - "zds.tutorial.views.view_part", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"]]) - part["tutorial"] = tutorial - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_repo_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url"] = part["get_absolute_url"] \ - + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) - if chapter_pk == str(chapter["pk"]): - find = True - chapter["intro"] = get_blob(repo.commit(sha).tree, - chapter["introduction"]) - chapter["conclu"] = get_blob(repo.commit(sha).tree, - chapter["conclusion"]) - - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_repo_path() - ext["txt"] = get_blob(repo.commit(sha).tree, ext["text"]) - cpt_e += 1 - chapter_tab.append(chapter) - if chapter_pk == str(chapter["pk"]): - final_chapter = chapter - final_position = len(chapter_tab) - 1 - cpt_c += 1 - cpt_p += 1 + # Orphan validation. There aren't validator attached to the validations. - # if chapter can't find - if not find: - raise Http404 + if type == "orphan": + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=True, + status="PENDING").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=True, + status="PENDING", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() + elif type == "reserved": - prev_chapter = (chapter_tab[final_position - 1] if final_position - > 0 else None) - next_chapter = (chapter_tab[final_position + 1] if final_position + 1 - < len(chapter_tab) else None) + # Reserved validation. There are a validator attached to the + # validations. - if tutorial.js_support: - is_js = "js" + if subcategory is None: + validations = Validation.objects.filter( + validator__isnull=False, + status="PENDING_V").order_by("date_proposition").all() + else: + validations = Validation.objects.filter(validator__isnull=False, + status="PENDING_V", + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition") \ + .all() else: - is_js = "" - - return render(request, "tutorial/chapter/view.html", { - "tutorial": mandata, - "chapter": final_chapter, - "prev": prev_chapter, - "next": next_chapter, - "version": sha, - "is_js": is_js - }) - - -def view_chapter_online( - request, - tutorial_pk, - tutorial_slug, - part_pk, - part_slug, - chapter_pk, - chapter_slug, -): - """View chapter.""" - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - if not tutorial.in_public(): - raise Http404 + # Default, we display all validations. - # find the good manifest file + if subcategory is None: + validations = Validation.objects.filter( + Q(status="PENDING") | Q(status="PENDING_V")).order_by("date_proposition").all() + else: + validations = Validation.objects.filter(Q(status="PENDING") + | Q(status="PENDING_V" + )).filter(tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition")\ + .all() + return render(request, "tutorial/validation/index.html", + {"validations": validations}) - mandata = tutorial.load_json_for_public() - tutorial.load_dic(mandata, sha=tutorial.sha_public) - mandata["update"] = tutorial.update - mandata['get_parts'] = mandata["parts"] - parts = mandata["parts"] - cpt_p = 1 - final_chapter = None - chapter_tab = [] - final_position = 0 +@permission_required("tutorial.change_tutorial", raise_exception=True) +@login_required +@require_POST +def reservation(request, validation_pk): + """Display tutorials list in validation.""" - find = False - for part in parts: - cpt_c = 1 - part["slug"] = slugify(part["title"]) - part["get_absolute_url_online"] = reverse( - "zds.tutorial.views.view_part_online", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"]]) - part["tutorial"] = mandata - part["position_in_tutorial"] = cpt_p - part["get_chapters"] = part["chapters"] - for chapter in part["chapters"]: - chapter["part"] = part - chapter["path"] = tutorial.get_prod_path() - chapter["slug"] = slugify(chapter["title"]) - chapter["type"] = "BIG" - chapter["position_in_part"] = cpt_c - chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url_online"] = part[ - "get_absolute_url_online"] + "{0}/{1}/".format(chapter["pk"], chapter["slug"]) - if chapter_pk == str(chapter["pk"]): - find = True - intro = open( - os.path.join( - tutorial.get_prod_path(), - chapter["introduction"] + - ".html"), - "r") - chapter["intro"] = intro.read() - intro.close() - conclu = open( - os.path.join( - tutorial.get_prod_path(), - chapter["conclusion"] + - ".html"), - "r") - chapter["conclu"] = conclu.read() - conclu.close() - cpt_e = 1 - for ext in chapter["extracts"]: - ext["chapter"] = chapter - ext["position_in_chapter"] = cpt_e - ext["path"] = tutorial.get_path() - text = open(os.path.join(tutorial.get_prod_path(), - ext["text"] + ".html"), "r") - ext["txt"] = text.read() - text.close() - cpt_e += 1 - else: - intro = None - conclu = None - chapter_tab.append(chapter) - if chapter_pk == str(chapter["pk"]): - final_chapter = chapter - final_position = len(chapter_tab) - 1 - cpt_c += 1 - cpt_p += 1 + validation = get_object_or_404(Validation, pk=validation_pk) + if validation.validator: + validation.validator = None + validation.date_reserve = None + validation.status = "PENDING" + validation.save() + messages.info(request, _(u"Le tutoriel n'est plus sous réserve.")) + return redirect(reverse("zds.tutorial.views.list_validation")) + else: + validation.validator = request.user + validation.date_reserve = datetime.now() + validation.status = "PENDING_V" + validation.save() + messages.info(request, + _(u"Le tutoriel a bien été \ + réservé par {0}.").format(request.user.username)) + return redirect( + validation.content.get_absolute_url() + + "?version=" + validation.version + ) - # if chapter can't find - if not find: - raise Http404 - prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None) - next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None) +@login_required +def history(request, tutorial_pk, tutorial_slug): + """History of the tutorial.""" + + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + if request.user not in tutorial.authors.all(): + if not request.user.has_perm("tutorial.change_tutorial"): + raise PermissionDenied - return render(request, "tutorial/chapter/view_online.html", { - "chapter": final_chapter, - "parts": parts, - "prev": prev_chapter, - "next": next_chapter, - }) + repo = Repo(tutorial.get_repo_path()) + logs = repo.head.reference.log() + logs = sorted(logs, key=attrgetter("time"), reverse=True) + return render(request, "tutorial/tutorial/history.html", + {"tutorial": tutorial, "logs": logs}) -@can_write_and_read_now @login_required -def add_chapter(request): - """Add a new chapter to given part.""" +@permission_required("tutorial.change_tutorial", raise_exception=True) +def history_validation(request, tutorial_pk): + """History of the validation of a tutorial.""" - try: - part_pk = request.GET["partie"] - except KeyError: - raise Http404 - part = get_object_or_404(Container, pk=part_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # Make sure the user is allowed to do that + # Get subcategory to filter validations. - if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - form = ChapterForm(request.POST, request.FILES) - if form.is_valid(): - data = form.data - chapter = Container() - chapter.title = data["title"] - chapter.part = part - chapter.position_in_part = part.get_chapters().count() + 1 - chapter.update_position_in_tutorial() - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = part.tutorial.gallery - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - chapter.image = img - - chapter.save() - if chapter.tutorial: - chapter_path = os.path.join( - os.path.join( - settings.ZDS_APP['tutorial']['repo_path'], - chapter.tutorial.get_phy_slug()), - chapter.get_phy_slug()) - chapter.introduction = os.path.join(chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join(chapter.get_phy_slug(), - "conclusion.md") - else: - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - chapter.introduction = os.path.join( - chapter.part.get_phy_slug(), - chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug(), "conclusion.md") - chapter.save() - maj_repo_chapter( - request, - new_slug_path=chapter_path, - chapter=chapter, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="add", - msg=request.POST.get('msg_commit', None) - ) - if "submit_continue" in request.POST: - form = ChapterForm() - messages.success(request, - _(u'Le chapitre « {0} » a été ajouté ' - u'avec succès.').format(chapter.title)) - else: - return redirect(chapter.get_absolute_url()) + try: + subcategory = get_object_or_404(Category, pk=request.GET["subcategory"]) + except (KeyError, Http404): + subcategory = None + if subcategory is None: + validations = \ + Validation.objects.filter(tutorial__pk=tutorial_pk) \ + .order_by("date_proposition" + ).all() else: - form = ChapterForm() - - return render(request, "tutorial/chapter/new.html", {"part": part, - "form": form}) + validations = Validation.objects.filter(tutorial__pk=tutorial_pk, + tutorial__subcategory__in=[subcategory]) \ + .order_by("date_proposition" + ).all() + return render(request, "tutorial/validation/history.html", + {"validations": validations, "tutorial": tutorial}) @can_write_and_read_now @login_required -def modify_chapter(request): - """Modify the given chapter.""" +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def reject_tutorial(request): + """Staff reject tutorial of an author.""" + + # Retrieve current tutorial; - if not request.method == "POST": - raise Http404 - data = request.POST try: - chapter_pk = request.POST["chapter"] + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - chapter = get_object_or_404(Container, pk=chapter_pk) - - # Make sure the user is allowed to do that - - if request.user not in chapter.get_tutorial().authors.all() and \ - not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if "move" in data: - try: - new_pos = int(request.POST["move_target"]) - except ValueError: - - # User misplayed with the move button - - return redirect(chapter.get_absolute_url()) - move(chapter, new_pos, "position_in_part", "part", "get_chapters") - chapter.update_position_in_tutorial() - chapter.save() - - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug()) - - maj_repo_part(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - part=chapter.part, - action="maj", - msg=_(u"Déplacement du chapitre {}").format(chapter.title)) - messages.info(request, _(u"Le chapitre a bien été déplacé.")) - elif "delete" in data: - old_pos = chapter.position_in_part - old_tut_pos = chapter.position_in_tutorial - - if chapter.part: - parent = chapter.part - else: - parent = chapter.tutorial - - # Move other chapters first - - for tut_c in chapter.part.get_chapters(): - if old_pos <= tut_c.position_in_part: - tut_c.position_in_part = tut_c.position_in_part - 1 - tut_c.save() - maj_repo_chapter(request, chapter=chapter, - old_slug_path=chapter.get_path(), action="del") - - # Then delete the chapter - new_slug_path_part = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug()) - chapter.delete() - - # Update all the position_in_tutorial fields for the next chapters - - for tut_c in \ - Container.objects.filter(position_in_tutorial__gt=old_tut_pos): - tut_c.update_position_in_tutorial() - tut_c.save() + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") - maj_repo_part(request, - old_slug_path=new_slug_path_part, - new_slug_path=new_slug_path_part, - part=chapter.part, - action="maj", - msg=_(u"Suppression du chapitre {}").format(chapter.title)) - messages.info(request, _(u"Le chapitre a bien été supprimé.")) + if request.user == validation.validator: + validation.comment_validator = request.POST["text"] + validation.status = "REJECT" + validation.date_validation = datetime.now() + validation.save() - return redirect(parent.get_absolute_url()) + # Remove sha_validation because we rejected this version of the tutorial. - return redirect(chapter.get_absolute_url()) + tutorial.sha_validation = None + tutorial.pubdate = None + tutorial.save() + messages.info(request, _(u"Le tutoriel a bien été refusé.")) + comment_reject = '\n'.join(['> '+line for line in validation.comment_validator.split('\n')]) + # send feedback + msg = ( + _(u'Désolé, le zeste **{0}** n\'a malheureusement ' + u'pas passé l’étape de validation. Mais ne désespère pas, ' + u'certaines corrections peuvent surement être faite pour ' + u'l’améliorer et repasser la validation plus tard. ' + u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' + u'N\'hésite pas a lui envoyer un petit message pour discuter ' + u'de la décision ou demander plus de détail si tout cela te ' + u'semble injuste ou manque de clarté.') + .format(tutorial.title, + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), + comment_reject)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Refus de Validation : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) + else: + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le refuser.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) @can_write_and_read_now @login_required -def edit_chapter(request): - """Edit the given chapter.""" +@require_POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +def valid_tutorial(request): + """Staff valid tutorial of an author.""" + + # Retrieve current tutorial; try: - chapter_pk = int(request.GET["chapitre"]) + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - except ValueError: - raise Http404 + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_validation).latest("date_proposition") + + if request.user == validation.validator: + (output, err) = mep(tutorial, tutorial.sha_validation) + messages.info(request, output) + messages.error(request, err) + validation.comment_validator = request.POST["text"] + validation.status = "ACCEPT" + validation.date_validation = datetime.now() + validation.save() - chapter = get_object_or_404(Container, pk=chapter_pk) - big = chapter.part - small = chapter.tutorial + # Update sha_public with the sha of validation. We don't update sha_draft. + # So, the user can continue to edit his tutorial in offline. - # Make sure the user is allowed to do that + if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': + tutorial.pubdate = datetime.now() + tutorial.sha_public = validation.version + tutorial.source = request.POST["source"] + tutorial.sha_validation = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été validé.")) - if (big and request.user not in chapter.part.tutorial.authors.all() - or small and request.user not in chapter.tutorial.authors.all())\ - and not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - introduction = os.path.join(chapter.get_path(), "introduction.md") - conclusion = os.path.join(chapter.get_path(), "conclusion.md") - if request.method == "POST": + # send feedback - if chapter.part: - form = ChapterForm(request.POST, request.FILES) - gal = chapter.part.tutorial.gallery - else: - form = EmbdedChapterForm(request.POST, request.FILES) - gal = chapter.tutorial.gallery - if form.is_valid(): - data = form.data - # avoid collision - if content_has_changed([introduction, conclusion], data["last_hash"]): - form = render_chapter_form(chapter) - return render(request, "tutorial/part/edit.html", - { - "chapter": chapter, - "last_hash": compute_hash([introduction, conclusion]), - "new_version": True, - "form": form - }) - chapter.title = data["title"] - - old_slug = chapter.get_path() - chapter.save() - chapter.update_children() - - if chapter.part: - if chapter.tutorial: - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.tutorial.get_phy_slug(), - chapter.get_phy_slug()) - else: - new_slug = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - - # Create image - - if "image" in request.FILES: - img = Image() - img.physical = request.FILES["image"] - img.gallery = gal - img.title = request.FILES["image"] - img.slug = slugify(request.FILES["image"]) - img.pubdate = datetime.now() - img.save() - chapter.image = img - maj_repo_chapter( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - chapter=chapter, - introduction=data["introduction"], - conclusion=data["conclusion"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(chapter.get_absolute_url()) + msg = ( + _(u'Félicitations ! Le zeste [{0}]({1}) ' + u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' + u'peuvent venir l\'éplucher et réagir a son sujet. ' + u'Je te conseille de rester a leur écoute afin ' + u'd\'apporter des corrections/compléments.' + u'Un Tutoriel vivant et a jour est bien plus lu ' + u'qu\'un sujet abandonné !') + .format(tutorial.title, + settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), + validation.validator.username, + settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(),)) + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + tutorial.authors.all(), + _(u"Publication : {0}").format(tutorial.title), + "", + msg, + True, + direct=False, + ) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) else: - form = render_chapter_form(chapter) - return render(request, "tutorial/chapter/edit.html", {"chapter": chapter, - "last_hash": compute_hash([introduction, conclusion]), - "form": form}) + messages.error(request, + _(u"Vous devez avoir réservé ce tutoriel " + u"pour pouvoir le valider.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) +@can_write_and_read_now @login_required -def add_extract(request): - """Add extract.""" - - try: - chapter_pk = int(request.GET["chapitre"]) - except KeyError: - raise Http404 - except ValueError: - raise Http404 - - chapter = get_object_or_404(Container, pk=chapter_pk) - part = chapter.part - - # If part exist, we check if the user is in authors of the tutorial of the - # part or If part doesn't exist, we check if the user is in authors of the - # tutorial of the chapter. - - if part and request.user not in chapter.part.tutorial.authors.all() \ - or not part and request.user not in chapter.tutorial.authors.all(): - - # If the user isn't an author or a staff, we raise an exception. - - if not request.user.has_perm("tutorial.change_tutorial"): - raise PermissionDenied - if request.method == "POST": - data = request.POST +@permission_required("tutorial.change_tutorial", raise_exception=True) +@require_POST +def invalid_tutorial(request, tutorial_pk): + """Staff invalid tutorial of an author.""" - # Using the « preview button » + # Retrieve current tutorial - if "preview" in data: - form = ExtractForm(initial={"title": data["title"], - "text": data["text"], - 'msg_commit': data['msg_commit']}) - return render(request, "tutorial/extract/new.html", - {"chapter": chapter, "form": form}) - else: + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + un_mep(tutorial) + validation = Validation.objects.filter( + tutorial__pk=tutorial_pk, + version=tutorial.sha_public).latest("date_proposition") + validation.status = "PENDING" + validation.date_validation = None + validation.save() - # Save extract. + # Only update sha_validation because contributors can contribute on + # rereading version. - form = ExtractForm(request.POST) - if form.is_valid(): - data = form.data - extract = Extract() - extract.chapter = chapter - extract.position_in_chapter = chapter.get_extract_count() + 1 - extract.title = data["title"] - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract(request, new_slug_path=extract.get_path(), - extract=extract, text=data["text"], - action="add", - msg=request.POST.get('msg_commit', None)) - return redirect(extract.get_absolute_url()) - else: - form = ExtractForm() + tutorial.sha_public = None + tutorial.sha_validation = validation.version + tutorial.pubdate = None + tutorial.save() + messages.success(request, _(u"Le tutoriel a bien été dépublié.")) + return redirect(tutorial.get_absolute_url() + "?version=" + + validation.version) - return render(request, "tutorial/extract/new.html", {"chapter": chapter, - "form": form}) +# User actions on tutorial. @can_write_and_read_now @login_required -def edit_extract(request): - """Edit extract.""" +@require_POST +def ask_validation(request): + """User ask validation for his tutorial.""" + + # Retrieve current tutorial; + try: - extract_pk = request.GET["extrait"] + tutorial_pk = request.POST["tutorial"] except KeyError: raise Http404 - extract = get_object_or_404(Extract, pk=extract_pk) - part = extract.chapter.part - - # If part exist, we check if the user is in authors of the tutorial of the - # part or If part doesn't exist, we check if the user is in authors of the - # tutorial of the chapter. - - if part and request.user \ - not in extract.chapter.part.tutorial.authors.all() or not part \ - and request.user not in extract.chapter.tutorial.authors.all(): + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - # If the user isn't an author or a staff, we raise an exception. + # If the user isn't an author of the tutorial or isn't in the staff, he + # hasn't permission to execute this method: + if request.user not in tutorial.authors.all(): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - if request.method == "POST": - data = request.POST - # Using the « preview button » - - if "preview" in data: - form = ExtractForm(initial={ - "title": data["title"], - "text": data["text"], - 'msg_commit': data['msg_commit'] - }) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, "form": form, - "last_hash": compute_hash([extract.get_path()]) - }) - else: - if content_has_changed([extract.get_path()], data["last_hash"]): - form = ExtractForm(initial={ - "title": extract.title, - "text": extract.get_text(), - 'msg_commit': data['msg_commit']}) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "new_version": True, - "form": form - }) - # Edit extract. - - form = ExtractForm(request.POST) - if form.is_valid(): - data = form.data - old_slug = extract.get_path() - extract.title = data["title"] - extract.text = extract.get_path(relative=True) - - # Use path retrieve before and use it to create the new slug. - extract.save() - new_slug = extract.get_path() - - maj_repo_extract( - request, - old_slug_path=old_slug, - new_slug_path=new_slug, - extract=extract, - text=data["text"], - action="maj", - msg=request.POST.get('msg_commit', None) - ) - return redirect(extract.get_absolute_url()) + old_validation = Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING_V']).first() + if old_validation is not None: + old_validator = old_validation.validator else: - form = ExtractForm({"title": extract.title, - "text": extract.get_text()}) - return render(request, "tutorial/extract/edit.html", - { - "extract": extract, - "last_hash": compute_hash([extract.get_path()]), - "form": form - }) - - -@can_write_and_read_now -def modify_extract(request): - if not request.method == "POST": - raise Http404 - data = request.POST - try: - extract_pk = request.POST["extract"] - except KeyError: - raise Http404 - extract = get_object_or_404(Extract, pk=extract_pk) - chapter = extract.chapter - if "delete" in data: - pos_current_extract = extract.position_in_chapter - for extract_c in extract.chapter.get_extracts(): - if pos_current_extract <= extract_c.position_in_chapter: - extract_c.position_in_chapter = extract_c.position_in_chapter \ - - 1 - extract_c.save() - - # Use path retrieve before and use it to create the new slug. - - old_slug = extract.get_path() - - if extract.chapter.tutorial: - new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug()) - else: - new_slug_path_chapter = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - - maj_repo_extract(request, old_slug_path=old_slug, extract=extract, - action="del") - - maj_repo_chapter(request, - old_slug_path=new_slug_path_chapter, - new_slug_path=new_slug_path_chapter, - chapter=chapter, - action="maj", - msg=_(u"Suppression de l'extrait {}").format(extract.title)) - return redirect(chapter.get_absolute_url()) - elif "move" in data: - try: - new_pos = int(request.POST["move_target"]) - except ValueError: - # Error, the user misplayed with the move button - return redirect(extract.get_absolute_url()) - - move(extract, new_pos, "position_in_chapter", "chapter", "get_extracts") - extract.save() - - if extract.chapter.tutorial: - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug()) - else: - new_slug_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) + old_validator = None + # delete old pending validation + Validation.objects.filter(tutorial__pk=tutorial_pk, + status__in=['PENDING', 'PENDING_V'])\ + .delete() + # We create and save validation object of the tutorial. - maj_repo_chapter(request, - old_slug_path=new_slug_path, - new_slug_path=new_slug_path, - chapter=chapter, - action="maj", - msg=_(u"Déplacement de l'extrait {}").format(extract.title)) - return redirect(extract.get_absolute_url()) - raise Http404 + validation = Validation() + validation.content = tutorial + validation.date_proposition = datetime.now() + validation.comment_authors = request.POST["text"] + validation.version = request.POST["version"] + if old_validator is not None: + validation.validator = old_validator + validation.date_reserve + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + msg = \ + (_(u'Bonjour {0},' + u'Le tutoriel *{1}* que tu as réservé a été mis à jour en zone de validation, ' + u'Pour retrouver les modifications qui ont été faites, je t\'invite à ' + u'consulter l\'historique des versions' + u'\n\n> Merci').format(old_validator.username, tutorial.title)) + send_mp( + bot, + [old_validator], + _(u"Mise à jour de tuto : {0}").format(tutorial.title), + _(u"En validation"), + msg, + False, + ) + validation.save() + validation.content.source = request.POST["source"] + validation.content.sha_validation = request.POST["version"] + validation.content.save() + messages.success(request, + _(u"Votre demande de validation a été envoyée à l'équipe.")) + return redirect(tutorial.get_absolute_url()) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) + tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) def find_tuto(request, pk_user): try: type = request.GET["type"] @@ -2600,560 +1468,6 @@ def replace_real_url(md_text, dict): return md_text -def import_content( - request, - tuto, - images, - logo, -): - tutorial = PublishableContent() - - # add create date - - tutorial.create_at = datetime.now() - tree = etree.parse(tuto) - racine_big = tree.xpath("/bigtuto") - racine_mini = tree.xpath("/minituto") - if len(racine_big) > 0: - - # it's a big tuto - - tutorial.type = "BIG" - tutorial_title = tree.xpath("/bigtuto/titre")[0] - tutorial_intro = tree.xpath("/bigtuto/introduction")[0] - tutorial_conclu = tree.xpath("/bigtuto/conclusion")[0] - tutorial.title = tutorial_title.text.strip() - tutorial.description = tutorial_title.text.strip() - tutorial.images = "images" - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - - # Creating the gallery - - gal = Gallery() - gal.title = tutorial_title.text - gal.slug = slugify(tutorial_title.text) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - tutorial.save() - tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - mapping = upload_images(images, tutorial) - maj_repo_tuto( - request, - new_slug_path=tuto_path, - tuto=tutorial, - introduction=replace_real_url(tutorial_intro.text, mapping), - conclusion=replace_real_url(tutorial_conclu.text, mapping), - action="add", - ) - tutorial.authors.add(request.user) - part_count = 1 - for partie in tree.xpath("/bigtuto/parties/partie"): - part_title = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/titre")[0] - part_intro = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/introduction")[0] - part_conclu = tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/conclusion")[0] - part = Container() - part.title = part_title.text.strip() - part.position_in_tutorial = part_count - part.tutorial = tutorial - part.save() - part.introduction = os.path.join(part.get_phy_slug(), "introduction.md") - part.conclusion = os.path.join(part.get_phy_slug(), "conclusion.md") - part_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - part.tutorial.get_phy_slug(), - part.get_phy_slug()) - part.save() - maj_repo_part( - request, - None, - part_path, - part, - replace_real_url(part_intro.text, mapping), - replace_real_url(part_conclu.text, mapping), - action="add", - ) - chapter_count = 1 - for chapitre in tree.xpath("/bigtuto/parties/partie[" - + str(part_count) - + "]/chapitres/chapitre"): - chapter_title = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/titre")[0] - chapter_intro = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/introduction")[0] - chapter_conclu = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/conclusion")[0] - chapter = Container() - chapter.title = chapter_title.text.strip() - chapter.position_in_part = chapter_count - chapter.position_in_tutorial = part_count * chapter_count - chapter.part = part - chapter.save() - chapter.introduction = os.path.join( - part.get_phy_slug(), - chapter.get_phy_slug(), - "introduction.md") - chapter.conclusion = os.path.join( - part.get_phy_slug(), - chapter.get_phy_slug(), - "conclusion.md") - chapter_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - chapter.part.tutorial.get_phy_slug(), - chapter.part.get_phy_slug(), - chapter.get_phy_slug()) - chapter.save() - maj_repo_chapter( - request, - new_slug_path=chapter_path, - chapter=chapter, - introduction=replace_real_url(chapter_intro.text, - mapping), - conclusion=replace_real_url(chapter_conclu.text, mapping), - action="add", - ) - extract_count = 1 - for souspartie in tree.xpath("/bigtuto/parties/partie[" - + str(part_count) + "]/chapitres/chapitre[" - + str(chapter_count) + "]/sousparties/souspartie"): - extract_title = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/sousparties/souspartie[" + - str(extract_count) + - "]/titre")[0] - extract_text = tree.xpath( - "/bigtuto/parties/partie[" + - str(part_count) + - "]/chapitres/chapitre[" + - str(chapter_count) + - "]/sousparties/souspartie[" + - str(extract_count) + - "]/texte")[0] - extract = Extract() - extract.title = extract_title.text.strip() - extract.position_in_chapter = extract_count - extract.chapter = chapter - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract( - request, - new_slug_path=extract.get_path(), - extract=extract, - text=replace_real_url( - extract_text.text, - mapping), - action="add") - extract_count += 1 - chapter_count += 1 - part_count += 1 - elif len(racine_mini) > 0: - - # it's a mini tuto - - tutorial.type = "MINI" - tutorial_title = tree.xpath("/minituto/titre")[0] - tutorial_intro = tree.xpath("/minituto/introduction")[0] - tutorial_conclu = tree.xpath("/minituto/conclusion")[0] - tutorial.title = tutorial_title.text.strip() - tutorial.description = tutorial_title.text.strip() - tutorial.images = "images" - tutorial.introduction = "introduction.md" - tutorial.conclusion = "conclusion.md" - - # Creating the gallery - - gal = Gallery() - gal.title = tutorial_title.text - gal.slug = slugify(tutorial_title.text) - gal.pubdate = datetime.now() - gal.save() - - # Attach user to gallery - - userg = UserGallery() - userg.gallery = gal - userg.mode = "W" # write mode - userg.user = request.user - userg.save() - tutorial.gallery = gal - tutorial.save() - tuto_path = os.path.join(settings.ZDS_APP['tutorial']['repo_path'], tutorial.get_phy_slug()) - mapping = upload_images(images, tutorial) - maj_repo_tuto( - request, - new_slug_path=tuto_path, - tuto=tutorial, - introduction=replace_real_url(tutorial_intro.text, mapping), - conclusion=replace_real_url(tutorial_conclu.text, mapping), - action="add", - ) - tutorial.authors.add(request.user) - chapter = Container() - chapter.tutorial = tutorial - chapter.save() - extract_count = 1 - for souspartie in tree.xpath("/minituto/sousparties/souspartie"): - extract_title = tree.xpath("/minituto/sousparties/souspartie[" - + str(extract_count) + "]/titre")[0] - extract_text = tree.xpath("/minituto/sousparties/souspartie[" - + str(extract_count) + "]/texte")[0] - extract = Extract() - extract.title = extract_title.text.strip() - extract.position_in_chapter = extract_count - extract.chapter = chapter - extract.save() - extract.text = extract.get_path(relative=True) - extract.save() - maj_repo_extract(request, new_slug_path=extract.get_path(), - extract=extract, - text=replace_real_url(extract_text.text, - mapping), action="add") - extract_count += 1 - - -@can_write_and_read_now -@login_required -@require_POST -def local_import(request): - import_content(request, request.POST["tuto"], request.POST["images"], - request.POST["logo"]) - return redirect(reverse("zds.member.views.tutorials")) - - -@can_write_and_read_now -@login_required -def import_tuto(request): - if request.method == "POST": - # for import tuto - if "import-tuto" in request.POST: - form = ImportForm(request.POST, request.FILES) - if form.is_valid(): - import_content(request, request.FILES["file"], request.FILES["images"], "") - return redirect(reverse("zds.member.views.tutorials")) - else: - form_archive = ImportArchiveForm(user=request.user) - - elif "import-archive" in request.POST: - form_archive = ImportArchiveForm(request.user, request.POST, request.FILES) - if form_archive.is_valid(): - (check, reason) = import_archive(request) - if not check: - messages.error(request, reason) - else: - messages.success(request, reason) - return redirect(reverse("zds.member.views.tutorials")) - else: - form = ImportForm() - - else: - form = ImportForm() - form_archive = ImportArchiveForm(user=request.user) - - profile = get_object_or_404(Profile, user=request.user) - oldtutos = [] - if profile.sdz_tutorial: - olds = profile.sdz_tutorial.strip().split(":") - else: - olds = [] - for old in olds: - oldtutos.append(get_info_old_tuto(old)) - return render( - request, - "tutorial/tutorial/import.html", - {"form": form, "form_archive": form_archive, "old_tutos": oldtutos} - ) - - -# Handling repo -def maj_repo_tuto( - request, - old_slug_path=None, - new_slug_path=None, - tuto=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - if action == "del": - shutil.rmtree(old_slug_path) - else: - if action == "maj": - if old_slug_path != new_slug_path: - shutil.move(old_slug_path, new_slug_path) - repo = Repo(new_slug_path) - msg = _(u"Modification du tutoriel : «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - repo = Repo.init(new_slug_path, bare=False) - msg = _(u"Création du tutoriel «{}» {} {}").format(tuto.title, get_sep(msg), get_text_is_empty(msg)).strip() - repo = Repo(new_slug_path) - index = repo.index - man_path = os.path.join(new_slug_path, "manifest.json") - tuto.dump_json(path=man_path) - index.add(["manifest.json"]) - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - index.add(["introduction.md"]) - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - index.add(["conclusion.md"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - tuto.sha_draft = com.hexsha - tuto.save() - - -def maj_repo_part( - request, - old_slug_path=None, - new_slug_path=None, - part=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - repo = Repo(part.tutorial.get_repo_path()) - index = repo.index - if action == "del": - shutil.rmtree(old_slug_path) - msg = _(u"Suppresion de la partie : «{}»").format(part.title) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - - msg = _(u"Modification de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - msg = _(u"Création de la partie «{}» {} {}").format(part.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - index.add([part.get_phy_slug()]) - man_path = os.path.join(part.tutorial.get_repo_path(), "manifest.json") - part.tutorial.dump_json(path=man_path) - index.add(["manifest.json"]) - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - index.add([os.path.join(part.get_repo_path(relative=True), "introduction.md")]) - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - index.add([os.path.join(part.get_repo_path(relative=True), "conclusion.md" - )]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['litteral_name']) - com_part = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - part.tutorial.sha_draft = com_part.hexsha - part.tutorial.save() - part.save() - - -def maj_repo_chapter( - request, - old_slug_path=None, - new_slug_path=None, - chapter=None, - introduction=None, - conclusion=None, - action=None, - msg=None, -): - - if chapter.tutorial: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.tutorial.get_phy_slug())) - ph = None - else: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], chapter.part.tutorial.get_phy_slug())) - ph = os.path.join(chapter.part.get_phy_slug(), chapter.get_phy_slug()) - index = repo.index - if action == "del": - shutil.rmtree(old_slug_path) - msg = _(u"Suppresion du chapitre : «{}»").format(chapter.title) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - if chapter.tutorial: - msg = _(u"Modification du tutoriel «{}» " - u"{} {}").format(chapter.tutorial.title, get_sep(msg), get_text_is_empty(msg)).strip() - else: - msg = _(u"Modification du chapitre «{}» " - u"{} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg)).strip() - elif action == "add": - if not os.path.exists(new_slug_path): - os.makedirs(new_slug_path, mode=0o777) - msg = _(u"Création du chapitre «{}» {} {}").format(chapter.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - if introduction is not None: - intro = open(os.path.join(new_slug_path, "introduction.md"), "w") - intro.write(smart_str(introduction).strip()) - intro.close() - if conclusion is not None: - conclu = open(os.path.join(new_slug_path, "conclusion.md"), "w") - conclu.write(smart_str(conclusion).strip()) - conclu.close() - if ph is not None: - index.add([ph]) - - # update manifest - - if chapter.tutorial: - man_path = os.path.join(chapter.tutorial.get_repo_path(), "manifest.json") - chapter.tutorial.dump_json(path=man_path) - else: - man_path = os.path.join(chapter.part.tutorial.get_repo_path(), - "manifest.json") - chapter.part.tutorial.dump_json(path=man_path) - index.add(["manifest.json"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com_ch = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - if chapter.tutorial: - chapter.tutorial.sha_draft = com_ch.hexsha - chapter.tutorial.save() - else: - chapter.part.tutorial.sha_draft = com_ch.hexsha - chapter.part.tutorial.save() - chapter.save() - - -def maj_repo_extract( - request, - old_slug_path=None, - new_slug_path=None, - extract=None, - text=None, - action=None, - msg=None, -): - - if extract.chapter.tutorial: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.tutorial.get_phy_slug())) - else: - repo = Repo(os.path.join(settings.ZDS_APP['tutorial']['repo_path'], - extract.chapter.part.tutorial.get_phy_slug())) - index = repo.index - - chap = extract.chapter - - if action == "del": - msg = _(u"Suppression de l'extrait : «{}»").format(extract.title) - extract.delete() - if old_slug_path: - os.remove(old_slug_path) - else: - if action == "maj": - if old_slug_path != new_slug_path: - os.rename(old_slug_path, new_slug_path) - msg = _(u"Mise à jour de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - elif action == "add": - msg = _(u"Création de l'extrait «{}» {} {}").format(extract.title, get_sep(msg), get_text_is_empty(msg))\ - .strip() - ext = open(new_slug_path, "w") - ext.write(smart_str(text).strip()) - ext.close() - index.add([extract.get_repo_path(relative=True)]) - - # update manifest - if chap.tutorial: - man_path = os.path.join(chap.tutorial.get_repo_path(), "manifest.json") - chap.tutorial.dump_json(path=man_path) - else: - man_path = os.path.join(chap.part.tutorial.get_repo_path(), "manifest.json") - chap.part.tutorial.dump_json(path=man_path) - - index.add(["manifest.json"]) - aut_user = str(request.user.pk) - aut_email = str(request.user.email) - if aut_email is None or aut_email.strip() == "": - aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) - com_ex = index.commit( - msg, - author=Actor( - aut_user, - aut_email), - committer=Actor( - aut_user, - aut_email)) - if chap.tutorial: - chap.tutorial.sha_draft = com_ex.hexsha - chap.tutorial.save() - else: - chap.part.tutorial.sha_draft = com_ex.hexsha - chap.part.tutorial.save() - - def insert_into_zip(zip_file, git_tree): """recursively add files from a git_tree to a zip archive""" for blob in git_tree.blobs: # first, add files : From fa370b8bd4d3610b0b1c54fa5c3edbef2e3e1896 Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 1 Feb 2015 10:59:53 +0100 Subject: [PATCH 112/887] =?UTF-8?q?am=C3=A9liore=20les=20perfs=20sde=20l'a?= =?UTF-8?q?ffichage=20+=20quelques=20commentaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 814f39ecdb..65089d153a 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -202,7 +202,12 @@ def get_forms(self, context, content): context["formReject"] = form_reject def get_object(self, queryset=None): - obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) + query_set = PublishableContent.objects\ + .select_related("licence")\ + .prefetch_related("authors")\ + .filter(pk=self.kwargs["pk"]) + + obj = query_set.first() if obj.slug != self.kwargs['slug']: raise Http404 return obj @@ -657,6 +662,7 @@ def dispatch(self, *args, **kwargs): return super(EditExtract, self).dispatch(*args, **kwargs) def get_object(self): + """get the PublishableContent object that the user asked. We double check pk and slug""" obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) if obj.slug != self.kwargs['slug']: raise Http404 @@ -668,7 +674,7 @@ def get_context_data(self, **kwargs): context['content'] = self.content.load_version() container = context['content'] - # get the extract: + # if the extract is at a depth of 3 we get the first parent container if 'parent_container_slug' in self.kwargs: try: container = container.children_dict[self.kwargs['parent_container_slug']] @@ -678,6 +684,7 @@ def get_context_data(self, **kwargs): if not isinstance(container, Container): raise Http404 + # if extract is at depth 2 or 3 we get its direct parent container if 'container_slug' in self.kwargs: try: container = container.children_dict[self.kwargs['container_slug']] @@ -830,6 +837,7 @@ def get_queryset(self): .exclude(sha_public='') if self.tag is not None: query_set = query_set.filter(subcategory__in=[self.tag]) + return query_set.order_by('-pubdate') def get_context_data(self, **kwargs): From fa77a7f1f95646cc039fc6e3bd14cb3573a337ab Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 1 Feb 2015 11:29:47 +0100 Subject: [PATCH 113/887] =?UTF-8?q?derni=C3=A8re=20optimisation=20de=20req?= =?UTF-8?q?u=C3=AAte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/content.html | 14 +++++++------- zds/tutorialv2/views.py | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 20ed95446c..34e7c36b0e 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -38,13 +38,13 @@

    {% endif %} - {% if user in content.authors.all or perms.content.change_content %} + {% if user.user in content.authors.all %} {% include 'tutorialv2/includes/tags_authors.part.html' with content=content add_author=True %} {% else %} {% include 'tutorialv2/includes/tags_authors.part.html' with content=content %} {% endif %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% if content.in_validation %} {% if validation.version == version %} {% if validation.is_pending %} @@ -134,7 +134,7 @@

    {% block sidebar_new %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% if content.sha_draft == version %} {% trans "Éditer" %} @@ -163,7 +163,7 @@

    {% block sidebar_actions %} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {# other action (valid, beta, ...) #} {% endif %} {% endblock %} @@ -171,13 +171,13 @@

    {% block sidebar_blocks %} - {% if perms.content.change_content %} - {# more actions ?!? #} + {% if can_edit and not user in content.authors.all %} + {# more actions only for validators?!? #} {% endif %} {# include "content/includes/summary.part.html" #} - {% if user in content.authors.all or perms.content.change_content %} + {% if can_edit %} {% endif %} diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index 5353ca0590..d6593a96a0 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -132,70 +132,45 @@

    {% block sidebar_actions %} {% if user in tutorial.authors.all or perms.tutorial.change_tutorial %} - {% if chapter.part %} -
  • - - {% blocktrans %} - Déplacer le chapitre - {% endblocktrans %} - - +
  • - {% if chapter.position_in_part > 1 %} - - {% endif %} - - {% if chapter.position_in_part < chapter.part.chapters|length %} - - {% endif %} - - - {% for chapter_mv in chapter.part.chapters %} - {% if chapter != chapter_mv and chapter_mv.position_in_part|add:-1 != chapter.position_in_part %} - - {% endif %} - {% endfor %} - - - {% for chapter_mv in chapter.part.chapters %} - {% if chapter != chapter_mv and chapter_mv.position_in_part|add:1 != chapter.position_in_part %} - - {% endif %} - {% endfor %} - - - - - {% csrf_token %} - - - - - {% endif %} {% endif %} {% endblock %} From e46a7210e26f3e7de2f5d128a2b1cc9c89b37ae3 Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 15 Mar 2015 22:22:51 +0100 Subject: [PATCH 163/887] =?UTF-8?q?debut=20d=C3=A9placement=20inter-partie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 27 ++++++++++++++++++++++++- zds/tutorialv2/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++ zds/tutorialv2/views.py | 6 ++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 4be3ea49e2..59748a7d8a 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -524,7 +524,7 @@ def move_child_down(self, child_slug): def move_child_after(self, child_slug, refer_slug): """ - Change the child's ordering by moving down the child whose slug equals child_slug. + Change the child's ordering by moving the child to be below the reference child. This method **does not** automaticaly update the repo :param child_slug: the child's slug :param refer_slug: the referent child's slug. @@ -536,6 +536,7 @@ def move_child_after(self, child_slug, refer_slug): raise ValueError(refer_slug + " does not exist") child_pos = self.children.index(self.children_dict[child_slug]) refer_pos = self.children.index(self.children_dict[refer_slug]) + # if we want our child to get down (reference is an lower child) if child_pos < refer_pos: for i in range(child_pos, refer_pos): @@ -545,6 +546,30 @@ def move_child_after(self, child_slug, refer_slug): for i in range(child_pos, refer_pos + 1, - 1): self.move_child_up(child_slug) + def move_child_before(self, child_slug, refer_slug): + """ + Change the child's ordering by moving the child to be just above the reference child. . + This method **does not** automaticaly update the repo + :param child_slug: the child's slug + :param refer_slug: the referent child's slug. + :raise ValueError: if one slug does not refer to an existing child + """ + if child_slug not in self.children_dict: + raise ValueError(child_slug + " does not exist") + if refer_slug not in self.children_dict: + raise ValueError(refer_slug + " does not exist") + child_pos = self.children.index(self.children_dict[child_slug]) + refer_pos = self.children.index(self.children_dict[refer_slug]) + + # if we want our child to get down (reference is an lower child) + if child_pos < refer_pos: + for i in range(child_pos, refer_pos - 1): + self.move_child_down(child_slug) + elif child_pos > refer_pos: + # if we want our child to get up (reference is an upper child) + for i in range(child_pos, refer_pos, - 1): + self.move_child_up(child_slug) + class Extract: """ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index d9990bbbf3..0f618b43ca 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -6,6 +6,15 @@ from zds.utils import get_current_user +class TooDeepContainerError(ValueError): + """ + Exception used to represent the fact you can't add a container to a level greater than two + """ + + def __init__(self, *args, **kwargs): + super(TooDeepContainerError, self).__init__(*args, **kwargs) + + def search_container_or_404(base_content, kwargs_array): """ :param base_content: the base Publishable content we will use to retrieve the container @@ -122,3 +131,37 @@ def mark_read(content): content=content, user=get_current_user()) a.save() + + +def try_adopt_new_child(adoptive_parent_full_path, child, root): + """ + Try the adoptive parent to take the responsability of the child + :param parent_full_path: + :param child_slug: + :param root: + :raise Http404: if adoptive_parent_full_path is not found on root hierarchy + :raise TypeError: if the adoptive parent is not allowed to adopt the child due to its type + :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent + :return: + """ + splitted = adoptive_parent_full_path.split('/') + if len(splitted) == 1: + container = root + elif len(splitted) == 2: + container = search_container_or_404(root, {'parent_container_slug': splitted[1]}) + elif len(splitted) == 3: + container = search_container_or_404(root, + {'parent_container_slug': splitted[1], + 'container_slug': splitted[2]}) + else: + raise Http404 + if isinstance(child, Extract): + if not container.can_add_extract(): + raise TypeError + # Todo : handle file deplacement + if isinstance(child, Container): + if not container.can_add_container(): + raise TypeError + if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: + raise TooDeepContainerError + # Todo: handle dir deplacement diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8bc8c87755..8aca85012b 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1024,6 +1024,12 @@ def form_valid(self, form): parent.move_child_up(child_slug) if form.data['moving_method'] == MoveElementForm.MOVE_DOWN: parent.move_child_down(child_slug) + if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: + target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] + parent.move_child_after(child_slug, target) + if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: + target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] + parent.move_child_before(child_slug, target) versioned.dump_json() parent.repo_update(parent.title, From 73358f1e2e2f56f354922e77937df36cee4f23f1 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 17 Mar 2015 16:53:17 +0100 Subject: [PATCH 164/887] adoption d'un enfant --- zds/tutorialv2/models.py | 16 ++++++++++------ zds/tutorialv2/utils.py | 7 +++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 59748a7d8a..cca2ed1c73 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -460,9 +460,10 @@ def repo_add_extract(self, title, text, commit_message=''): return cm.hexsha - def repo_delete(self, commit_message=''): + def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided + :param do_commit: tells if we have to commit the change now or let the outter program do it :return: commit sha """ path = self.get_path(relative=True) @@ -482,9 +483,10 @@ def repo_delete(self, commit_message=''): if commit_message == '': commit_message = u'Suppression du conteneur « {} »'.format(self.title) - cm = repo.index.commit(commit_message, **get_commit_author()) + if do_commit: + cm = repo.index.commit(commit_message, **get_commit_author()) - self.top_container().sha_draft = cm.hexsha + self.top_container().sha_draft = cm.hexsha return cm.hexsha @@ -710,9 +712,10 @@ def repo_update(self, title, text, commit_message=''): return cm.hexsha - def repo_delete(self, commit_message=''): + def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided + :param do_commit: tells if we have to commit the change now or let the outter program do it :return: commit sha """ path = self.get_path(relative=True) @@ -732,9 +735,10 @@ def repo_delete(self, commit_message=''): if commit_message == '': commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) - cm = repo.index.commit(commit_message, **get_commit_author()) + if do_commit: + cm = repo.index.commit(commit_message, **get_commit_author()) - self.container.top_container().sha_draft = cm.hexsha + self.container.top_container().sha_draft = cm.hexsha return cm.hexsha diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 0f618b43ca..52fc96cc20 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -158,10 +158,13 @@ def try_adopt_new_child(adoptive_parent_full_path, child, root): if isinstance(child, Extract): if not container.can_add_extract(): raise TypeError - # Todo : handle file deplacement + child.repo_delete('', False) + container.add_extract(child, generate_slug=False) if isinstance(child, Container): if not container.can_add_container(): raise TypeError if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - # Todo: handle dir deplacement + child.repo_delete('', False) + container.add_container(child) + From e18d072b5e9907c76e1d2dac178ea3414df9f6d1 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Fri, 20 Mar 2015 14:06:31 +0100 Subject: [PATCH 165/887] =?UTF-8?q?d=C3=A9placement=20'avant=20XXX'=20impl?= =?UTF-8?q?=C3=A9ment=C3=A9=20et=20test=C3=A9=20quand=20ils=20sont=20fr?= =?UTF-8?q?=C3=A8res?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 10 ++++++++++ zds/tutorialv2/tests/tests_views.py | 29 +++++++++++++++++++++++++++++ zds/tutorialv2/utils.py | 21 +++++++++------------ zds/tutorialv2/views.py | 15 +++++++++++++-- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index cca2ed1c73..3e5d9f08fb 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -130,6 +130,16 @@ def get_tree_depth(self): depth += 1 return depth + def has_child_with_path(self, child_path): + """ + Check that the given path represent the full path + of a child of this container. + :param child_path: the full path (/maincontainer/subc1/subc2/childslug) we want to check + """ + if self.get_path(True) not in child_path: + return False + return child_path.replace(self.get_path(True), "").replace("/", "") in self.children_dict + def top_container(self): """ :return: Top container (for which parent is `None`) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index cf4ebf7e95..09e01cf154 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -769,6 +769,35 @@ def test_move_up_extract(self): follow=False) self.assertEqual(result.status_code, 403) + def test_move_before(self): + # test 1 : move extract after a sibling + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.extract2 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + self.extract3 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + old_sha = tuto.sha_draft + # test moving smoothly + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.extract3.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] + self.assertEqual(self.extract2.slug, extract.slug) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 52fc96cc20..bb9c644201 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -19,10 +19,17 @@ def search_container_or_404(base_content, kwargs_array): """ :param base_content: the base Publishable content we will use to retrieve the container :param kwargs_array: an array that may contain `parent_container_slug` and `container_slug` keys + or the string representation :return: the Container object we were searching for :raise Http404 if no suitable container is found """ container = None + if isinstance(kwargs_array, str): + dic = {} + dic["parent_container_slug"] = kwargs_array.split("/")[0] + if len(kwargs_array.split("/")) == 2: + dic["container_slug"] = kwargs_array.split("/")[1] + kwargs_array = dic if 'parent_container_slug' in kwargs_array: try: @@ -133,7 +140,7 @@ def mark_read(content): a.save() -def try_adopt_new_child(adoptive_parent_full_path, child, root): +def try_adopt_new_child(adoptive_parent, child): """ Try the adoptive parent to take the responsability of the child :param parent_full_path: @@ -144,17 +151,7 @@ def try_adopt_new_child(adoptive_parent_full_path, child, root): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - splitted = adoptive_parent_full_path.split('/') - if len(splitted) == 1: - container = root - elif len(splitted) == 2: - container = search_container_or_404(root, {'parent_container_slug': splitted[1]}) - elif len(splitted) == 3: - container = search_container_or_404(root, - {'parent_container_slug': splitted[1], - 'container_slug': splitted[2]}) - else: - raise Http404 + container = adoptive_parent if isinstance(child, Extract): if not container.can_add_extract(): raise TypeError diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8aca85012b..15ab388a65 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1026,10 +1026,21 @@ def form_valid(self, form): parent.move_child_down(child_slug) if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] - parent.move_child_after(child_slug, target) + if not parent.has_child_with_path(target): + target_parent = search_container_or_404(versionned, target.split("/")[1:-2]) + child = target_parent.children_dict[target.split("/")[-1]] + try_adopt_new_child(target_parent, parent[child_slug]) + target_parent.move_child_after(child_slug, target) if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] - parent.move_child_before(child_slug, target) + if not parent.has_child_with_path(target): + + target_parent = search_container_or_404(versioned, target.split("/")[1:-2]) + if target.split("/")[-1] not in target_parent: + raise Http404 + child = target_parent.children_dict[target.split("/")[-1]] + try_adopt_new_child(target_parent, parent[child_slug]) + parent.move_child_before(child_slug, target.split("/")[-1]) versioned.dump_json() parent.repo_update(parent.title, From 428ad210c21c2d4926bddd166c07563d6eacf5cc Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Mon, 23 Mar 2015 14:15:23 +0100 Subject: [PATCH 166/887] =?UTF-8?q?d=C3=A9placement=20dvers=20un=20nouveau?= =?UTF-8?q?=20parent=20test=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 5 +++-- zds/tutorialv2/tests/tests_views.py | 24 ++++++++++++++++++++++++ zds/tutorialv2/utils.py | 4 ++-- zds/tutorialv2/views.py | 11 +++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 3e5d9f08fb..efa1ae00b8 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -726,7 +726,7 @@ def repo_delete(self, commit_message='', do_commit=True): """ :param commit_message: commit message used instead of default one if provided :param do_commit: tells if we have to commit the change now or let the outter program do it - :return: commit sha + :return: commit sha, None if no commit is done """ path = self.get_path(relative=True) repo = self.container.top_container().repository @@ -750,7 +750,8 @@ def repo_delete(self, commit_message='', do_commit=True): self.container.top_container().sha_draft = cm.hexsha - return cm.hexsha + return cm.hexsha + return None class VersionedContent(Container): diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 09e01cf154..d8f46346f8 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -797,6 +797,30 @@ def test_move_before(self): versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] self.assertEqual(self.extract2.slug, extract.slug) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for extract (smoothly) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.extract4 = ExtractFactory(container=self.chapter2, db_object=self.tuto) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.extract4.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index bb9c644201..8301ad233c 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -24,10 +24,10 @@ def search_container_or_404(base_content, kwargs_array): :raise Http404 if no suitable container is found """ container = None - if isinstance(kwargs_array, str): + if isinstance(kwargs_array, basestring): dic = {} dic["parent_container_slug"] = kwargs_array.split("/")[0] - if len(kwargs_array.split("/")) == 2: + if len(kwargs_array.split("/")) >= 2: dic["container_slug"] = kwargs_array.split("/")[1] kwargs_array = dic diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 15ab388a65..4b37bfb229 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,6 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm +from zds.tutorialv2.utils import try_adopt_new_child from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -1034,12 +1035,14 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): - - target_parent = search_container_or_404(versioned, target.split("/")[1:-2]) - if target.split("/")[-1] not in target_parent: + + target_parent = search_container_or_404(versioned, target) + print target_parent.get_path() + if target.split("/")[-1] not in target_parent.children_dict: raise Http404 child = target_parent.children_dict[target.split("/")[-1]] - try_adopt_new_child(target_parent, parent[child_slug]) + try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) versioned.dump_json() From d922944acc2ff5de04d5a5435b9cae276a1cdbd4 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 24 Mar 2015 14:30:12 +0100 Subject: [PATCH 167/887] =?UTF-8?q?ajout=20de=20tests=20dans=20le=20d?= =?UTF-8?q?=C3=A9placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 18 ++++- zds/tutorialv2/tests/tests_views.py | 107 +++++++++++++++++++++++++++- zds/tutorialv2/utils.py | 4 +- zds/tutorialv2/views.py | 36 ++++++---- 4 files changed, 145 insertions(+), 20 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index efa1ae00b8..66061bb1d8 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -116,6 +116,7 @@ def get_last_child_position(self): def get_tree_depth(self): """ + Represent the depth where this container is found Tree depth is no more than 2, because there is 3 levels for Containers : - PublishableContent (0), - Part (1), @@ -130,6 +131,20 @@ def get_tree_depth(self): depth += 1 return depth + def get_tree_level(self): + """ + Represent the level in the tree of this container, i.e the depth of its deepest child + :return: tree level + """ + current = self + if len(self.children) == 0: + return 1 + elif isinstance(self.children[0], Extract): + return 2 + else: + return 1 + max([i.get_tree_level() for i in self.children]) + + def has_child_with_path(self, child_path): """ Check that the given path represent the full path @@ -498,7 +513,8 @@ def repo_delete(self, commit_message='', do_commit=True): self.top_container().sha_draft = cm.hexsha - return cm.hexsha + return cm.hexsha + return None def move_child_up(self, child_slug): """ diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index d8f46346f8..da7ba39a01 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -769,9 +769,9 @@ def test_move_up_extract(self): follow=False) self.assertEqual(result.status_code, 403) - def test_move_before(self): + def test_move_extract_before(self): # test 1 : move extract after a sibling - # login with author + # login with author self.assertEqual( self.client.login( username=self.user_author.username, @@ -813,6 +813,7 @@ def test_move_before(self): 'pk': tuto.pk }, follow=True) + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -821,7 +822,107 @@ def test_move_before(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + # test try to move to a container that can't get extract + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.chapter1.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move near an extract that does not exist + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'before:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(404, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + + def test_move_container_before(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for container (smoothly) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.chapter3.slug, + 'container_slug': self.part1.slug, + 'first_level_slug': '', + 'moving_method': 'before:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter4.slug, chapter.slug) + # test changing parent for too deep container + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.part1.slug, + 'container_slug': self.tuto.slug, + 'first_level_slug': '', + 'moving_method': 'before:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter4.slug, chapter.slug) + + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 8301ad233c..c74db53e74 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -51,7 +51,7 @@ def search_container_or_404(base_content, kwargs_array): else: if not isinstance(container, Container): raise Http404 - else: + elif container == base_content: # if we have no subcontainer, there is neither "container_slug" nor "parent_container_slug return base_content if container is None: @@ -160,7 +160,7 @@ def try_adopt_new_child(adoptive_parent, child): if isinstance(child, Container): if not container.can_add_container(): raise TypeError - if container.get_tree_depth() + child.get_tree_depth() > settings.ZDS_APP['content']['max_tree_depth']: + if container.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError child.repo_delete('', False) container.add_container(child) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4b37bfb229..1ef0f55553 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,7 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm -from zds.tutorialv2.utils import try_adopt_new_child +from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -1002,7 +1002,7 @@ def form_valid(self, form): versioned = content.load_version() base_container_slug = form.data["container_slug"] child_slug = form.data['child_slug'] - + if base_container_slug == '': raise Http404 @@ -1013,14 +1013,17 @@ def form_valid(self, form): parent = versioned else: search_params = {} + if form.data['first_level_slug'] != '': + search_params['parent_container_slug'] = form.data['first_level_slug'] search_params['container_slug'] = base_container_slug else: - search_params['parent_container_slug'] = base_container_slug + search_params['container_slug'] = base_container_slug parent = search_container_or_404(versioned, search_params) - + try: + child = parent.children_dict[child_slug] if form.data['moving_method'] == MoveElementForm.MOVE_UP: parent.move_child_up(child_slug) if form.data['moving_method'] == MoveElementForm.MOVE_DOWN: @@ -1035,13 +1038,16 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): - - target_parent = search_container_or_404(versioned, target) - print target_parent.get_path() - if target.split("/")[-1] not in target_parent.children_dict: - raise Http404 + if "/" not in target: + target_parent = versioned + else: + target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) + + if target.split("/")[-1] not in target_parent.children_dict: + raise Http404 child = target_parent.children_dict[target.split("/")[-1]] try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) @@ -1053,18 +1059,20 @@ def form_valid(self, form): content.sha_draft = versioned.sha_draft content.save() messages.info(self.request, _(u"L'élément a bien été déplacé.")) - + except TooDeepContainerError: + messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir'\ + ' la sous-section d\'une autre section.')) except ValueError: raise Http404 except IndexError: messages.warning(self.request, _(u"L'élément est déjà à la place souhaitée.")) + except TypeError: + messages.error(self.request, _(u"L'élément ne peut pas être déplacé à cet endroit")) if base_container_slug == versioned.slug: return redirect(reverse("content:view", args=[content.pk, content.slug])) - else: - search_params['slug'] = content.slug - search_params['pk'] = content.pk - return redirect(reverse("content:view-container", kwargs=search_params)) + else: + return redirect(child.get_absolute_url()) @can_write_and_read_now From 51fda4ffea3fa33292acf5a4a9e5243cdf8b8825 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Thu, 26 Mar 2015 13:55:37 +0100 Subject: [PATCH 168/887] =?UTF-8?q?tests=20du=20d=C3=A9placement=20termin?= =?UTF-8?q?=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 154 ++++++++++++++++++++++++++++ zds/tutorialv2/views.py | 13 ++- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index da7ba39a01..307545774b 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -921,7 +921,161 @@ def test_move_container_before(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[1] self.assertEqual(self.chapter4.slug, chapter.slug) + + def test_move_extract_after(self): + # test 1 : move extract after a sibling + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.extract2 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + self.extract3 = ExtractFactory(container=self.chapter1, db_object=self.tuto) + old_sha = tuto.sha_draft + # test moving smoothly + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.extract3.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] + self.assertEqual(self.extract2.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[1] + self.assertEqual(self.extract3.slug, extract.slug) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for extract (smoothly) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.extract4 = ExtractFactory(container=self.chapter2, db_object=self.tuto) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter1.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.extract4.get_path(True)[:-3], + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move to a container that can't get extract + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.chapter1.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + # test try to move near an extract that does not exist + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.extract1.slug, + 'container_slug': self.chapter2.slug, + 'first_level_slug': self.part1.slug, + 'moving_method': 'after:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(404, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] + self.assertEqual(self.extract1.slug, extract.slug) + extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] + self.assertEqual(self.extract4.slug, extract.slug) + self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) + + def test_move_container_after(self): + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) + self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + # test changing parent for container (smoothly) + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.chapter3.slug, + 'container_slug': self.part1.slug, + 'first_level_slug': '', + 'moving_method': 'after:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + + self.assertEqual(200, result.status_code) + self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter4.slug, chapter.slug) + # test changing parent for too deep container + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + old_sha = tuto.sha_draft + result = self.client.post( + reverse('content:move-element'), + { + 'child_slug': self.part1.slug, + 'container_slug': self.tuto.slug, + 'first_level_slug': '', + 'moving_method': 'after:'+self.chapter4.get_path(True), + 'pk': tuto.pk + }, + follow=True) + self.assertEqual(200, result.status_code) + self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) + versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() + self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(self.chapter3.slug, chapter.slug) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertEqual(self.chapter4.slug, chapter.slug) def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 1ef0f55553..50cb841b0a 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1031,10 +1031,17 @@ def form_valid(self, form): if form.data['moving_method'][0:len(MoveElementForm.MOVE_AFTER)] == MoveElementForm.MOVE_AFTER: target = form.data['moving_method'][len(MoveElementForm.MOVE_AFTER) + 1:] if not parent.has_child_with_path(target): - target_parent = search_container_or_404(versionned, target.split("/")[1:-2]) + if "/" not in target: + target_parent = versioned + else: + target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) + + if target.split("/")[-1] not in target_parent.children_dict: + raise Http404 child = target_parent.children_dict[target.split("/")[-1]] - try_adopt_new_child(target_parent, parent[child_slug]) - target_parent.move_child_after(child_slug, target) + try_adopt_new_child(target_parent, parent.children_dict[child_slug]) + parent = target_parent + parent.move_child_after(child_slug, target.split("/")[-1]) if form.data['moving_method'][0:len(MoveElementForm.MOVE_BEFORE)] == MoveElementForm.MOVE_BEFORE: target = form.data['moving_method'][len(MoveElementForm.MOVE_BEFORE) + 1:] if not parent.has_child_with_path(target): From 683290d8c90bfd871c6cc1b3498d22aa754e9dad Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Mon, 30 Mar 2015 14:02:16 +0200 Subject: [PATCH 169/887] =?UTF-8?q?pr=C3=A9paration=20=C3=A0=20l'int=C3=A9?= =?UTF-8?q?gration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 21 ++++++++++++++ zds/tutorialv2/utils.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 66061bb1d8..353eb9b739 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -597,6 +597,20 @@ def move_child_before(self, child_slug, refer_slug): # if we want our child to get up (reference is an upper child) for i in range(child_pos, refer_pos, - 1): self.move_child_up(child_slug) + + def traverse(self, only_container=True): + """ + traverse the + :param only_container: if we only want container's paths, not extract + :return: a generator that traverse all the container recursively (depth traversal) + """ + yield self + for child in children: + if isinstance(child, Container): + for _ in child.traverse(only_container): + yield _ + elif not only_container: + yield child class Extract: @@ -666,6 +680,13 @@ def get_edit_url(self): return reverse('content:edit-extract', args=args) + def get_full_slug(self): + """ + get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) + this method is an alias to extract.get_path(True)[:-3] (remove .md extension) + """ + return self.get_path(True)[:-3] + def get_delete_url(self): """ :return: url to delete the extract diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index c74db53e74..ea7c6cdfa6 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -165,3 +165,64 @@ def try_adopt_new_child(adoptive_parent, child): child.repo_delete('', False) container.add_container(child) +def get_target_tagged_tree(moveable_child, root): + """ + Gets the tagged tree with deplacement availability + :param moveable_child: the extract we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + check get_target_tagged_tree_for_extract and get_target_tagged_tree_for_container for format + """ + if isinstance(moveable_child, Extract): + return get_target_tagged_tree_for_extract(moveable_child, root) + else: + return get_target_tagged_tree_for_container(moveable_child, root) + +def get_target_tagged_tree_for_extract(moveable_child, root): + """ + Gets the tagged tree with deplacement availability when moveable_child is an extract + :param moveable_child: the extract we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + .. sourcecode::python + [ + (relative_path_root, False), + (relative_path_of_a_container, False), + (relative_path_of_another_extract, True) + ... + ] + """ + target_tagged_tree = [] + for child in root.traverse(False): + if is_instance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), child != moveable_child)) + else: + target_tagged_tree.append((child.get_path(True), False)) + + return target_tagged_tree + +def get_target_tagged_tree_for_container(moveable_child, root): + """ + Gets the tagged tree with deplacement availability when moveable_child is an extract + :param moveable_child: the container we want to move + :param root: the VersionnedContent we use as root + :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + .. sourcecode::python + [ + (relative_path_root, True), + (relative_path_of_a_too_deep_container, False), + (relativbe_path_of_a_good_container, False), + (relative_path_of_another_extract, False) + ... + ] + """ + target_tagged_tree = [] + for child in root.traverse(False): + if is_instance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), False)) + else: + composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + enabled = composed_depth > settings.ZDS_APP['content']['max_tree_depth'] + target_tagged_tree.append((child.get_path(True), enabled and child != moveable_child)) + + return target_tagged_tree From 03d64b35ae3f0fdd82e4f87868ae837019c16dd3 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Tue, 31 Mar 2015 14:03:35 +0200 Subject: [PATCH 170/887] =?UTF-8?q?int=C3=A9gration=20du=20d=C3=A9placemen?= =?UTF-8?q?t=20des=20sections=20dans=20la=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/container.html | 18 ++++++++- zds/tutorialv2/models.py | 3 +- zds/tutorialv2/utils.py | 47 ++++++++---------------- zds/tutorialv2/views.py | 7 ++-- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index d6593a96a0..26815e3a9a 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -159,9 +159,23 @@

    {% trans "Descendre" %} {% endif %} + + {% for element in containers_target %} + + {% endfor %} + + {% for element in containers_target %} + + {% endfor %} - {% if container.parent.parent %} - + {% if container.get_tree_depth = 2 %} + {% endif %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 353eb9b739..2dbad4f899 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -34,7 +34,6 @@ from zds.utils.models import HelpWriting from uuslug import uuslug - TYPE_CHOICES = ( ('TUTORIAL', 'Tutoriel'), ('ARTICLE', 'Article'), @@ -605,7 +604,7 @@ def traverse(self, only_container=True): :return: a generator that traverse all the container recursively (depth traversal) """ yield self - for child in children: + for child in self.children: if isinstance(child, Container): for _ in child.traverse(only_container): yield _ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index ea7c6cdfa6..13393b883e 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,19 +151,19 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - container = adoptive_parent + adoptive_parent if isinstance(child, Extract): - if not container.can_add_extract(): + if not adoptive_parent.can_add_extract(): raise TypeError child.repo_delete('', False) - container.add_extract(child, generate_slug=False) + adoptive_parent.add_extract(child, generate_slug=False) if isinstance(child, Container): - if not container.can_add_container(): + if not adoptive_parent.can_add_container(): raise TypeError - if container.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: + if adoptive_parent.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - child.repo_delete('', False) - container.add_container(child) + adoptive_parent.repo_delete('', False) + adoptive_parent.add_container(child) def get_target_tagged_tree(moveable_child, root): """ @@ -184,20 +184,14 @@ def get_target_tagged_tree_for_extract(moveable_child, root): :param moveable_child: the extract we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - .. sourcecode::python - [ - (relative_path_root, False), - (relative_path_of_a_container, False), - (relative_path_of_another_extract, True) - ... - ] + tuples are (relative_path, title, level, can_be_a_target) """ target_tagged_tree = [] for child in root.traverse(False): if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child != moveable_child)) + target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_level(), child != moveable_child)) else: - target_tagged_tree.append((child.get_path(True), False)) + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) return target_tagged_tree @@ -207,22 +201,13 @@ def get_target_tagged_tree_for_container(moveable_child, root): :param moveable_child: the container we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - .. sourcecode::python - [ - (relative_path_root, True), - (relative_path_of_a_too_deep_container, False), - (relativbe_path_of_a_good_container, False), - (relative_path_of_another_extract, False) - ... - ] + extracts are not included """ target_tagged_tree = [] - for child in root.traverse(False): - if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), False)) - else: - composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() - enabled = composed_depth > settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), enabled and child != moveable_child)) + for child in root.traverse(True): + composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), + enabled and child != moveable_child and child != root)) return target_tagged_tree diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 50cb841b0a..0857185a42 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -8,7 +8,7 @@ from django.template.loader import render_to_string from zds.forum.models import Forum, Topic from zds.tutorialv2.forms import BetaForm, MoveElementForm -from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError +from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError, get_target_tagged_tree from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic try: @@ -348,7 +348,7 @@ def get_context_data(self, **kwargs): """Show the given tutorial if exists.""" context = super(DisplayContainer, self).get_context_data(**kwargs) context['container'] = search_container_or_404(context['content'], self.kwargs) - + context['containers_target'] = get_target_tagged_tree(context['container'], context['content']) # pagination: search for `previous` and `next`, if available if context['content'].type != 'ARTICLE' and not context['content'].has_extracts(): chapters = context['content'].get_list_of_chapters() @@ -374,8 +374,7 @@ class EditContainer(LoggedWithReadWriteHability, SingleContentFormViewMixin): content = None def get_context_data(self, **kwargs): - context = super(EditContainer, self).get_context_data(**kwargs) - context['container'] = search_container_or_404(self.versioned_object, self.kwargs) + context = super(EditContainer, self).get_context_data(**kwargs) return context From 261085cf166ec2bfacceb2eeb0910c6fb80258fe Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Tue, 31 Mar 2015 22:10:41 -0400 Subject: [PATCH 171/887] Assure qu'on puisse naviguer dans des versions precedentes --- templates/tutorialv2/includes/child.part.html | 8 ++- .../tutorialv2/includes/summary.part.html | 7 ++- templates/tutorialv2/view/content.html | 2 +- zds/tutorialv2/mixins.py | 10 +++- zds/tutorialv2/models.py | 54 +++++++++++++----- zds/tutorialv2/tests/tests_views.py | 48 ++++++++-------- zds/tutorialv2/utils.py | 56 ++++++++++--------- zds/tutorialv2/views.py | 17 +++--- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html index db052d0434..a30c23b4b1 100644 --- a/templates/tutorialv2/includes/child.part.html +++ b/templates/tutorialv2/includes/child.part.html @@ -3,7 +3,13 @@

    - + {{ child.title }}

    diff --git a/templates/tutorialv2/includes/summary.part.html b/templates/tutorialv2/includes/summary.part.html index 11a37419fe..e13e1fbe9f 100644 --- a/templates/tutorialv2/includes/summary.part.html +++ b/templates/tutorialv2/includes/summary.part.html @@ -48,7 +48,12 @@

    {% if online %} href="{{ subchild.get_absolute_url_online }}" {% else %} - href="{{ subchild.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}" + {% if subchild.text %} + {# subchild is an extract #} + href="{{ subchild.container.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}#{{ subchild.position_in_parent }}-{{ subchild.slug }}" + {% else %} + href="{{ subchild.get_absolute_url }}{% if version %}?version={{ version }}{% endif %}" + {% endif %} {% endif %} class="mobile-menu-link mobile-menu-sublink {% if current_container.long_slug == subchild.long_slug %}unread{% endif %}" > diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 5bc7eabd7e..b1d67076b9 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -153,7 +153,7 @@

    {% endif %} {% else %} - + {% trans "Version brouillon" %} {% endif %} diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index aff362c9c7..0f486fca01 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -30,8 +30,6 @@ def get_object(self, queryset=None): obj = queryset.first() else: obj = get_object_or_404(PublishableContent, pk=self.kwargs['pk']) - if 'slug' in self.kwargs and obj.slug != self.kwargs['slug']: - raise Http404 if self.must_be_author and self.request.user not in obj.authors.all(): if self.authorized_for_staff and self.request.user.has_perm('tutorial.change_tutorial'): return obj @@ -57,7 +55,13 @@ def get_versioned_object(self): raise PermissionDenied # load versioned file - return self.object.load_version_or_404(sha) + versioned = self.object.load_version_or_404(sha) + + if 'slug' in self.kwargs \ + and (versioned.slug != self.kwargs['slug'] and self.object.slug != self.kwargs['slug']): + raise Http404 + + return versioned class SingleContentPostMixin(SingleContentViewMixin): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 2dbad4f899..b9a45e0fe0 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -135,7 +135,7 @@ def get_tree_level(self): Represent the level in the tree of this container, i.e the depth of its deepest child :return: tree level """ - current = self + if len(self.children) == 0: return 1 elif isinstance(self.children[0], Extract): @@ -143,7 +143,6 @@ def get_tree_level(self): else: return 1 + max([i.get_tree_level() for i in self.children]) - def has_child_with_path(self, child_path): """ Check that the given path represent the full path @@ -596,12 +595,12 @@ def move_child_before(self, child_slug, refer_slug): # if we want our child to get up (reference is an upper child) for i in range(child_pos, refer_pos, - 1): self.move_child_up(child_slug) - + def traverse(self, only_container=True): """ - traverse the + Traverse the container :param only_container: if we only want container's paths, not extract - :return: a generator that traverse all the container recursively (depth traversal) + :return: a generator that traverse all the container recursively (depth traversal) """ yield self for child in self.children: @@ -681,7 +680,7 @@ def get_edit_url(self): def get_full_slug(self): """ - get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) + get the slug of curent extract with its full path (part1/chapter1/slug_of_extract) this method is an alias to extract.get_path(True)[:-3] (remove .md extension) """ return self.get_path(True)[:-3] @@ -800,6 +799,7 @@ class VersionedContent(Container): """ current_version = None + slug_repository = '' repository = None # Metadata from json : @@ -836,11 +836,25 @@ class VersionedContent(Container): update_date = None source = None - def __init__(self, current_version, _type, title, slug): + def __init__(self, current_version, _type, title, slug, slug_repository=''): + """ + :param current_version: version of the content + :param _type: either "TUTORIAL" or "ARTICLE" + :param title: title of the content + :param slug: slug of the content + :param slug_repository: slug of the directory that contains the repository, named after database slug. + if not provided, use `self.slug` instead. + """ + Container.__init__(self, title, slug) self.current_version = current_version self.type = _type + if slug_repository != '': + self.slug_repository = slug_repository + else: + self.slug_repository = slug + if os.path.exists(self.get_path()): self.repository = Repo(self.get_path()) @@ -873,16 +887,20 @@ def get_absolute_url_beta(self): else: return self.get_absolute_url() - def get_path(self, relative=False): + def get_path(self, relative=False, use_current_slug=False): """ Get the physical path to the draft version of the Content. :param relative: if `True`, the path will be relative, absolute otherwise. + :param use_current_slug: if `True`, use `self.slug` instead of `self.slug_last_draft` :return: physical path """ if relative: return '' else: - return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) + slug = self.slug_repository + if use_current_slug: + slug = self.slug + return os.path.join(settings.ZDS_APP['content']['repo_private_path'], slug) def get_prod_path(self): """ @@ -943,16 +961,17 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi if slug != self.slug: # move repository - old_path = self.get_path() + old_path = self.get_path(use_current_slug=True) self.slug = slug - new_path = self.get_path() + new_path = self.get_path(use_current_slug=True) shutil.move(old_path, new_path) self.repository = Repo(new_path) + self.slug_repository = slug return self.repo_update(title, introduction, conclusion, commit_message) -def get_content_from_json(json, sha): +def get_content_from_json(json, sha, slug_last_draft): """ Transform the JSON formated data into `VersionedContent` :param json: JSON data from a `manifest.json` file @@ -961,7 +980,7 @@ def get_content_from_json(json, sha): """ # TODO: should definitely be static # create and fill the container - versioned = VersionedContent(sha, 'TUTORIAL', json['title'], json['slug']) + versioned = VersionedContent(sha, 'TUTORIAL', json['title'], json['slug'], slug_last_draft) if 'version' in json and json['version'] == 2: # fill metadata : @@ -1055,6 +1074,7 @@ def init_new_repo(db_object, introduction_text, conclusion_text, commit_message= versioned_content = VersionedContent(None, db_object.type, db_object.title, + db_object.slug, db_object.slug) # fill some information that are missing : @@ -1275,7 +1295,7 @@ def load_version_or_404(self, sha=None, public=False): """ try: return self.load_version(sha, public) - except BadObject: + except (BadObject, IOError): raise Http404 def load_version(self, sha=None, public=False): @@ -1296,10 +1316,14 @@ def load_version(self, sha=None, public=False): sha = self.sha_public path = self.get_repo_path() + + if not os.path.isdir(path): + raise IOError(path) + repo = Repo(path) data = get_blob(repo.commit(sha).tree, 'manifest.json') json = json_reader.loads(data) - versioned = get_content_from_json(json, sha) + versioned = get_content_from_json(json, sha, self.slug) self.insert_data_in_versioned(versioned) return versioned diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 307545774b..1e0c5cfbc9 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -788,7 +788,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.extract3.get_path(True)[:-3], + 'moving_method': 'before:' + self.extract3.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) @@ -797,7 +797,7 @@ def test_move_extract_before(self): versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[0] self.assertEqual(self.extract2.slug, extract.slug) - + tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft # test changing parent for extract (smoothly) @@ -809,11 +809,11 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.extract4.get_path(True)[:-3], + 'moving_method': 'before:' + self.extract4.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -831,7 +831,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.chapter1.get_path(True), + 'moving_method': 'before:' + self.chapter1.get_path(True), 'pk': tuto.pk }, follow=True) @@ -852,7 +852,7 @@ def test_move_extract_before(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'before:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'moving_method': 'before:' + self.chapter1.get_path(True) + "/un-mauvais-extrait", 'pk': tuto.pk }, follow=True) @@ -864,7 +864,7 @@ def test_move_extract_before(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[1] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + def test_move_container_before(self): # login with author self.assertEqual( @@ -886,11 +886,11 @@ def test_move_container_before(self): 'child_slug': self.chapter3.slug, 'container_slug': self.part1.slug, 'first_level_slug': '', - 'moving_method': 'before:'+self.chapter4.get_path(True), + 'moving_method': 'before:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -908,11 +908,11 @@ def test_move_container_before(self): 'child_slug': self.part1.slug, 'container_slug': self.tuto.slug, 'first_level_slug': '', - 'moving_method': 'before:'+self.chapter4.get_path(True), + 'moving_method': 'before:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -921,7 +921,7 @@ def test_move_container_before(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[1] self.assertEqual(self.chapter4.slug, chapter.slug) - + def test_move_extract_after(self): # test 1 : move extract after a sibling # login with author @@ -941,7 +941,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.extract3.get_path(True)[:-3], + 'moving_method': 'after:' + self.extract3.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) @@ -952,7 +952,7 @@ def test_move_extract_after(self): self.assertEqual(self.extract2.slug, extract.slug) extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children[1] self.assertEqual(self.extract3.slug, extract.slug) - + tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft # test changing parent for extract (smoothly) @@ -964,11 +964,11 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter1.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.extract4.get_path(True)[:-3], + 'moving_method': 'after:' + self.extract4.get_path(True)[:-3], 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -986,7 +986,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.chapter1.get_path(True), + 'moving_method': 'after:' + self.chapter1.get_path(True), 'pk': tuto.pk }, follow=True) @@ -1007,7 +1007,7 @@ def test_move_extract_after(self): 'child_slug': self.extract1.slug, 'container_slug': self.chapter2.slug, 'first_level_slug': self.part1.slug, - 'moving_method': 'after:'+self.chapter1.get_path(True)+"/un-mauvais-extrait", + 'moving_method': 'after:' + self.chapter1.get_path(True) + "/un-mauvais-extrait", 'pk': tuto.pk }, follow=True) @@ -1019,7 +1019,7 @@ def test_move_extract_after(self): extract = versioned.children_dict[self.part1.slug].children_dict[self.chapter2.slug].children[0] self.assertEqual(self.extract4.slug, extract.slug) self.assertEqual(2, len(versioned.children_dict[self.part1.slug].children_dict[self.chapter1.slug].children)) - + def test_move_container_after(self): # login with author self.assertEqual( @@ -1041,11 +1041,11 @@ def test_move_container_after(self): 'child_slug': self.chapter3.slug, 'container_slug': self.part1.slug, 'first_level_slug': '', - 'moving_method': 'after:'+self.chapter4.get_path(True), + 'moving_method': 'after:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -1063,11 +1063,11 @@ def test_move_container_after(self): 'child_slug': self.part1.slug, 'container_slug': self.tuto.slug, 'first_level_slug': '', - 'moving_method': 'after:'+self.chapter4.get_path(True), + 'moving_method': 'after:' + self.chapter4.get_path(True), 'pk': tuto.pk }, follow=True) - + self.assertEqual(200, result.status_code) self.assertEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() @@ -1076,7 +1076,7 @@ def test_move_container_after(self): self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[0] self.assertEqual(self.chapter4.slug, chapter.slug) - + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 13393b883e..edb6c0c585 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,7 +151,6 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ - adoptive_parent if isinstance(child, Extract): if not adoptive_parent.can_add_extract(): raise TypeError @@ -165,49 +164,56 @@ def try_adopt_new_child(adoptive_parent, child): adoptive_parent.repo_delete('', False) adoptive_parent.add_container(child) -def get_target_tagged_tree(moveable_child, root): + +def get_target_tagged_tree(movable_child, root): """ Gets the tagged tree with deplacement availability - :param moveable_child: the extract we want to move + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + :return: an array of tuples that represent the capacity of movable_child to be moved near another child check get_target_tagged_tree_for_extract and get_target_tagged_tree_for_container for format """ - if isinstance(moveable_child, Extract): - return get_target_tagged_tree_for_extract(moveable_child, root) + if isinstance(movable_child, Extract): + return get_target_tagged_tree_for_extract(movable_child, root) else: - return get_target_tagged_tree_for_container(moveable_child, root) - -def get_target_tagged_tree_for_extract(moveable_child, root): + return get_target_tagged_tree_for_container(movable_child, root) + + +def get_target_tagged_tree_for_extract(movable_child, root): """ - Gets the tagged tree with deplacement availability when moveable_child is an extract - :param moveable_child: the extract we want to move + Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child - tuples are (relative_path, title, level, can_be_a_target) + :return: an array of tuples that represent the capacity of movable_child to be moved near another child + tuples are (relative_path, title, level, can_be_a_target) """ target_tagged_tree = [] for child in root.traverse(False): - if is_instance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_level(), child != moveable_child)) + if isinstance(child, Extract): + target_tagged_tree.append((child.get_full_slug(), + child.title, + child.container.get_tree_level() + 1, + child != movable_child)) else: target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) - + return target_tagged_tree - -def get_target_tagged_tree_for_container(moveable_child, root): + + +def get_target_tagged_tree_for_container(movable_child, root): """ - Gets the tagged tree with deplacement availability when moveable_child is an extract - :param moveable_child: the container we want to move + Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the container we want to move :param root: the VersionnedContent we use as root - :return: an array of tuples that represent the capacity of moveable_child to be moved near another child + :return: an array of tuples that represent the capacity of movable_child to be moved near another child extracts are not included """ target_tagged_tree = [] for child in root.traverse(True): - composed_depth = child.get_tree_depth() + moveable_child.get_tree_level() + composed_depth = child.get_tree_depth() + movable_child.get_tree_level() enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), - enabled and child != moveable_child and child != root)) - + target_tagged_tree.append((child.get_path(True), + child.title, child.get_tree_level(), + enabled and child != movable_child and child != root)) + return target_tagged_tree diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 0857185a42..6e271fd5cf 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -374,7 +374,7 @@ class EditContainer(LoggedWithReadWriteHability, SingleContentFormViewMixin): content = None def get_context_data(self, **kwargs): - context = super(EditContainer, self).get_context_data(**kwargs) + context = super(EditContainer, self).get_context_data(**kwargs) return context @@ -1001,7 +1001,7 @@ def form_valid(self, form): versioned = content.load_version() base_container_slug = form.data["container_slug"] child_slug = form.data['child_slug'] - + if base_container_slug == '': raise Http404 @@ -1014,13 +1014,12 @@ def form_valid(self, form): search_params = {} if form.data['first_level_slug'] != '': - search_params['parent_container_slug'] = form.data['first_level_slug'] search_params['container_slug'] = base_container_slug else: search_params['container_slug'] = base_container_slug parent = search_container_or_404(versioned, search_params) - + try: child = parent.children_dict[child_slug] if form.data['moving_method'] == MoveElementForm.MOVE_UP: @@ -1034,7 +1033,7 @@ def form_valid(self, form): target_parent = versioned else: target_parent = search_container_or_404(versioned, "/".join(target.split("/")[:-1])) - + if target.split("/")[-1] not in target_parent.children_dict: raise Http404 child = target_parent.children_dict[target.split("/")[-1]] @@ -1053,7 +1052,7 @@ def form_valid(self, form): raise Http404 child = target_parent.children_dict[target.split("/")[-1]] try_adopt_new_child(target_parent, parent.children_dict[child_slug]) - + parent = target_parent parent.move_child_before(child_slug, target.split("/")[-1]) @@ -1066,8 +1065,8 @@ def form_valid(self, form): content.save() messages.info(self.request, _(u"L'élément a bien été déplacé.")) except TooDeepContainerError: - messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir'\ - ' la sous-section d\'une autre section.')) + messages.error(self.request, _(u'Cette section contient déjà trop de sous-section pour devenir' + u' la sous-section d\'une autre section.')) except ValueError: raise Http404 except IndexError: @@ -1077,7 +1076,7 @@ def form_valid(self, form): if base_container_slug == versioned.slug: return redirect(reverse("content:view", args=[content.pk, content.slug])) - else: + else: return redirect(child.get_absolute_url()) From cb644a29bc642de64eebac0c5627d55ecedd4cf3 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Wed, 1 Apr 2015 14:03:54 +0200 Subject: [PATCH 172/887] =?UTF-8?q?le=20repo=20est=20correctement=20d?= =?UTF-8?q?=C3=A9plac=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 17 +++++++++++++++++ zds/tutorialv2/utils.py | 7 +++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index b9a45e0fe0..ff67099011 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -970,6 +970,23 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi return self.repo_update(title, introduction, conclusion, commit_message) + def change_child_directory(self, child, adoptive_parent): + + old_path = child.get_path(False) # absolute path because we want to access the adress + adoptive_parent_path = adoptive_parent.get_path(False) + if isinstance(child, Extract): + old_parent = child.container + old_parent.children = [c for c in old_parent.children if c.slug != child.slug] + adoptive_parent.add_extract(child) + else: + old_parent = child.parent + old_parent.children = [c for c in old_parent.children if c.slug != child.slug] + adoptive_parent.add_container(child) + self.repository.index.move([old_path, child.get_path(False)]) + + self.dump_json() + + def get_content_from_json(json, sha, slug_last_draft): """ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index edb6c0c585..1b98c6f627 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -151,18 +151,17 @@ def try_adopt_new_child(adoptive_parent, child): :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent :return: """ + if isinstance(child, Extract): if not adoptive_parent.can_add_extract(): raise TypeError - child.repo_delete('', False) - adoptive_parent.add_extract(child, generate_slug=False) if isinstance(child, Container): if not adoptive_parent.can_add_container(): raise TypeError if adoptive_parent.get_tree_depth() + child.get_tree_level() > settings.ZDS_APP['content']['max_tree_depth']: raise TooDeepContainerError - adoptive_parent.repo_delete('', False) - adoptive_parent.add_container(child) + adoptive_parent.top_container().change_child_directory(child, adoptive_parent) + def get_target_tagged_tree(movable_child, root): From ab10b86f35c4a6a9e4a14e383f735ad05ed31a12 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Wed, 1 Apr 2015 14:26:53 +0200 Subject: [PATCH 173/887] =?UTF-8?q?Am=C3=A9liore=20l'int=C3=A9gration=20da?= =?UTF-8?q?ns=20la=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/container.html | 5 +++-- zds/tutorialv2/utils.py | 8 ++++---- zds/utils/templatetags/times.py | 7 +++++++ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 zds/utils/templatetags/times.py diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index 26815e3a9a..b6de29b5b2 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -3,6 +3,7 @@ {% load thumbnail %} {% load emarkdown %} {% load i18n %} +{% load times %} {% block title %} @@ -163,14 +164,14 @@

    {% for element in containers_target %} {% endfor %} {% for element in containers_target %} {% endfor %} diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 1b98c6f627..8437817873 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -189,12 +189,12 @@ def get_target_tagged_tree_for_extract(movable_child, root): target_tagged_tree = [] for child in root.traverse(False): if isinstance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), + target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_depth(), child != moveable_child)) child.title, child.container.get_tree_level() + 1, child != movable_child)) else: - target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_level(), False)) + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), False)) return target_tagged_tree @@ -209,9 +209,9 @@ def get_target_tagged_tree_for_container(movable_child, root): """ target_tagged_tree = [] for child in root.traverse(True): - composed_depth = child.get_tree_depth() + movable_child.get_tree_level() + composed_depth = child.get_tree_depth() + moveable_child.get_tree_depth() enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] - target_tagged_tree.append((child.get_path(True), + target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), child.title, child.get_tree_level(), enabled and child != movable_child and child != root)) diff --git a/zds/utils/templatetags/times.py b/zds/utils/templatetags/times.py new file mode 100644 index 0000000000..555f6fab2b --- /dev/null +++ b/zds/utils/templatetags/times.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter(name='times') +def times(number): + return range(number) From 44a59d32f3fa9337b8a72be54da9e0bf5b77b61d Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 1 Apr 2015 17:15:30 -0400 Subject: [PATCH 174/887] Fini le travail - Corrige des incoherences - ajout une fonction `commit_changes()` a `VersionedContent` - Ajoute deux tests unitaires qui fixent le comportement et assurent l'acces a travers l'historique --- templates/tutorialv2/view/content.html | 2 +- zds/tutorialv2/models.py | 106 +++++++------ zds/tutorialv2/tests/tests_models.py | 126 ++++++++++++++++ zds/tutorialv2/tests/tests_views.py | 199 +++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 45 deletions(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index b1d67076b9..37f63b7dc2 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -153,7 +153,7 @@

    {% endif %} {% else %} - + {% trans "Version brouillon" %} {% endif %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index ff67099011..8976dbfa6a 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -2,8 +2,7 @@ from math import ceil import shutil -from django.http import Http404 -from gitdb.exc import BadObject +from datetime import datetime try: import ujson as json_reader @@ -20,10 +19,13 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import models -from datetime import datetime +from django.http import Http404 +from gitdb.exc import BadObject +from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext as _ + from git.repo import Repo from git import Actor -from django.core.exceptions import PermissionDenied from zds.gallery.models import Image, Gallery from zds.utils import slugify, get_current_user @@ -350,13 +352,14 @@ def get_conclusion(self): return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree, self.conclusion) - def repo_update(self, title, introduction, conclusion, commit_message=''): + def repo_update(self, title, introduction, conclusion, commit_message='', do_commit=True): """ Update the container information and commit them into the repository :param title: the new title :param introduction: the new introduction text :param conclusion: the new conclusion text :param commit_message: commit message that will be used instead of the default one + :param do_commit: perform the commit in repository if `True` :return : commit sha """ @@ -367,6 +370,9 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): self.title = title if self.get_tree_depth() > 0: # if top container, slug is generated from DB, so already changed old_path = self.get_path(relative=True) + old_slug = self.slug + + # move things self.slug = self.parent.get_unique_slug(title) new_path = self.get_path(relative=True) repo.index.move([old_path, new_path]) @@ -374,6 +380,10 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): # update manifest self.update_children() + # update parent children dict: + self.parent.children_dict.pop(old_slug) + self.parent.children_dict[self.slug] = self + # update introduction and conclusion rel_path = self.get_path(relative=True) if self.introduction is None: @@ -393,19 +403,18 @@ def repo_update(self, title, introduction, conclusion, commit_message=''): repo.index.add(['manifest.json', self.introduction, self.conclusion]) if commit_message == '': - commit_message = u'Mise à jour de « ' + self.title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) + commit_message = _(u'Mise à jour de « {} »').format(self.title) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) - def repo_add_container(self, title, introduction, conclusion, commit_message=''): + def repo_add_container(self, title, introduction, conclusion, commit_message='', do_commit=True): """ :param title: title of the new container :param introduction: text of its introduction :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` :return: commit sha """ subcontainer = Container(title) @@ -440,18 +449,17 @@ def repo_add_container(self, title, introduction, conclusion, commit_message='') repo.index.add(['manifest.json', subcontainer.introduction, subcontainer.conclusion]) if commit_message == '': - commit_message = u'Création du conteneur « ' + title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) + commit_message = _(u'Création du conteneur « {} »').format(title) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) - def repo_add_extract(self, title, text, commit_message=''): + def repo_add_extract(self, title, text, commit_message='', do_commit=True): """ :param title: title of the new extract :param text: text of the new extract :param commit_message: commit message that will be used instead of the default one + :param do_commit: perform the commit in repository if `True` :return: commit sha """ extract = Extract(title) @@ -476,12 +484,10 @@ def repo_add_extract(self, title, text, commit_message=''): repo.index.add(['manifest.json', extract.text]) if commit_message == '': - commit_message = u'Création de l\'extrait « ' + title + u' »' - cm = repo.index.commit(commit_message, **get_commit_author()) - - self.top_container().sha_draft = cm.hexsha + commit_message = _(u'Création de l\'extrait « {} »').format(title) - return cm.hexsha + if do_commit: + return self.top_container().commit_changes(commit_message) def repo_delete(self, commit_message='', do_commit=True): """ @@ -506,13 +512,9 @@ def repo_delete(self, commit_message='', do_commit=True): if commit_message == '': commit_message = u'Suppression du conteneur « {} »'.format(self.title) - if do_commit: - cm = repo.index.commit(commit_message, **get_commit_author()) - self.top_container().sha_draft = cm.hexsha - - return cm.hexsha - return None + if do_commit: + return self.top_container().commit_changes(commit_message) def move_child_up(self, child_slug): """ @@ -605,8 +607,8 @@ def traverse(self, only_container=True): yield self for child in self.children: if isinstance(child, Container): - for _ in child.traverse(only_container): - yield _ + for _y in child.traverse(only_container): + yield _y elif not only_container: yield child @@ -717,7 +719,7 @@ def get_text(self): self.container.top_container().repository.commit(self.container.top_container().current_version).tree, self.text) - def repo_update(self, title, text, commit_message=''): + def repo_update(self, title, text, commit_message='', do_commit=True): """ :param title: new title of the extract :param text: new text of the extract @@ -730,12 +732,18 @@ def repo_update(self, title, text, commit_message=''): if title != self.title: # get a new slug old_path = self.get_path(relative=True) + old_slug = self.slug self.title = title self.slug = self.container.get_unique_slug(title) - new_path = self.get_path(relative=True) + # move file + new_path = self.get_path(relative=True) repo.index.move([old_path, new_path]) + # update parent children dict: + self.container.children_dict.pop(old_slug) + self.container.children_dict[self.slug] = self + # edit text path = self.container.top_container().get_path() @@ -751,11 +759,9 @@ def repo_update(self, title, text, commit_message=''): if commit_message == '': commit_message = u'Modification de l\'extrait « {} », situé dans le conteneur « {} »'\ .format(self.title, self.container.title) - cm = repo.index.commit(commit_message, **get_commit_author()) - - self.container.top_container().sha_draft = cm.hexsha - return cm.hexsha + if do_commit: + return self.container.top_container().commit_changes(commit_message) def repo_delete(self, commit_message='', do_commit=True): """ @@ -780,13 +786,9 @@ def repo_delete(self, commit_message='', do_commit=True): if commit_message == '': commit_message = u'Suppression de l\'extrait « {} »'.format(self.title) - if do_commit: - cm = repo.index.commit(commit_message, **get_commit_author()) - self.container.top_container().sha_draft = cm.hexsha - - return cm.hexsha - return None + if do_commit: + return self.container.top_container().commit_changes(commit_message) class VersionedContent(Container): @@ -970,6 +972,15 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi return self.repo_update(title, introduction, conclusion, commit_message) + def commit_changes(self, commit_message): + """Commit change made to the repository""" + cm = self.repository.index.commit(commit_message, **get_commit_author()) + + self.sha_draft = cm.hexsha + self.current_version = cm.hexsha + + return cm.hexsha + def change_child_directory(self, child, adoptive_parent): old_path = child.get_path(False) # absolute path because we want to access the adress @@ -1135,8 +1146,15 @@ def get_commit_author(): :return: correctly formatted commit author for `repo.index.commit()` """ user = get_current_user() - aut_user = str(user.pk) - aut_email = str(user.email) + + if user: + aut_user = str(user.pk) + aut_email = str(user.email) + + else: + aut_user = ZDS_APP['member']['bot_account'] + aut_email = None + if aut_email is None or aut_email.strip() == "": aut_email = "inconnu@{}".format(settings.ZDS_APP['site']['dns']) return {'author': Actor(aut_user, aut_email), 'committer': Actor(aut_user, aut_email)} diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index fe13d94dc2..7fc45d5e7b 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -88,6 +88,132 @@ def test_ensure_unique_slug(self): new_extract_3 = ExtractFactory(title='aa', container=new_chapter_1, db_object=self.tuto) self.assertNotEqual(new_extract_3.slug, new_extract_1.slug) # same parent, forbidden + def test_workflow_repository(self): + """ + Test to ensure the behavior of repo_*() functions : + - if they change the filesystem as they are suppose to ; + - if they change the `self.sha_*` as they are suppose to. + """ + + new_title = u'Un nouveau titre' + other_new_title = u'Un titre différent' + random_text = u'J\'ai faim!' + other_random_text = u'Oh, du chocolat <3' + + versioned = self.tuto.load_version() + current_version = versioned.current_version + slug_repository = versioned.slug_repository + + # VersionedContent: + old_path = versioned.get_path() + self.assertTrue(os.path.isdir(old_path)) + new_slug = versioned.get_unique_slug(new_title) # normally, you get a new slug by asking database ! + + versioned.repo_update_top_container(new_title, new_slug, random_text, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = versioned.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isdir(new_path)) + self.assertFalse(os.path.isdir(old_path)) + + self.assertNotEqual(slug_repository, versioned.slug_repository) # if this test fail, you're in trouble + + # Container: + + # 1. add new part: + versioned.repo_add_container(new_title, random_text, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + part = versioned.children[-1] + old_path = part.get_path() + self.assertTrue(os.path.isdir(old_path)) + self.assertTrue(os.path.exists(os.path.join(versioned.get_path(), part.introduction))) + self.assertTrue(os.path.exists(os.path.join(versioned.get_path(), part.conclusion))) + self.assertEqual(part.get_introduction(), random_text) + self.assertEqual(part.get_conclusion(), random_text) + + # 2. update the part + part.repo_update(other_new_title, other_random_text, other_random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = part.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isdir(new_path)) + self.assertFalse(os.path.isdir(old_path)) + + self.assertEqual(part.get_introduction(), other_random_text) + self.assertEqual(part.get_conclusion(), other_random_text) + + # 3. delete it + part.repo_delete() # boom ! + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + self.assertFalse(os.path.isdir(new_path)) + + # Extract : + + # 1. add new extract + versioned.repo_add_container(new_title, random_text, random_text) # need to add a new part before + part = versioned.children[-1] + + part.repo_add_extract(new_title, random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + extract = part.children[-1] + old_path = extract.get_path() + self.assertTrue(os.path.isfile(old_path)) + self.assertEqual(extract.get_text(), random_text) + + # 2. update extract + extract.repo_update(other_new_title, other_random_text) + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + current_version = versioned.current_version + + new_path = extract.get_path() + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isfile(new_path)) + self.assertFalse(os.path.isfile(old_path)) + + self.assertEqual(extract.get_text(), other_random_text) + + # 3. update parent and see if it still works: + part.repo_update(other_new_title, other_random_text, other_random_text) + + old_path = new_path + new_path = extract.get_path() + + self.assertNotEqual(old_path, new_path) + self.assertTrue(os.path.isfile(new_path)) + self.assertFalse(os.path.isfile(old_path)) + + self.assertEqual(extract.get_text(), other_random_text) + + # 4. Boom, no more extract + extract.repo_delete() + self.assertNotEqual(versioned.sha_draft, current_version) + self.assertNotEqual(versioned.current_version, current_version) + self.assertEqual(versioned.current_version, versioned.sha_draft) + + self.assertFalse(os.path.exists(new_path)) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 1e0c5cfbc9..365dc87303 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -1077,6 +1077,205 @@ def test_move_container_after(self): chapter = versioned.children_dict[self.part2.slug].children[0] self.assertEqual(self.chapter4.slug, chapter.slug) + def test_history_navigation(self): + """ensure that, if the title (and so the slug) of the content change, its content remain accessible""" + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + + # check access + result = self.client.get( + reverse('content:view', args=[tuto.pk, tuto.slug]), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': self.part1.slug + }), + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }), + follow=False) + self.assertEqual(result.status_code, 200) + + # edit tutorial: + old_slug_tuto = tuto.slug + version_1 = tuto.sha_draft # "version 1" is the one before any change + + new_licence = LicenceFactory() + random = 'Pâques, c\'est bientôt?' + + result = self.client.post( + reverse('content:edit', args=[tuto.pk, tuto.slug]), + { + 'title': random, + 'description': random, + 'introduction': random, + 'conclusion': random, + 'type': u'TUTORIAL', + 'licence': new_licence.pk, + 'subcategory': self.subcategory.pk, + }, + follow=False) + self.assertEqual(result.status_code, 302) + + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + version_2 = tuto.sha_draft # "version 2" is the one with the different slug for the tutorial + self.assertNotEqual(tuto.slug, old_slug_tuto) + + # check access using old slug and no version + result = self.client.get( + reverse('content:view', args=[tuto.pk, old_slug_tuto]), + follow=False) + self.assertEqual(result.status_code, 404) # it is not possible, so get 404 + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': self.part1.slug + }), + follow=False) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }), + follow=False) + self.assertEqual(result.status_code, 404) + + # check access with old slug and version + result = self.client.get( + reverse('content:view', args=[tuto.pk, old_slug_tuto]) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': self.part1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': self.part1.slug, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + # edit container: + old_slug_part = self.part1.slug + result = self.client.post( + reverse('content:edit-container', kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': self.part1.slug + }), + { + 'title': random, + 'introduction': random, + 'conclusion': random + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # we can still access to the container using old slug ! + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + # and even to it using version 1 and old tuto slug !! + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + # but you can also access it with the current slug (for retro-compatibility) + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) From c8e75a45ae996f73195b185f20065c8491bbf979 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 1 Apr 2015 22:28:49 -0400 Subject: [PATCH 175/887] Ajoute un morceau de test sur le cas de la suppression --- zds/tutorialv2/tests/tests_views.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 365dc87303..8f32c94459 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -264,6 +264,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(versioned.get_conclusion(), random) self.assertEqual(versioned.description, random) self.assertEqual(versioned.licence.pk, new_licence.pk) + self.assertNotEqual(versioned.slug, slug) slug = tuto.slug # make the title change also change the slug !! @@ -292,6 +293,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit container: + old_slug_container = container.slug result = self.client.post( reverse('content:edit-container', kwargs={'pk': pk, 'slug': slug, 'container_slug': container.slug}), { @@ -307,6 +309,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(container.title, random) self.assertEqual(container.get_introduction(), random) self.assertEqual(container.get_conclusion(), random) + self.assertNotEqual(container.slug, old_slug_container) # add a subcontainer result = self.client.post( @@ -339,6 +342,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit subcontainer: + old_slug_subcontainer = subcontainer.slug result = self.client.post( reverse('content:edit-container', kwargs={ @@ -360,6 +364,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(subcontainer.title, random) self.assertEqual(subcontainer.get_introduction(), random) self.assertEqual(subcontainer.get_conclusion(), random) + self.assertNotEqual(subcontainer.slug, old_slug_subcontainer) # add extract to subcontainer: result = self.client.post( @@ -396,6 +401,7 @@ def test_basic_tutorial_workflow(self): self.assertEqual(result.status_code, 200) # edit extract: + old_slug_extract = extract.slug result = self.client.post( reverse('content:edit-extract', kwargs={ @@ -416,6 +422,7 @@ def test_basic_tutorial_workflow(self): extract = versioned.children[0].children[0].children[0] self.assertEqual(extract.title, random) self.assertEqual(extract.get_text(), random) + self.assertNotEqual(old_slug_extract, extract.slug) # then, delete extract: result = self.client.get( @@ -1210,6 +1217,11 @@ def test_history_navigation(self): follow=False) self.assertEqual(result.status_code, 302) + tuto = PublishableContent.objects.get(pk=self.tuto.pk) + version_3 = tuto.sha_draft # "version 3" is the one with the modified part + versioned = tuto.load_version() + current_slug_part = versioned.children[0].slug + # we can still access to the container using old slug ! result = self.client.get( reverse('content:view-container', @@ -1276,6 +1288,83 @@ def test_history_navigation(self): follow=False) self.assertEqual(result.status_code, 200) + # delete part + result = self.client.post( + reverse('content:delete', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'object_slug': current_slug_part + }), + follow=False) + self.assertEqual(result.status_code, 302) + + # we can still access to the part in version 3: + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': current_slug_part + }) + '?version=' + version_3, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': current_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_3, + follow=False) + + # version 2: + self.assertEqual(result.status_code, 200) + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'container_slug': old_slug_part + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': tuto.slug, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_2, + follow=False) + self.assertEqual(result.status_code, 200) + + # version 1: + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'container_slug': old_slug_part + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('content:view-container', + kwargs={ + 'pk': tuto.pk, + 'slug': old_slug_tuto, + 'parent_container_slug': old_slug_part, + 'container_slug': self.chapter1.slug + }) + '?version=' + version_1, + follow=False) + self.assertEqual(result.status_code, 200) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) From f33738b7f4bacef65399a1df3fbeef58fa931e13 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Thu, 2 Apr 2015 14:08:11 +0200 Subject: [PATCH 176/887] =?UTF-8?q?int=C3=A9gration=20compl=C3=A8te=20du?= =?UTF-8?q?=20d=C3=A9placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/includes/child.part.html | 17 ++++++++++++++++- zds/tutorialv2/models.py | 9 ++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/templates/tutorialv2/includes/child.part.html b/templates/tutorialv2/includes/child.part.html index a30c23b4b1..35154463a7 100644 --- a/templates/tutorialv2/includes/child.part.html +++ b/templates/tutorialv2/includes/child.part.html @@ -1,6 +1,7 @@ {% load emarkdown %} {% load i18n %} - +{% load times %} +{% load target_tree %}

    {% if child.position_in_parent < child.parent.children|length %} {% endif %} + + {% for element in child|target_tree %} + + {% endfor %} + + {% for element in child|target_tree %} + + {% endfor %} diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 8976dbfa6a..6496dd3963 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -982,9 +982,12 @@ def commit_changes(self, commit_message): return cm.hexsha def change_child_directory(self, child, adoptive_parent): - - old_path = child.get_path(False) # absolute path because we want to access the adress - adoptive_parent_path = adoptive_parent.get_path(False) + """ + Move an element of this content to a new location. + This method changes the repository index and stage every change but does **not** commit. + :param child: the child we want to move, can be either an Extract or a Container object + :param adoptive_parent: the container where the child *will be* moved, must be a Container object + """ if isinstance(child, Extract): old_parent = child.container old_parent.children = [c for c in old_parent.children if c.slug != child.slug] From f6bdef80520d170a349e82fbb8c377dfd3c088f1 Mon Sep 17 00:00:00 2001 From: Francois Dambrine Date: Fri, 3 Apr 2015 09:00:04 +0200 Subject: [PATCH 177/887] ajout du filtre target_tree --- zds/utils/templatetags/target_tree.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 zds/utils/templatetags/target_tree.py diff --git a/zds/utils/templatetags/target_tree.py b/zds/utils/templatetags/target_tree.py new file mode 100644 index 0000000000..873500838a --- /dev/null +++ b/zds/utils/templatetags/target_tree.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +from zds.tutorialv2.utils import get_target_tagged_tree +from zds.tutorialv2.models import Extract, Container +from django import template + + +register = template.Library() +@register.filter('target_tree') +def target_tree(child): + """ + A django filter that wrap zds.tutorialv2.utils.get_target_tagged_tree function + """ + if isinstance(child, Container): + root = child.top_container() + elif isinstance(child, Extract): + root = child.container.top_container() + return get_target_tagged_tree(child, root) From 231fa834eeed6413f5bafe489222a507fce1278a Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 4 Apr 2015 11:35:00 +0200 Subject: [PATCH 178/887] gros avancement dans la documentation --- doc/source/back-end-code/tutorialv2.rst | 31 +++++ doc/source/back-end/maniveste_contenu.rst | 160 ++++++++++++++++++++++ doc/source/back-end/tutorialv2.rst | 151 ++++++++++++++++++++ zds/tutorialv2/models.py | 22 ++- zds/tutorialv2/utils.py | 10 +- zds/utils/templatetags/target_tree.py | 2 + zds/utils/templatetags/times.py | 4 +- 7 files changed, 371 insertions(+), 9 deletions(-) create mode 100644 doc/source/back-end-code/tutorialv2.rst create mode 100644 doc/source/back-end/maniveste_contenu.rst create mode 100644 doc/source/back-end/tutorialv2.rst diff --git a/doc/source/back-end-code/tutorialv2.rst b/doc/source/back-end-code/tutorialv2.rst new file mode 100644 index 0000000000..480938f77f --- /dev/null +++ b/doc/source/back-end-code/tutorialv2.rst @@ -0,0 +1,31 @@ +========================================== +Les tutoriels v2 (ZEP12) (``tutorialv2/``) +========================================== + +Module situé dans ``zds/tutorialv2/``. + +.. contents:: Fichiers documentés : + +Modèles (``models.py``) +======================= + +.. automodule:: zds.tutorialv2.models + :members: + +Vues (``views.py``) +=================== + +.. automodule:: zds.tutorialv2.views + :members: + +Les importations (``importation.py``) +===================================== + +.. automodule:: zds.tutorialv2.importation + :members: + +Les forumulaires (``forms.py``) +=============================== + +.. automodule:: zds.tutorialv2.forms + :members: diff --git a/doc/source/back-end/maniveste_contenu.rst b/doc/source/back-end/maniveste_contenu.rst new file mode 100644 index 0000000000..daa4e281e4 --- /dev/null +++ b/doc/source/back-end/maniveste_contenu.rst @@ -0,0 +1,160 @@ +========================= +Les fichiers de manifeste +========================= + +Chaque contenu publiable (tutoriel, article) est décrit par un fichier de manifeste écrit au format json. + +Ce fichier de manifeste a pour but d'exprimer, versionner et instancier les informations et méta information du contenu tout au long du workflow de publication. + +Les informations en question sont l'architecture, les titres, les liens vers les sources, les informations de license ainsi que la version du fichier de manifeste lui même. + +Le fichier de manifeste est intrasèquement lié à un interpréteur qui est inclus dans le module de contenu associé. + +Les versions du manifeste +========================= + +Nomenclature +------------ + +La version du manifeste (et de son interpréteur) suit une nomenclature du type "sementic version", c'est à dire que la version est séparée en 3 parties selon le format v X.Y.Z + +- X numéro de version majeur +- Y numéro de version à ajout fonctionnel mineur +- Z numéro de version de correction de bug + +Plus précisément : + +- L'incrémentation de X signifie que la nouvelle version est potentiellement incompatible avec la version X-1. Un outil de migration doit alors être créé. +- L'incrémentation de Y signifie que la nouvelle version est possède une compatibilité descendante avec la version X.Y-1 mais que la compatibilité ascendante n'est pas assurée. C'est à dire que le nouvel interpréteur peut interpréter un manifeste de type X.Y-1 +mais l'ancien interpréteur ne peut pas interpréter un manifeste X.Y. Le cas typique d'incrémentation de Y est le passage d'obligatoire à optionnel d'un champ du manifeste. +- L'incrémentation de Z assure la compatibilité ascendante ET descendante, les cas typiques d'incrémentation de Z est l'ajout d'un champ optionnel au manifeste. + + Sauf cas exceptionnel, la numérotation de X commence à 1, la numérotation de Y commence à 0, la numérotation de Z commence à 0. + + La version du manifeste est donnée par le champ éponyme situé à la racine du manifeste ( ```{ version="2.0.0"}```). + L'absence du champ version est interprétée comme ``̀{version="1.0.0"}```. + Les 0 non significatifs sont optionnels ainsi ```{version="1"}``` est strictement équivalent à ```{version:"1.0"}``` lui même strictement équivalent à ```{version:"1.0.0"}```. + +Version 1.0 +----------- + +La version 1.0 définit trois types de manifeste selon que nous faisons face à un article, un mini tutoriel ou un big tutoriel. + +MINI TUTO ++++++++++ + +.. sourcecode:: json + + { + "title": "Mon Tutoriel No10", + "description": "Description du Tutoriel No10", + "type": "MINI", + "introduction": "introduction.md", + "conclusion": "conclusion.md" + } + +BIG TUTO +++++++++ + +.. sourcecode:: json + + { + "title": "3D temps réel avec Irrlicht", + "description": "3D temps réel avec Irrlicht", + "type": "BIG", + "licence": "Tous droits réservés", + "introduction": "introduction.md", + "conclusion": "conclusion.md", + "parts": [ + { + "pk": 7, + "title": "Chapitres de base", + "introduction": "7_chapitres-de-base/introduction.md", + "conclusion": "7_chapitres-de-base/conclusion.md", + "chapters": [ + { + "pk": 25, + "title": "Introduction", + "introduction": "7_chapitres-de-base/25_introduction/introduction.md", + "conclusion": "7_chapitres-de-base/25_introduction/conclusion.md", + "extracts": [ + { + "pk": 87, + "title": "Ce qu'est un moteur 3D", + "text": "7_chapitres-de-base/25_introduction/87_ce-quest-un-moteur-3d.md" + }, + { + "pk": 88, + "title": "Irrlicht", + "text": "7_chapitres-de-base/25_introduction/88_irrlicht.md" + } + ] + },(...) + ] + }, (...) + ] + } + +Article ++++++++ + +.. sourcecode:: json + + { + "title": "Mon Article No5", + "description": "Description de l'article No5", + "type": "article", + "text": "text.md" + } + + +Version 2.0 +----------- + +.. sourcecode:: json + + { + version : 2, + type : "TUTORIAL", + description : "description du tutorial", + title : "titre du tutorial", + slug : "titre-du-tutorial", + introduction : "introduction.md", + conclusion : "conclusion.md", + licence : "CC-BY-SA", + children : [ + { + object : "container", + title : "Titre de mon chapitre", + slug : "titre-de-mon-chapitre", + introduction : "titre-de-mon-chapitre/introduction.md", + conclusion : "titre-de-mon-chapitre/conclusion.md", + children : [ + { + object : "extract", + title : "titre de mon extrait", + slug : "titre-de-mon-extrait", + text : "titre-de-mon-extrait.md" + }, + (...) + ] + }, + (...) + ] + } + +1. type: Le type de contenu, vaut "TUTORIAL" ou "ARTICLE", **obligatoire** +2. description : La description du contenu, est affichée comme sous titre dans la page finale, **obligatoire** +3. title : Le titre du contenu, **obligatoire** +4. slug : slug du tutoriel qui permet de faire une url SEO friendly, **obligatoire** : ATENTION si ce slug existe déjà sur notre base de données, il est possible qu'un nombre lui soit ajouté +5. introduction: le nom du fichier .md qui possède l'introduction, il doit pointer vers le dossier courant. *optionnel mais conseillé* +6. conclusion: le nom du fichier .md qui possède la conclusion, il doit pointer vers le dossier courant. *optionnel mais conseillé* +7. licence: nom complet de la license. A priori les licences CC et Tous drois réservés sont supportés. le support de toute autre licence dépendra du site utilisant le code de zds (fork) que vous visez. **obligatoire** +8. children : tableau contenant l'architecture du contenu **obligatoire** + 1. object : type d'enfant (container ou extract selon que c'est une section ou un texte) **obligatoire** + 2. title: le titre de l'enfant **obligatoire** + 3. slug: le slug de l'enfant pour créer une url SEO friendly, doit être unique dans le tutoriel, le slug est utilisé pour trouver le chemin vers l'enfant dans le système de fichier si c'est une section.**obligatoire** + 4. introduction: nom du fichier contenant l'introduction quand l'enfant est de type container *optionnel mais conseillé* + 5. conclusion: nom du fichier contenant la conclusion quand l'enfant est de type container *optionnel mais conseillé* + 6. children: tableau vers les enfants de niveau inférieur si l'enfant est de type container **obligatoire** + 7. text: nom du fichier contenant le texte quand l'enfant est de type extract, nous conseillons de garder la convention nom de fichier = slug.md mais rien n'est obligatoire **obligatoire** diff --git a/doc/source/back-end/tutorialv2.rst b/doc/source/back-end/tutorialv2.rst new file mode 100644 index 0000000000..7e9b50dabb --- /dev/null +++ b/doc/source/back-end/tutorialv2.rst @@ -0,0 +1,151 @@ +======================================= +Les tutoriels et articles v2.0 (ZEP 12) +======================================= + +Vocabulaire et définitions +========================== + +- **Contenu** (*content*): désigne, de manière générale, quelque chose qui peut être produit et édité sur ZdS, c'est à dire un article ou un tutoriel. Tout contenu sur ZdS possède un dossier qui lui est propre et dont le contenu est expliqué plus loin. Ce dossier contient des informations sur le contenu lui-même (*metadata*, par exemple une description, une licence, ...) et des textes, organisés dans une arborescence bien précise. +- **Article** : contenu, généralement cours, visant à faire découvrir un sujet plus qu'à l'expliquer (ex: découverte d'une nouvelle technique, "c'est toute une histoire", ...) ou a donner un état des lieux sur un sujet donné de manière concises (ex: statistiques sur le ZdS, nouvelle version d'un programme, actualité, ...) +- **Tutoriel** : contenu, en général plus long, qui a pour vocation d'apprendre quelque chose ou d'informer de manière plus complète sur des points précis. +- **GIT**: système de versionnage employé (entre autre) par ZdS. Il permet de faire cohexister différentes versions d'un contenu de manière simple et transparente pour l'auteur. +- **Version** : toute modification apportée sur un contenu (ou une de ces partie) génère une nouvelle version de celui-ci, qui est indentifiée par un *hash*, c'est à dire une chaine de caractère de 20 caractère de long (aussi appellée *sha* en référence à l'algorithme qui sert à les générer) et qui identifie de manière unique cette version parmi toutes celles que le contenu contient. ZdS peut également identifier certaines versions de manière spécifique : ainsi, on peut distinguer la version brouillon (*draft*), la version bêta (*beta*), la version en validation (*validation*) et la version publié (*public*). ZdS retient tout simplement les *hash* associés à ces différentes versions du tutoriel pour les identifier comme telles. +- Fichier **manifest.json** : fichier à la racine de tout contenu et qui décrit celui-ci de deux manières : par des informations factuelles sur le contenu en lui-même (*metadata*) et par son arborescence. Le contenu de ce fichier est détaillé plus loin. On retiendra qu'à chaque version correspond en général un fichier manifest.json donné dont le contenu peut fortement changer d'une version à l'autre. +- **Conteneur** (*container*) : sous-structure d'un contenu. Voir plus loin. +- **Extrait** (*extract*) : base atomique (plus petite unité) d'un contenu. Voir plus loin. + +De la structure générale d'un contenu +===================================== + +Un **contenu** est un conteneur avec des métadonnées (*metadata*, décrites ci-dessous). On peut également le désigner sous le nom de **conteneur principal** (il n'as pas de conteneur parent). Pour différencier articles et tutoriels, une de ces métadonnées est le type (*type*). + +Un **conteneur** (de manière générale) peut posséder un conteneur parent et des enfants (*children*). Ces enfants peuvent être eux-même des conteneurs ou des extraits. Il a donc pour rôle de regrouper différents éléments. Outre des enfants, un conteneur possède un titre (*title*), une introduction (*introduction*), une conclusion (*conclusion*). On notera qu'un conteneur ne peut pas posséder pour enfant des conteneur ET des extraits. + +Un **extrait** est une unité de texte. Il possède un titre (*title*) et un texte (*text*). + +Tout les textes sont formatés en *markdown* (dans la version définie par le ZdS, avec les ajouts). + +Conteneur et extraits sont des **objets** (*object*). Dès lors, ils possèdent tout deux un *slug* (litérallement, "limace") : il s'agit d'une chaine de caractère généré à partir du titre de l'objet et qui, tout en restant lisible par un être humain, le simplifie considérablement : un *slug* est uniquement composé de caractères alphanumériques minuscules et non-accentués (`[a-z0-9]*`) ainsi que des caractères `-` (tiret) et `_` (*underscore*)[^underscore]. Ce *slug* a deux utilités : il est employé dans l'URL permetant d'accéder à l'objet et dans le nom de fichier/dossier employer pour le stocker. Dès lors, cette spécification **impose** que ce *slug* soit unique au sein du conteneur parent, et que le *slug* du contenu soit unique au sein de tout les contenu de ZdS (ce qui ne signifie pas que tout les slugs doivent être uniques, tant que ces deux règles sont respectées). + +[^underscore]: à noter que l'*underscore* est conservé par compatibilité avec l'ancien système, les nouveaux *slugs* générés par le système d'édition de ZdS n'en contiendront pas. + +.. attention:: + + Lors du déplacement d'un conteneur ou d'un extrait, les slugs sont modifiés de manière à ce qu'il n'y aie pas de colision. + + À noter que le slug doit être différent de celui donné au nom des introductions et des conclusions éventuelles. L'implémentation du ZdS considère que ceux-ci sont `introduction` et `conclusion`, mais ce n'est pas obligatoire. + +En fonction de sa position dans l'arborescence du contenu, un conteneur peut aussi bien représenter le tutoriel/article lui-même (s'il est conteneur principal), une partie ou un chapitre. Ainsi, dans l'exemple suivant : + +.. sourcecode:: text + + Contenu (tutoriel) + | + +-- Conteneur 1 + | + +-- Extrait 1 + + +le ``Conteneur 1`` sera rendu par ZdS comme étant un chapitre d'un (moyen-) tutoriel, et dans l'exemple suivant : + +.. sourcecode:: text + + Contenu (tutoriel) + | + +-- Conteneur 1 + | + +-- Conteneur 2 + | | + | +-- Extrait 1 + | + +-- Conteneur 3 + | + +-- Extrait 1 + + +le `Conteneur 1` sera rendu par ZdS comme étant une partie d'un (big-) tutoriel, et `Conteneur 2` et `Conteneur 3` comme étant les chapitres de cette partie. + +Les deux exemples donnés plus haut reprennent l'arboresence typique d'un contenu : Conteneur principal-[conteneur]~*n*~-extraits (ou *n* peut être nul). En fonction de la profondeur de l'arborescence (plus grande distance entre un conteneur enfant et le conteneur principal), le contenu sera nommé de manière différente. S'il s'agit d'un contenu de type tutoriel, on distinguera : + ++ **Mini-tutoriel** : le contenu n'as que des extraits pour enfant (au minimum 1) ; ++ **Moyen-tutoriel** : le contenu a au moins un conteneur pour enfant. Ces conteneurs sont pour ZdS des chapitres ; ++ **Big-tutoriel** : le contenu a un ou plusieurs conteneur-enfants (considéré par ZdS comme des parties), dont au moins 1 possède un conteneur enfant (considérés par Zds comme des chapitres). + +Il n'est pas possible de créer une arborescence à plus de 2 niveaux (parce que ça n'as pas de sens). + +On considère qu'un article est l'équivalent d'un mini-tutoriel, mais dont le but est différent (voir ci-dessus). + +Aspects techniques et fonctionnels +================================== + +Métadonnées d'un contenu +------------------------ + +On distingue actuelement deux types de métadonnées (*metadata*) : celles qui sont versionnées (et donc reprises dans le manifest.json) et celle qui ne le sont pas. La liste exhaustive de ces dernière (à l'heure actuelle) est la suivante : + ++ Les *hash* des différentes version du tutoriels (`sha_draft`, `sha_beta`, `sha_public` et `sha_validation`) ; ++ Les auteurs du contenu ; ++ Les catégories auquel appartient le contenu ; ++ La miniature ; ++ La source du contenu si elle n'as pas été rédigée sur ZdS mais importée avec une licence compatible ; ++ La présence ou pas de JSFiddle dans le contenu ; ++ Différentes informations temporelles : date de création (`creation_date`), de publication (`pubdate`) et de dernière modification (`update_date`). + +Ces différentes informations sont stockées dans la base de donnée, au travers du modèle `PublishableContent`. Pour des raisons de facilité, certaines des métadonnées versionnées sont également stockée en base de donnée : le titre, le type de contenu, la licence et la description. En ce qui concerne la version de celle-ci, c'est TOUJOURS celle correspondant **à la version brouillon** qui sont stockées, il ne faut donc **en aucun cas** les employer pour résoudre une URL ou à travers une template correspondant à la version publiée. + +Les métadonnées versionnées sont stockées dans le fichier manifest.json + + +Le stockage en pratique +----------------------- + +Comme énoncé plus haut, chaque contenu possède un dossier qui lui est propre (dont le nom est le slug du contenu), stocké dans l'endroit défini par la variable `ZDS_APP['content']['repo_path']`. Dans ce dossier ce trouve le fichier manifest.json. + +Pour chaque conteneur, un dossier est créé, qui contient les éventuels fichiers correspondants aux introduction, conclusion et différents extraits, ainsi que des dossiers pour les éventuels conteneurs enfants. Il s'agit de la forme d'un contenu tel que généré par ZdS en utilisant l'éditeur intégré. + +Il est demandé de se conformer un maximum à cette structure pour éviter les mauvaises surprises en cas d'édition externe (voir ci-dessous). + +Éventuelle édition externe +-------------------------- + +Actuellement, l'importation est possible sous forme principalement d'un POC à l'aide d'un fichier ZIP. Ce mécanisme doit être conservé mais peut être étendu : ne plus être lié à la base de donnée pour autre chose que les métadonnées du contenu externe permet une simplification considérable de l'édition hors-ligne (entre autre, la possibilité d'ajouter ou de supprimer comme bon semble à l'auteur). + +Au maximum, ce système tentera d'être compréhensif envers une arborescence qui serait différente de celle énoncée ci-dessous. Par contre **l'importation réorganisera les fichiers importés de la manière décrite ci-dessus**, afin de parer aux mauvaises surprises. + +Tout contenu qui ne correspond pas aux règles précisées ci-dessus ne sera pas ré-importable. Ne sera pas ré-importable non plus un contenu dont les fichiers indiqués dans le manifest.json n'existent pas ou sont incorrects. Seront supprimés les fichiers qui seraient inutiles (images, qui actuelement doivent être importées séparément dans une gallerie, autres fichiers supplémentaires, pour des raisons élémentaire de sécurité). + +Publication d'un contenu ("mise en production") +=============================================== + +Processus de publication +------------------------ + +Apès avoir passé les étapes de validations (`détaillées ailleurs`_), le contenu est près à être publié. Cette action +est effectuée par un membre du staff. Le but de la publication est +double : permettre aux visiteurs de lire le contenu, mais aussi +d’effectuer certains traitements (détaillés par après) afin que celui-ci +soit sous une forme qui soit plus rapidement affichable par ZdS. C’est +pourquoi ces contenus ne sont pas stockés au même endroit (voir +``ZDS_AP['content']['repo_public_path']`` + +.. _détaillées ailleurs: http://zds-site.readthedocs.org/fr/latest/tutorial/tutorial.html#cycle-de-vie-des-tutoriels + +). + +La mise en production se passe comme suis : + +1. S'il s'agit d'un nouveau contenu (jamais publié), un dossier dont le nom est le slug du contenu est créé. Dans le cas contraire, le contenu de ce dossier est entièrement effacé. +2. Le manifest.json correspondant à la version de validation (`sha_publication`) est copié dans ce dossier. Il servira principalement à valider les URLs, créer le sommaire et gérer le comportement des boutons "précédents" et "suivants" dans les conteneur dont les enfants sont des extraits (voir ci-dessous). +3. L'arborescence des dossiers est conservée pour les conteneur dont les enfants sont des conteneur, et leur éventuelles introduction et conclusion sont parsé en HTML. À l'inverse, pour les conteneurs dont les enfants sont des extraits, un fichier HTML unique est créé, reprenant de manière continue la forme parsée de l'éventuelle introduction, des différents extraits dans l'ordre et de l'éventuelle conclusion. +4. Le `sha_public` est mis à jour dans la base de donnée et l'objet `Validation` est changé de même. + +Consultation d'un contenu publié +-------------------------------- + +On ne doit pas avoir à ce servir de GIT pour afficher la version publiée d'un contenu. + +Dès lors, deux cas se présentent : + ++ L'utilisateur consulte un conteneur dont les enfants sont eux-mêmes des conteneur (c'est à dire le conteneur principal ou une partie d'un big-tutoriel) : le manifest.json est employé pour générer le sommaire, comme c'est le cas actuelement, l'introduction et la conclusion sont également affichés. ++ L'utilisateur consulte un conteneur dont les enfants sont des extraits: le fichier HTML généré durant la mise en production est employé tel quel par la *template* correspondante, additionné de l'éventuelle possibilité de faire suivant/précédent (qui nécéssite la lecture du manifest.json) + diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 6496dd3963..1a4c9104fb 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -790,6 +790,23 @@ def repo_delete(self, commit_message='', do_commit=True): if do_commit: return self.container.top_container().commit_changes(commit_message) + def get_tree_depth(self): + """ + Represent the depth where this exrtact is found + Tree depth is no more than 3, because there is 3 levels for Containers +1 for extract : + - PublishableContent (0), + - Part (1), + - Chapter (2) + Note that `'max_tree_depth` is `2` to ensure that there is no more than 3 levels + :return: Tree depth + """ + depth = 1 + current = self.container + while current.parent is not None: + current = current.parent + depth += 1 + return depth + class VersionedContent(Container): """ @@ -988,6 +1005,7 @@ def change_child_directory(self, child, adoptive_parent): :param child: the child we want to move, can be either an Extract or a Container object :param adoptive_parent: the container where the child *will be* moved, must be a Container object """ + old_path = child.get_path(False) if isinstance(child, Extract): old_parent = child.container old_parent.children = [c for c in old_parent.children if c.slug != child.slug] @@ -996,11 +1014,11 @@ def change_child_directory(self, child, adoptive_parent): old_parent = child.parent old_parent.children = [c for c in old_parent.children if c.slug != child.slug] adoptive_parent.add_container(child) + self.repository.index.move([old_path, child.get_path(False)]) self.dump_json() - - + def get_content_from_json(json, sha, slug_last_draft): """ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 8437817873..4e56d81c6e 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -1,4 +1,5 @@ # coding: utf-8 + from django.http import Http404 from zds.tutorialv2.models import PublishableContent, ContentRead, Container, Extract @@ -163,7 +164,6 @@ def try_adopt_new_child(adoptive_parent, child): adoptive_parent.top_container().change_child_directory(child, adoptive_parent) - def get_target_tagged_tree(movable_child, root): """ Gets the tagged tree with deplacement availability @@ -189,10 +189,8 @@ def get_target_tagged_tree_for_extract(movable_child, root): target_tagged_tree = [] for child in root.traverse(False): if isinstance(child, Extract): - target_tagged_tree.append((child.get_full_slug(), child.title, child.get_tree_depth(), child != moveable_child)) - child.title, - child.container.get_tree_level() + 1, - child != movable_child)) + target_tagged_tree.append((child.get_full_slug(), + child.title, child.get_tree_depth(), child != movable_child)) else: target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), False)) @@ -209,7 +207,7 @@ def get_target_tagged_tree_for_container(movable_child, root): """ target_tagged_tree = [] for child in root.traverse(True): - composed_depth = child.get_tree_depth() + moveable_child.get_tree_depth() + composed_depth = child.get_tree_depth() + movable_child.get_tree_depth() enabled = composed_depth <= settings.ZDS_APP['content']['max_tree_depth'] target_tagged_tree.append((child.get_path(True), child.title, child.get_tree_depth(), child.title, child.get_tree_level(), diff --git a/zds/utils/templatetags/target_tree.py b/zds/utils/templatetags/target_tree.py index 873500838a..8a876a7a85 100644 --- a/zds/utils/templatetags/target_tree.py +++ b/zds/utils/templatetags/target_tree.py @@ -6,6 +6,8 @@ register = template.Library() + + @register.filter('target_tree') def target_tree(child): """ diff --git a/zds/utils/templatetags/times.py b/zds/utils/templatetags/times.py index 555f6fab2b..8f7e223af2 100644 --- a/zds/utils/templatetags/times.py +++ b/zds/utils/templatetags/times.py @@ -1,7 +1,9 @@ from django import template + register = template.Library() -@register.filter(name='times') + +@register.filter(name='times') def times(number): return range(number) From 121056430861fc4a3a023657d24b4b4e4b4f1cec Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Sat, 4 Apr 2015 09:08:01 -0400 Subject: [PATCH 179/887] Assure le comportement de intro et conclu si `None` --- doc/source/back-end-code/tutorialv2.rst | 6 + doc/source/back-end/maniveste_contenu.rst | 160 ---------------------- doc/source/back-end/tutorialv2.rst | 31 ++--- requirements.txt | 4 +- zds/tutorialv2/models.py | 2 +- zds/utils/templatetags/times.py | 1 - 6 files changed, 24 insertions(+), 180 deletions(-) delete mode 100644 doc/source/back-end/maniveste_contenu.rst diff --git a/doc/source/back-end-code/tutorialv2.rst b/doc/source/back-end-code/tutorialv2.rst index 480938f77f..c3fe5824ba 100644 --- a/doc/source/back-end-code/tutorialv2.rst +++ b/doc/source/back-end-code/tutorialv2.rst @@ -18,6 +18,12 @@ Vues (``views.py``) .. automodule:: zds.tutorialv2.views :members: +Mixins (``mixins.py``) +====================== + +.. automodule:: zds.tutorialv2.mixins + :members: + Les importations (``importation.py``) ===================================== diff --git a/doc/source/back-end/maniveste_contenu.rst b/doc/source/back-end/maniveste_contenu.rst deleted file mode 100644 index daa4e281e4..0000000000 --- a/doc/source/back-end/maniveste_contenu.rst +++ /dev/null @@ -1,160 +0,0 @@ -========================= -Les fichiers de manifeste -========================= - -Chaque contenu publiable (tutoriel, article) est décrit par un fichier de manifeste écrit au format json. - -Ce fichier de manifeste a pour but d'exprimer, versionner et instancier les informations et méta information du contenu tout au long du workflow de publication. - -Les informations en question sont l'architecture, les titres, les liens vers les sources, les informations de license ainsi que la version du fichier de manifeste lui même. - -Le fichier de manifeste est intrasèquement lié à un interpréteur qui est inclus dans le module de contenu associé. - -Les versions du manifeste -========================= - -Nomenclature ------------- - -La version du manifeste (et de son interpréteur) suit une nomenclature du type "sementic version", c'est à dire que la version est séparée en 3 parties selon le format v X.Y.Z - -- X numéro de version majeur -- Y numéro de version à ajout fonctionnel mineur -- Z numéro de version de correction de bug - -Plus précisément : - -- L'incrémentation de X signifie que la nouvelle version est potentiellement incompatible avec la version X-1. Un outil de migration doit alors être créé. -- L'incrémentation de Y signifie que la nouvelle version est possède une compatibilité descendante avec la version X.Y-1 mais que la compatibilité ascendante n'est pas assurée. C'est à dire que le nouvel interpréteur peut interpréter un manifeste de type X.Y-1 -mais l'ancien interpréteur ne peut pas interpréter un manifeste X.Y. Le cas typique d'incrémentation de Y est le passage d'obligatoire à optionnel d'un champ du manifeste. -- L'incrémentation de Z assure la compatibilité ascendante ET descendante, les cas typiques d'incrémentation de Z est l'ajout d'un champ optionnel au manifeste. - - Sauf cas exceptionnel, la numérotation de X commence à 1, la numérotation de Y commence à 0, la numérotation de Z commence à 0. - - La version du manifeste est donnée par le champ éponyme situé à la racine du manifeste ( ```{ version="2.0.0"}```). - L'absence du champ version est interprétée comme ``̀{version="1.0.0"}```. - Les 0 non significatifs sont optionnels ainsi ```{version="1"}``` est strictement équivalent à ```{version:"1.0"}``` lui même strictement équivalent à ```{version:"1.0.0"}```. - -Version 1.0 ------------ - -La version 1.0 définit trois types de manifeste selon que nous faisons face à un article, un mini tutoriel ou un big tutoriel. - -MINI TUTO -+++++++++ - -.. sourcecode:: json - - { - "title": "Mon Tutoriel No10", - "description": "Description du Tutoriel No10", - "type": "MINI", - "introduction": "introduction.md", - "conclusion": "conclusion.md" - } - -BIG TUTO -++++++++ - -.. sourcecode:: json - - { - "title": "3D temps réel avec Irrlicht", - "description": "3D temps réel avec Irrlicht", - "type": "BIG", - "licence": "Tous droits réservés", - "introduction": "introduction.md", - "conclusion": "conclusion.md", - "parts": [ - { - "pk": 7, - "title": "Chapitres de base", - "introduction": "7_chapitres-de-base/introduction.md", - "conclusion": "7_chapitres-de-base/conclusion.md", - "chapters": [ - { - "pk": 25, - "title": "Introduction", - "introduction": "7_chapitres-de-base/25_introduction/introduction.md", - "conclusion": "7_chapitres-de-base/25_introduction/conclusion.md", - "extracts": [ - { - "pk": 87, - "title": "Ce qu'est un moteur 3D", - "text": "7_chapitres-de-base/25_introduction/87_ce-quest-un-moteur-3d.md" - }, - { - "pk": 88, - "title": "Irrlicht", - "text": "7_chapitres-de-base/25_introduction/88_irrlicht.md" - } - ] - },(...) - ] - }, (...) - ] - } - -Article -+++++++ - -.. sourcecode:: json - - { - "title": "Mon Article No5", - "description": "Description de l'article No5", - "type": "article", - "text": "text.md" - } - - -Version 2.0 ------------ - -.. sourcecode:: json - - { - version : 2, - type : "TUTORIAL", - description : "description du tutorial", - title : "titre du tutorial", - slug : "titre-du-tutorial", - introduction : "introduction.md", - conclusion : "conclusion.md", - licence : "CC-BY-SA", - children : [ - { - object : "container", - title : "Titre de mon chapitre", - slug : "titre-de-mon-chapitre", - introduction : "titre-de-mon-chapitre/introduction.md", - conclusion : "titre-de-mon-chapitre/conclusion.md", - children : [ - { - object : "extract", - title : "titre de mon extrait", - slug : "titre-de-mon-extrait", - text : "titre-de-mon-extrait.md" - }, - (...) - ] - }, - (...) - ] - } - -1. type: Le type de contenu, vaut "TUTORIAL" ou "ARTICLE", **obligatoire** -2. description : La description du contenu, est affichée comme sous titre dans la page finale, **obligatoire** -3. title : Le titre du contenu, **obligatoire** -4. slug : slug du tutoriel qui permet de faire une url SEO friendly, **obligatoire** : ATENTION si ce slug existe déjà sur notre base de données, il est possible qu'un nombre lui soit ajouté -5. introduction: le nom du fichier .md qui possède l'introduction, il doit pointer vers le dossier courant. *optionnel mais conseillé* -6. conclusion: le nom du fichier .md qui possède la conclusion, il doit pointer vers le dossier courant. *optionnel mais conseillé* -7. licence: nom complet de la license. A priori les licences CC et Tous drois réservés sont supportés. le support de toute autre licence dépendra du site utilisant le code de zds (fork) que vous visez. **obligatoire** -8. children : tableau contenant l'architecture du contenu **obligatoire** - 1. object : type d'enfant (container ou extract selon que c'est une section ou un texte) **obligatoire** - 2. title: le titre de l'enfant **obligatoire** - 3. slug: le slug de l'enfant pour créer une url SEO friendly, doit être unique dans le tutoriel, le slug est utilisé pour trouver le chemin vers l'enfant dans le système de fichier si c'est une section.**obligatoire** - 4. introduction: nom du fichier contenant l'introduction quand l'enfant est de type container *optionnel mais conseillé* - 5. conclusion: nom du fichier contenant la conclusion quand l'enfant est de type container *optionnel mais conseillé* - 6. children: tableau vers les enfants de niveau inférieur si l'enfant est de type container **obligatoire** - 7. text: nom du fichier contenant le texte quand l'enfant est de type extract, nous conseillons de garder la convention nom de fichier = slug.md mais rien n'est obligatoire **obligatoire** diff --git a/doc/source/back-end/tutorialv2.rst b/doc/source/back-end/tutorialv2.rst index 7e9b50dabb..ba24459b13 100644 --- a/doc/source/back-end/tutorialv2.rst +++ b/doc/source/back-end/tutorialv2.rst @@ -25,15 +25,17 @@ Un **extrait** est une unité de texte. Il possède un titre (*title*) et un tex Tout les textes sont formatés en *markdown* (dans la version définie par le ZdS, avec les ajouts). -Conteneur et extraits sont des **objets** (*object*). Dès lors, ils possèdent tout deux un *slug* (litérallement, "limace") : il s'agit d'une chaine de caractère généré à partir du titre de l'objet et qui, tout en restant lisible par un être humain, le simplifie considérablement : un *slug* est uniquement composé de caractères alphanumériques minuscules et non-accentués (`[a-z0-9]*`) ainsi que des caractères `-` (tiret) et `_` (*underscore*)[^underscore]. Ce *slug* a deux utilités : il est employé dans l'URL permetant d'accéder à l'objet et dans le nom de fichier/dossier employer pour le stocker. Dès lors, cette spécification **impose** que ce *slug* soit unique au sein du conteneur parent, et que le *slug* du contenu soit unique au sein de tout les contenu de ZdS (ce qui ne signifie pas que tout les slugs doivent être uniques, tant que ces deux règles sont respectées). +Conteneur et extraits sont des **objets** (*object*). Dès lors, ils possèdent tout deux un *slug* (litérallement, "limace") : il s'agit d'une chaine de caractère généré à partir du titre de l'objet et qui, tout en restant lisible par un être humain, le simplifie considérablement : un *slug* est uniquement composé de caractères alphanumériques minuscules et non-accentués (``[a-z0-9]*``) ainsi que des caractères ``-`` (tiret) et ``_`` (*underscore*). Ce *slug* a deux utilités : il est employé dans l'URL permetant d'accéder à l'objet et dans le nom de fichier/dossier employer pour le stocker. Dès lors, cette spécification **impose** que ce *slug* soit unique au sein du conteneur parent, et que le *slug* du contenu soit unique au sein de tout les contenu de ZdS (ce qui ne signifie pas que tout les slugs doivent être uniques, tant que ces deux règles sont respectées). -[^underscore]: à noter que l'*underscore* est conservé par compatibilité avec l'ancien système, les nouveaux *slugs* générés par le système d'édition de ZdS n'en contiendront pas. +.. note:: + + À noter que l'*underscore* est conservé par compatibilité avec l'ancien système, les nouveaux *slugs* générés par le système d'édition de ZdS n'en contiendront pas. .. attention:: Lors du déplacement d'un conteneur ou d'un extrait, les slugs sont modifiés de manière à ce qu'il n'y aie pas de colision. - À noter que le slug doit être différent de celui donné au nom des introductions et des conclusions éventuelles. L'implémentation du ZdS considère que ceux-ci sont `introduction` et `conclusion`, mais ce n'est pas obligatoire. + À noter que le slug doit être différent de celui donné au nom des introductions et des conclusions éventuelles. L'implémentation du ZdS considère que ceux-ci sont ``introduction`` et ``conclusion``, mais ce n'est pas obligatoire. En fonction de sa position dans l'arborescence du contenu, un conteneur peut aussi bien représenter le tutoriel/article lui-même (s'il est conteneur principal), une partie ou un chapitre. Ainsi, dans l'exemple suivant : @@ -63,7 +65,7 @@ le ``Conteneur 1`` sera rendu par ZdS comme étant un chapitre d'un (moyen-) tut +-- Extrait 1 -le `Conteneur 1` sera rendu par ZdS comme étant une partie d'un (big-) tutoriel, et `Conteneur 2` et `Conteneur 3` comme étant les chapitres de cette partie. +le ``Conteneur 1`` sera rendu par ZdS comme étant une partie d'un (big-) tutoriel, et ``Conteneur 2`` et ``Conteneur 3`` comme étant les chapitres de cette partie. Les deux exemples donnés plus haut reprennent l'arboresence typique d'un contenu : Conteneur principal-[conteneur]~*n*~-extraits (ou *n* peut être nul). En fonction de la profondeur de l'arborescence (plus grande distance entre un conteneur enfant et le conteneur principal), le contenu sera nommé de manière différente. S'il s'agit d'un contenu de type tutoriel, on distinguera : @@ -83,15 +85,15 @@ Métadonnées d'un contenu On distingue actuelement deux types de métadonnées (*metadata*) : celles qui sont versionnées (et donc reprises dans le manifest.json) et celle qui ne le sont pas. La liste exhaustive de ces dernière (à l'heure actuelle) est la suivante : -+ Les *hash* des différentes version du tutoriels (`sha_draft`, `sha_beta`, `sha_public` et `sha_validation`) ; ++ Les *hash* des différentes version du tutoriels (``sha_draft``, ``sha_beta``, ``sha_public`` et ``sha_validation``) ; + Les auteurs du contenu ; + Les catégories auquel appartient le contenu ; + La miniature ; + La source du contenu si elle n'as pas été rédigée sur ZdS mais importée avec une licence compatible ; + La présence ou pas de JSFiddle dans le contenu ; -+ Différentes informations temporelles : date de création (`creation_date`), de publication (`pubdate`) et de dernière modification (`update_date`). ++ Différentes informations temporelles : date de création (``creation_date``), de publication (``pubdate``) et de dernière modification (``update_date``). -Ces différentes informations sont stockées dans la base de donnée, au travers du modèle `PublishableContent`. Pour des raisons de facilité, certaines des métadonnées versionnées sont également stockée en base de donnée : le titre, le type de contenu, la licence et la description. En ce qui concerne la version de celle-ci, c'est TOUJOURS celle correspondant **à la version brouillon** qui sont stockées, il ne faut donc **en aucun cas** les employer pour résoudre une URL ou à travers une template correspondant à la version publiée. +Ces différentes informations sont stockées dans la base de donnée, au travers du modèle ``PublishableContent``. Pour des raisons de facilité, certaines des métadonnées versionnées sont également stockée en base de donnée : le titre, le type de contenu, la licence et la description. En ce qui concerne la version de celle-ci, c'est TOUJOURS celle correspondant **à la version brouillon** qui sont stockées, il ne faut donc **en aucun cas** les employer pour résoudre une URL ou à travers une template correspondant à la version publiée. Les métadonnées versionnées sont stockées dans le fichier manifest.json @@ -99,7 +101,7 @@ Les métadonnées versionnées sont stockées dans le fichier manifest.json Le stockage en pratique ----------------------- -Comme énoncé plus haut, chaque contenu possède un dossier qui lui est propre (dont le nom est le slug du contenu), stocké dans l'endroit défini par la variable `ZDS_APP['content']['repo_path']`. Dans ce dossier ce trouve le fichier manifest.json. +Comme énoncé plus haut, chaque contenu possède un dossier qui lui est propre (dont le nom est le slug du contenu), stocké dans l'endroit défini par la variable ``ZDS_APP['content']['repo_path']``. Dans ce dossier ce trouve le fichier manifest.json. Pour chaque conteneur, un dossier est créé, qui contient les éventuels fichiers correspondants aux introduction, conclusion et différents extraits, ainsi que des dossiers pour les éventuels conteneurs enfants. Il s'agit de la forme d'un contenu tel que généré par ZdS en utilisant l'éditeur intégré. @@ -120,24 +122,19 @@ Publication d'un contenu ("mise en production") Processus de publication ------------------------ -Apès avoir passé les étapes de validations (`détaillées ailleurs`_), le contenu est près à être publié. Cette action +Apès avoir passé les étapes de validations (`détaillées ailleurs <./tutorial.html#cycle-de-vie-des-tutoriels>`__), le contenu est près à être publié. Cette action est effectuée par un membre du staff. Le but de la publication est double : permettre aux visiteurs de lire le contenu, mais aussi d’effectuer certains traitements (détaillés par après) afin que celui-ci soit sous une forme qui soit plus rapidement affichable par ZdS. C’est -pourquoi ces contenus ne sont pas stockés au même endroit (voir -``ZDS_AP['content']['repo_public_path']`` - -.. _détaillées ailleurs: http://zds-site.readthedocs.org/fr/latest/tutorial/tutorial.html#cycle-de-vie-des-tutoriels - -). +pourquoi ces contenus ne sont pas stockés au même endroit (voir ``ZDS_AP['content']['repo_public_path']``). La mise en production se passe comme suis : 1. S'il s'agit d'un nouveau contenu (jamais publié), un dossier dont le nom est le slug du contenu est créé. Dans le cas contraire, le contenu de ce dossier est entièrement effacé. -2. Le manifest.json correspondant à la version de validation (`sha_publication`) est copié dans ce dossier. Il servira principalement à valider les URLs, créer le sommaire et gérer le comportement des boutons "précédents" et "suivants" dans les conteneur dont les enfants sont des extraits (voir ci-dessous). +2. Le manifest.json correspondant à la version de validation (``sha_publication``) est copié dans ce dossier. Il servira principalement à valider les URLs, créer le sommaire et gérer le comportement des boutons "précédents" et "suivants" dans les conteneur dont les enfants sont des extraits (voir ci-dessous). 3. L'arborescence des dossiers est conservée pour les conteneur dont les enfants sont des conteneur, et leur éventuelles introduction et conclusion sont parsé en HTML. À l'inverse, pour les conteneurs dont les enfants sont des extraits, un fichier HTML unique est créé, reprenant de manière continue la forme parsée de l'éventuelle introduction, des différents extraits dans l'ordre et de l'éventuelle conclusion. -4. Le `sha_public` est mis à jour dans la base de donnée et l'objet `Validation` est changé de même. +4. Le ``sha_public`` est mis à jour dans la base de donnée et l'objet ``Validation`` est changé de même. Consultation d'un contenu publié -------------------------------- diff --git a/requirements.txt b/requirements.txt index eccee1c5d3..fdd03ce6d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,9 @@ djangorestframework==3.0.2 django-filter==0.8 django-oauth-toolkit==0.7.2 drf-extensions==0.2.6 +<<<<<<< HEAD +django-rest-swagger==0.2.8 +django-cors-headers==1.0.0 django-rest-swagger==0.2.9 django-cors-headers==1.0.0 - django-uuslug==1.0.3 diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 1a4c9104fb..ff5f11400d 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -1005,7 +1005,7 @@ def change_child_directory(self, child, adoptive_parent): :param child: the child we want to move, can be either an Extract or a Container object :param adoptive_parent: the container where the child *will be* moved, must be a Container object """ - old_path = child.get_path(False) + old_path = child.get_path(False) # absolute path because we want to access the address if isinstance(child, Extract): old_parent = child.container old_parent.children = [c for c in old_parent.children if c.slug != child.slug] diff --git a/zds/utils/templatetags/times.py b/zds/utils/templatetags/times.py index 8f7e223af2..e097f2cb59 100644 --- a/zds/utils/templatetags/times.py +++ b/zds/utils/templatetags/times.py @@ -1,6 +1,5 @@ from django import template - register = template.Library() From f50f3439824916ca64ae740a5351ecf7ed9d707b Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 4 Apr 2015 16:15:14 +0200 Subject: [PATCH 180/887] =?UTF-8?q?Corrige=20les=20erreurs=20de=20d=C3=A9p?= =?UTF-8?q?lacement=20au=20niveau=20des=20extraits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 11 +++++++++++ zds/utils/tutorialv2.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 8f32c94459..67ab889646 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -1,6 +1,7 @@ # coding: utf-8 import os +from os.path import isdir, isfile import shutil from django.conf import settings @@ -884,6 +885,7 @@ def test_move_container_before(self): self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) + self.extract5 = ExtractFactory(container=self.chapter3, db_object=self.tuto) tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft # test changing parent for container (smoothly) @@ -902,7 +904,12 @@ def test_move_container_before(self): self.assertNotEqual(old_sha, PublishableContent.objects.get(pk=tuto.pk).sha_draft) versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) + chapter = versioned.children_dict[self.part2.slug].children[0] + self.assertTrue(isdir(chapter.get_path())) + self.assertEqual(1, len(chapter.children)) + self.assertTrue(isfile(chapter.children[0].get_path())) + self.assertEqual(self.extract5.slug, chapter.children[0].slug) self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[1] self.assertEqual(self.chapter4.slug, chapter.slug) @@ -1038,6 +1045,7 @@ def test_move_container_after(self): self.chapter2 = ContainerFactory(parent=self.part1, db_object=self.tuto) self.chapter3 = ContainerFactory(parent=self.part1, db_object=self.tuto) self.part2 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) + self.extract5 = ExtractFactory(container=self.chapter3, db_object=self.tuto) self.chapter4 = ContainerFactory(parent=self.part2, db_object=self.tuto) tuto = PublishableContent.objects.get(pk=self.tuto.pk) old_sha = tuto.sha_draft @@ -1058,6 +1066,9 @@ def test_move_container_after(self): versioned = PublishableContent.objects.get(pk=tuto.pk).load_version() self.assertEqual(2, len(versioned.children_dict[self.part2.slug].children)) chapter = versioned.children_dict[self.part2.slug].children[1] + self.assertEqual(1, len(chapter.children)) + self.assertTrue(isfile(chapter.children[0].get_path())) + self.assertEqual(self.extract5.slug, chapter.children[0].slug) self.assertEqual(self.chapter3.slug, chapter.slug) chapter = versioned.children_dict[self.part2.slug].children[0] self.assertEqual(self.chapter4.slug, chapter.slug) diff --git a/zds/utils/tutorialv2.py b/zds/utils/tutorialv2.py index 15718e4605..bacc109a74 100644 --- a/zds/utils/tutorialv2.py +++ b/zds/utils/tutorialv2.py @@ -11,7 +11,7 @@ def export_extract(extract): dct['object'] = 'extract' dct['slug'] = extract.slug dct['title'] = extract.title - dct['text'] = extract.text + dct['text'] = extract.get_path(True) return dct From e078dfa7bdf6002d1e1b1998f41e46aa587e2ae0 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 4 Apr 2015 17:59:47 +0200 Subject: [PATCH 181/887] Corrections orthographiques --- doc/source/back-end/manifeste_contenu.rst | 50 +++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/doc/source/back-end/manifeste_contenu.rst b/doc/source/back-end/manifeste_contenu.rst index 2e8f6a7f2a..deb2a84503 100644 --- a/doc/source/back-end/manifeste_contenu.rst +++ b/doc/source/back-end/manifeste_contenu.rst @@ -2,13 +2,13 @@ Les fichiers de manifeste ========================= -Chaque contenu publiable (tutoriel, article) est décrit par un fichier de manifeste écrit au format json. +Chaque contenu publiable (tutoriel, article) est décrit par un fichier de manifeste écrit au format JSON. -Ce fichier de manifeste a pour but d'exprimer, versionner et instancier les informations et méta information du contenu tout au long du workflow de publication. +Ce fichier de manifeste a pour but d'exprimer, versionner et instancier les informations et méta informations du contenu tout au long du workflow de publication. -Les informations en question sont l'architecture, les titres, les liens vers les sources, les informations de license ainsi que la version du fichier de manifeste lui même. +Les informations en question sont l'architecture, les titres, les liens vers les sources, les informations de license ainsi que la version du fichier de manifeste lui-même. -Le fichier de manifeste est intrasèquement lié à un interpréteur qui est inclus dans le module de contenu associé. +Le fichier de manifeste est intrinsèquement lié à un interpréteur qui est inclus dans le module de contenu associé. Les versions du manifeste ========================= @@ -16,16 +16,16 @@ Les versions du manifeste Nomenclature ------------ -La version du manifeste (et de son interpréteur) suit une nomenclature du type "sementic version", c'est à dire que la version est séparée en 3 parties selon le format v X.Y.Z +La version du manifeste (et de son interpréteur) suit une nomenclature du type "semantic version" (SemVer), c'est-à-dire que la version est séparée en trois parties selon le format v X.Y.Z -- X numéro de version majeur -- Y numéro de version à ajout fonctionnel mineur -- Z numéro de version de correction de bug +- X : numéro de version majeur +- Y : numéro de version à ajout fonctionnel mineur +- Z : numéro de version de correction de bug Plus précisément : - L'incrémentation de X signifie que la nouvelle version est potentiellement incompatible avec la version X-1. Un outil de migration doit alors être créé. -- L'incrémentation de Y signifie que la nouvelle version est possède une compatibilité descendante avec la version X.Y-1 mais que la compatibilité ascendante n'est pas assurée. C'est à dire que le nouvel interpréteur peut interpréter un manifeste de type X.Y-1 +- L'incrémentation de Y signifie que la nouvelle version possède une compatibilité descendante avec la version X.Y-1 mais que la compatibilité ascendante n'est pas assurée. C'est-à-dire que le nouvel interpréteur peut interpréter un manifeste de type X.Y-1 mais l'ancien interpréteur ne peut pas interpréter un manifeste X.Y. Le cas typique d'incrémentation de Y est le passage d'obligatoire à optionnel d'un champ du manifeste. - L'incrémentation de Z assure la compatibilité ascendante ET descendante, les cas typiques d'incrémentation de Z est l'ajout d'un champ optionnel au manifeste. @@ -33,7 +33,7 @@ Sauf cas exceptionnel, la numérotation de X commence à 1, la numérotation de La version du manifeste est donnée par le champ éponyme situé à la racine du manifeste ( ```{ version="2.0.0"}```). L'absence du champ version est interprétée comme ``̀{version="1.0.0"}```. -Les 0 non significatifs sont optionnels ainsi ```{version="1"}``` est strictement équivalent à ```{version:"1.0"}``` lui même strictement équivalent à ```{version:"1.0.0"}```. +Les 0 non significatifs sont optionnels ainsi ```{version="1"}``` est strictement équivalent à ```{version:"1.0"}``` lui-même strictement équivalent à ```{version:"1.0.0"}```. Version 1.0 ----------- @@ -144,18 +144,18 @@ Version 2.0 ] } -1. type: Le type de contenu, vaut "TUTORIAL" ou "ARTICLE", **obligatoire** -2. description : La description du contenu, est affichée comme sous titre dans la page finale, **obligatoire** -3. title : Le titre du contenu, **obligatoire** -4. slug : slug du tutoriel qui permet de faire une url SEO friendly, **obligatoire** : ATENTION si ce slug existe déjà sur notre base de données, il est possible qu'un nombre lui soit ajouté -5. introduction: le nom du fichier .md qui possède l'introduction, il doit pointer vers le dossier courant. *optionnel mais conseillé* -6. conclusion: le nom du fichier .md qui possède la conclusion, il doit pointer vers le dossier courant. *optionnel mais conseillé* -7. licence: nom complet de la license. A priori les licences CC et Tous drois réservés sont supportés. le support de toute autre licence dépendra du site utilisant le code de zds (fork) que vous visez. **obligatoire** -8. children : tableau contenant l'architecture du contenu **obligatoire** - 1. object : type d'enfant (container ou extract selon que c'est une section ou un texte) **obligatoire** - 2. title: le titre de l'enfant **obligatoire** - 3. slug: le slug de l'enfant pour créer une url SEO friendly, doit être unique dans le tutoriel, le slug est utilisé pour trouver le chemin vers l'enfant dans le système de fichier si c'est une section.**obligatoire** - 4. introduction: nom du fichier contenant l'introduction quand l'enfant est de type container *optionnel mais conseillé* - 5. conclusion: nom du fichier contenant la conclusion quand l'enfant est de type container *optionnel mais conseillé* - 6. children: tableau vers les enfants de niveau inférieur si l'enfant est de type container **obligatoire** - 7. text: nom du fichier contenant le texte quand l'enfant est de type extract, nous conseillons de garder la convention nom de fichier = slug.md mais rien n'est obligatoire **obligatoire** \ No newline at end of file +1. ``type`` : Le type de contenu, vaut "TUTORIAL" ou "ARTICLE". **Obligatoire** +2. ``description`` : La description du contenu. Est affichée comme sous-titre dans la page finale. **Obligatoire** +3. ``title`` : Le titre du contenu. **Obligatoire** +4. ``slug`` : slug du tutoriel qui permet de faire une url SEO-friendly. **Obligatoire**. ATENTION : si ce slug existe déjà sur notre base de données, il est possible qu'un nombre lui soit ajouté +5. ``introduction`` : le nom du fichier .md qui possède l'introduction. Il doit pointer vers le dossier courant. *Optionnel mais conseillé* +6. ``conclusion`` : le nom du fichier .md qui possède la conclusion. Il doit pointer vers le dossier courant. *Optionnel mais conseillé* +7. ``licence`` : nom complet de la license. *A priori* les licences "CC" et "Tous drois réservés" sont supportées. Le support de toute autre licence dépendra du site utilisant le code de zds (fork) que vous visez. **Obligatoire** +8. ``children`` : tableau contenant l'architecture du contenu. **Obligatoire** + 1. ``object`` : type d'enfant (*container* ou *extract*, selon qu'il s'agit d'une section ou d'un texte). **Obligatoire** + 2. ``title`` : le titre de l'enfant. **Obligatoire** + 3. ``slug`` : le slug de l'enfant pour créer une url SEO friendly, doit être unique dans le tutoriel, le slug est utilisé pour trouver le chemin vers l'enfant dans le système de fichier si c'est une section.**obligatoire** + 4. ``introduction`` : nom du fichier contenant l'introduction quand l'enfant est de type *container*. *Optionnel mais conseillé* + 5. ``conclusion`` : nom du fichier contenant la conclusion quand l'enfant est de type *container*. *Optionnel mais conseillé* + 6. ``children`` : tableau vers les enfants de niveau inférieur si l'enfant est de type *container*. **Obligatoire** + 7. ``text`` : nom du fichier contenant le texte quand l'enfant est de type *extract*. Nous conseillons de garder la convention ``nom de fichier = slug.md`` mais rien n'est obligatoire à ce sujet. **Obligatoire** From 098ab0579daab3b84c8a868641d9f832089f7134 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 4 Apr 2015 18:26:37 +0200 Subject: [PATCH 182/887] =?UTF-8?q?autodoc=20op=C3=A9rationnelle=20sur=20z?= =?UTF-8?q?ep=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models.py | 198 ++++++++++++++++++++------------------- zds/tutorialv2/utils.py | 27 +++--- 2 files changed, 114 insertions(+), 111 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 7758e4950a..dc6bf960a6 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -101,8 +101,8 @@ def has_extracts(self): return isinstance(self.children[0], Extract) def has_sub_containers(self): - """ - Note : this function rely on the fact that the children can only be of one type. + """Note : this function rely on the fact that the children can only be of one type. + :return: `True` if the container has containers as children, `False` otherwise. """ if len(self.children) == 0: @@ -116,13 +116,14 @@ def get_last_child_position(self): return len(self.children) def get_tree_depth(self): - """ - Represent the depth where this container is found + """Represent the depth where this container is found Tree depth is no more than 2, because there is 3 levels for Containers : - PublishableContent (0), - Part (1), - Chapter (2) - Note that `'max_tree_depth` is `2` to ensure that there is no more than 3 levels + .. attention:: + that `'max_tree_depth` is `2` to ensure that there is no more than 3 levels + :return: Tree depth """ depth = 0 @@ -133,8 +134,8 @@ def get_tree_depth(self): return depth def get_tree_level(self): - """ - Represent the level in the tree of this container, i.e the depth of its deepest child + """Represent the level in the tree of this container, i.e the depth of its deepest child + :return: tree level """ @@ -146,9 +147,9 @@ def get_tree_level(self): return 1 + max([i.get_tree_level() for i in self.children]) def has_child_with_path(self, child_path): - """ - Check that the given path represent the full path + """Check that the given path represent the full path of a child of this container. + :param child_path: the full path (/maincontainer/subc1/subc2/childslug) we want to check """ if self.get_path(True) not in child_path: @@ -165,9 +166,9 @@ def top_container(self): return current def get_unique_slug(self, title): - """ - Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a + """Generate a slug from title, and check if it is already in slug pool. If it is the case, recursively add a "-x" to the end, where "x" is a number starting from 1. When generated, it is added to the slug pool. + :param title: title from which the slug is generated (with `slugify()`) :return: the unique slug """ @@ -184,8 +185,8 @@ def get_unique_slug(self, title): return new_slug def add_slug_to_pool(self, slug): - """ - Add a slug to the slug pool to be taken into account when generate a unique slug + """Add a slug to the slug pool to be taken into account when generate a unique slug + :param slug: the slug to add """ try: @@ -224,9 +225,10 @@ def can_add_extract(self): return False def add_container(self, container, generate_slug=False): - """ - Add a child Container, but only if no extract were previously added and tree depth is < 2. - Note: this function will also raise an Exception if article, because it cannot contain child container + """Add a child Container, but only if no extract were previously added and tree depth is < 2. + .. attention:: + this function will also raise an Exception if article, because it cannot contain child container + :param container: the new container :param generate_slug: if `True`, ask the top container an unique slug for this object """ @@ -243,8 +245,8 @@ def add_container(self, container, generate_slug=False): raise InvalidOperationError("Cannot add another level to this container") def add_extract(self, extract, generate_slug=False): - """ - Add a child container, but only if no container were previously added + """Add a child container, but only if no container were previously added + :param extract: the new extract :param generate_slug: if `True`, ask the top container an unique slug for this object """ @@ -261,8 +263,7 @@ def add_extract(self, extract, generate_slug=False): raise InvalidOperationError("Can't add an extract if this container already contains containers.") def update_children(self): - """ - Update the path for introduction and conclusion for the container and all its children. If the children is an + """Update the path for introduction and conclusion for the container and all its children. If the children is an extract, update the path to the text instead. This function is useful when `self.slug` has changed. Note : this function does not account for a different arrangement of the files. @@ -278,9 +279,9 @@ def update_children(self): # TODO : does this function should also rewrite `slug_pool` ? def get_path(self, relative=False): - """ - Get the physical path to the draft version of the container. + """Get the physical path to the draft version of the container. Note: this function rely on the fact that the top container is VersionedContainer. + :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ @@ -290,9 +291,9 @@ def get_path(self, relative=False): return os.path.join(base, self.slug) def get_prod_path(self): - """ - Get the physical path to the public version of the container. + """Get the physical path to the public version of the container. Note: this function rely on the fact that the top container is VersionedContainer. + :return: physical path """ base = '' @@ -353,8 +354,8 @@ def get_conclusion(self): self.conclusion) def repo_update(self, title, introduction, conclusion, commit_message='', do_commit=True): - """ - Update the container information and commit them into the repository + """Update the container information and commit them into the repository + :param title: the new title :param introduction: the new introduction text :param conclusion: the new conclusion text @@ -510,9 +511,9 @@ def repo_delete(self, commit_message='', do_commit=True): return self.top_container().commit_changes(commit_message) def move_child_up(self, child_slug): - """ - Change the child's ordering by moving up the child whose slug equals child_slug. + """Change the child's ordering by moving up the child whose slug equals child_slug. This method **does not** automaticaly update the repo + :param child_slug: the child's slug :raise ValueError: if the slug does not refer to an existing child :raise IndexError: if the extract is already the first child @@ -527,9 +528,9 @@ def move_child_up(self, child_slug): self.children[child_pos - 1].position_in_parent = child_pos def move_child_down(self, child_slug): - """ - Change the child's ordering by moving down the child whose slug equals child_slug. + """Change the child's ordering by moving down the child whose slug equals child_slug. This method **does not** automaticaly update the repo + :param child_slug: the child's slug :raise ValueError: if the slug does not refer to an existing child :raise IndexError: if the extract is already the last child @@ -544,9 +545,9 @@ def move_child_down(self, child_slug): self.children[child_pos + 1].position_in_parent = child_pos + 1 def move_child_after(self, child_slug, refer_slug): - """ - Change the child's ordering by moving the child to be below the reference child. + """Change the child's ordering by moving the child to be below the reference child. This method **does not** automaticaly update the repo + :param child_slug: the child's slug :param refer_slug: the referent child's slug. :raise ValueError: if one slug does not refer to an existing child @@ -568,9 +569,9 @@ def move_child_after(self, child_slug, refer_slug): self.move_child_up(child_slug) def move_child_before(self, child_slug, refer_slug): - """ - Change the child's ordering by moving the child to be just above the reference child. . + """Change the child's ordering by moving the child to be just above the reference child. . This method **does not** automaticaly update the repo + :param child_slug: the child's slug :param refer_slug: the referent child's slug. :raise ValueError: if one slug does not refer to an existing child @@ -592,8 +593,8 @@ def move_child_before(self, child_slug, refer_slug): self.move_child_up(child_slug) def traverse(self, only_container=True): - """ - Traverse the container + """Traverse the container + :param only_container: if we only want container's paths, not extract :return: a generator that traverse all the container recursively (depth traversal) """ @@ -906,8 +907,8 @@ def get_absolute_url_beta(self): return self.get_absolute_url() def get_path(self, relative=False, use_current_slug=False): - """ - Get the physical path to the draft version of the Content. + """Get the physical path to the draft version of the Content. + :param relative: if `True`, the path will be relative, absolute otherwise. :param use_current_slug: if `True`, use `self.slug` instead of `self.slug_last_draft` :return: physical path @@ -923,6 +924,7 @@ def get_path(self, relative=False, use_current_slug=False): def get_prod_path(self): """ Get the physical path to the public version of the content + :return: physical path """ return os.path.join(settings.ZDS_APP['content']['repo_public_path'], self.slug) @@ -952,8 +954,7 @@ def get_json(self): return data def dump_json(self, path=None): - """ - Write the JSON into file + """Write the JSON into file :param path: path to the file. If `None`, write in "manifest.json" """ if path is None: @@ -966,9 +967,9 @@ def dump_json(self, path=None): json_data.close() def repo_update_top_container(self, title, slug, introduction, conclusion, commit_message=''): - """ - Update the top container information and commit them into the repository. + """Update the top container information and commit them into the repository. Note that this is slightly different from the `repo_update()` function, because slug is generated using DB + :param title: the new title :param slug: the new slug, according to title (choose using DB!!) :param introduction: the new introduction text @@ -989,7 +990,10 @@ def repo_update_top_container(self, title, slug, introduction, conclusion, commi return self.repo_update(title, introduction, conclusion, commit_message) def commit_changes(self, commit_message): - """Commit change made to the repository""" + """Commit change made to the repository + + :param commit_message: The message that will appear in content history + """ cm = self.repository.index.commit(commit_message, **get_commit_author()) self.sha_draft = cm.hexsha @@ -998,11 +1002,11 @@ def commit_changes(self, commit_message): return cm.hexsha def change_child_directory(self, child, adoptive_parent): - """ - Move an element of this content to a new location. + """Move an element of this content to a new location. This method changes the repository index and stage every change but does **not** commit. + :param child: the child we want to move, can be either an Extract or a Container object - :param adoptive_parent: the container where the child *will be* moved, must be a Container object + :param adptive_parent: the container where the child *will be* moved, must be a Container object """ old_path = child.get_path(False) # absolute path because we want to access the address @@ -1020,8 +1024,8 @@ def change_child_directory(self, child, adoptive_parent): def get_content_from_json(json, sha, slug_last_draft): - """ - Transform the JSON formated data into `VersionedContent` + """Transform the JSON formated data into `VersionedContent` + :param json: JSON data from a `manifest.json` file :param sha: version :return: a `VersionedContent` with all the information retrieved from JSON @@ -1061,8 +1065,8 @@ def get_content_from_json(json, sha, slug_last_draft): def fill_containers_from_json(json_sub, parent): - """ - Function which call itself to fill container + """Function which call itself to fill container + :param json_sub: dictionary from "manifest.json" :param parent: the container to fill """ @@ -1097,9 +1101,9 @@ def fill_containers_from_json(json_sub, parent): def init_new_repo(db_object, introduction_text, conclusion_text, commit_message='', do_commit=True): - """ - Create a new repository in `settings.ZDS_APP['contents']['private_repo']` to store the files for a new content. + """Create a new repository in `settings.ZDS_APP['contents']['private_repo']` to store the files for a new content. Note that `db_object.sha_draft` will be set to the good value + :param db_object: `PublishableContent` (WARNING: should have a valid `slug`, so previously saved) :param introduction_text: introduction from form :param conclusion_text: conclusion from form @@ -1163,8 +1167,7 @@ def get_commit_author(): class PublishableContent(models.Model): - """ - A tutorial whatever its size or an article. + """A tutorial whatever its size or an article. A PublishableContent retains metadata about a content in database, such as @@ -1246,8 +1249,8 @@ def save(self, *args, **kwargs): super(PublishableContent, self).save(*args, **kwargs) def get_repo_path(self, relative=False): - """ - Get the path to the tutorial repository + """Get the path to the tutorial repository + :param relative: if `True`, the path will be relative, absolute otherwise. :return: physical path """ @@ -1258,52 +1261,52 @@ def get_repo_path(self, relative=False): return os.path.join(settings.ZDS_APP['content']['repo_private_path'], self.slug) def in_beta(self): - """ - A tutorial is not in beta if sha_beta is `None` or empty + """A tutorial is not in beta if sha_beta is `None` or empty + :return: `True` if the tutorial is in beta, `False` otherwise """ return (self.sha_beta is not None) and (self.sha_beta.strip() != '') def in_validation(self): - """ - A tutorial is not in validation if sha_validation is `None` or empty + """A tutorial is not in validation if sha_validation is `None` or empty + :return: `True` if the tutorial is in validation, `False` otherwise """ return (self.sha_validation is not None) and (self.sha_validation.strip() != '') def in_drafting(self): - """ - A tutorial is not in draft if sha_draft is `None` or empty + """A tutorial is not in draft if sha_draft is `None` or empty + :return: `True` if the tutorial is in draft, `False` otherwise """ return (self.sha_draft is not None) and (self.sha_draft.strip() != '') def in_public(self): - """ - A tutorial is not in on line if sha_public is `None` or empty + """A tutorial is not in on line if sha_public is `None` or empty + :return: `True` if the tutorial is on line, `False` otherwise """ return (self.sha_public is not None) and (self.sha_public.strip() != '') def is_beta(self, sha): - """ - Is this version of the content the beta version ? + """Is this version of the content the beta version ? + :param sha: version :return: `True` if the tutorial is in beta, `False` otherwise """ return self.in_beta() and sha == self.sha_beta def is_validation(self, sha): - """ - Is this version of the content the validation version ? + """Is this version of the content the validation version ? + :param sha: version :return: `True` if the tutorial is in validation, `False` otherwise """ return self.in_validation() and sha == self.sha_validation def is_public(self, sha): - """ - Is this version of the content the published version ? + """Is this version of the content the published version ? + :param sha: version :return: `True` if the tutorial is in public, `False` otherwise """ @@ -1322,9 +1325,9 @@ def is_tutorial(self): return self.type == 'TUTORIAL' def load_version_or_404(self, sha=None, public=False): - """ - Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if + """Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if `public` is `True`). + :param sha: version :param public: if `True`, use `sha_public` instead of `sha_draft` if `sha` is `None` :raise Http404: if sha is not None and related version could not be found @@ -1336,10 +1339,11 @@ def load_version_or_404(self, sha=None, public=False): raise Http404 def load_version(self, sha=None, public=False): - """ - Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if + """Using git, load a specific version of the content. if `sha` is `None`, the draft/public version is used (if `public` is `True`). - Note: for practical reason, the returned object is filled with information form DB. + .. attention:: + for practical reason, the returned object is filled with information from DB. + :param sha: version :param public: if `True`, use `sha_public` instead of `sha_draft` if `sha` is `None` :raise BadObject: if sha is not None and related version could not be found @@ -1366,8 +1370,8 @@ def load_version(self, sha=None, public=False): return versioned def insert_data_in_versioned(self, versioned): - """ - Insert some additional data from database in a VersionedContent + """Insert some additional data from database in a VersionedContent + :param versioned: the VersionedContent to fill """ @@ -1446,8 +1450,8 @@ def first_unread_note(self): return self.first_note() def antispam(self, user=None): - """ - Check if the user is allowed to post in an tutorial according to the SPAM_LIMIT_SECONDS value. + """Check if the user is allowed to post in an tutorial according to the SPAM_LIMIT_SECONDS value. + :param user: the user to check antispam. If `None`, current user is used. :return: `True` if the user is not able to note (the elapsed time is not enough), `False` otherwise. """ @@ -1467,8 +1471,8 @@ def antispam(self, user=None): return False def change_type(self, new_type): - """ - Allow someone to change the content type, basically from tutorial to article + """Allow someone to change the content type, basically from tutorial to article + :param new_type: the new type, either `"ARTICLE"` or `"TUTORIAL"` """ if new_type not in TYPE_CHOICES: @@ -1476,29 +1480,29 @@ def change_type(self, new_type): self.type = new_type def have_markdown(self): - """ - Check if the markdown zip archive is available + """Check if the markdown zip archive is available + :return: `True` if available, `False` otherwise """ return os.path.isfile(os.path.join(self.get_repo_path(), self.slug + ".md")) def have_html(self): - """ - Check if the html version of the content is available + """Check if the html version of the content is available + :return: `True` if available, `False` otherwise """ return os.path.isfile(os.path.join(self.get_repo_path(), self.slug + ".html")) def have_pdf(self): - """ - Check if the pdf version of the content is available + """Check if the pdf version of the content is available + :return: `True` if available, `False` otherwise """ return os.path.isfile(os.path.join(self.get_repo_path(), self.slug + ".pdf")) def have_epub(self): - """ - Check if the standard epub version of the content is available + """Check if the standard epub version of the content is available + :return: `True` if available, `False` otherwise """ return os.path.isfile(os.path.join(self.get_repo_path(), self.slug + ".epub")) @@ -1589,29 +1593,29 @@ def __unicode__(self): return self.content.title def is_pending(self): - """ - Check if the validation is pending + """Check if the validation is pending + :return: `True` if status is pending, `False` otherwise """ return self.status == 'PENDING' def is_pending_valid(self): - """ - Check if the validation is pending (but there is a validator) + """Check if the validation is pending (but there is a validator) + :return: `True` if status is pending, `False` otherwise """ return self.status == 'PENDING_V' def is_accept(self): - """ - Check if the content is accepted + """Check if the content is accepted + :return: `True` if status is accepted, `False` otherwise """ return self.status == 'ACCEPT' def is_reject(self): - """ - Check if the content is rejected + """Check if the content is rejected + :return: `True` if status is rejected, `False` otherwise """ return self.status == 'REJECT' diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 8796336a23..70fedd4c88 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -113,8 +113,8 @@ def get_last_articles(): def never_read(content, user=None): - """ - Check if a content note feed has been read by an user since its last post was added. + """Check if a content note feed has been read by an user since its last post was added. + :param content: the content to check :return: `True` if it is the case, `False` otherwise """ @@ -127,8 +127,8 @@ def never_read(content, user=None): def mark_read(content): - """ - Mark the last tutorial note as read for the user. + """Mark the last tutorial note as read for the user. + :param content: the content to mark """ if content.last_note is not None: @@ -143,15 +143,14 @@ def mark_read(content): def try_adopt_new_child(adoptive_parent, child): - """ - Try the adoptive parent to take the responsability of the child + """Try the adoptive parent to take the responsability of the child + :param parent_full_path: :param child_slug: :param root: :raise Http404: if adoptive_parent_full_path is not found on root hierarchy :raise TypeError: if the adoptive parent is not allowed to adopt the child due to its type :raise TooDeepContainerError: if the child is a container that is too deep to be adopted by the proposed parent - :return: """ if isinstance(child, Extract): @@ -166,8 +165,8 @@ def try_adopt_new_child(adoptive_parent, child): def get_target_tagged_tree(movable_child, root): - """ - Gets the tagged tree with deplacement availability + """Gets the tagged tree with deplacement availability + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of movable_child to be moved near another child @@ -180,12 +179,12 @@ def get_target_tagged_tree(movable_child, root): def get_target_tagged_tree_for_extract(movable_child, root): - """ - Gets the tagged tree with displacement availability when movable_child is an extract + """Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the extract we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of movable_child to be moved near another child - tuples are (relative_path, title, level, can_be_a_target) + tuples are ``(relative_path, title, level, can_be_a_target)`` """ target_tagged_tree = [] for child in root.traverse(False): @@ -199,8 +198,8 @@ def get_target_tagged_tree_for_extract(movable_child, root): def get_target_tagged_tree_for_container(movable_child, root): - """ - Gets the tagged tree with displacement availability when movable_child is an extract + """Gets the tagged tree with displacement availability when movable_child is an extract + :param movable_child: the container we want to move :param root: the VersionnedContent we use as root :return: an array of tuples that represent the capacity of movable_child to be moved near another child From 1125890993c9bdbbd0862cc0730c83d013769da9 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 4 Apr 2015 18:32:50 +0200 Subject: [PATCH 183/887] Corrections orthographiques --- doc/source/back-end/tutorialv2.rst | 170 ++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 49 deletions(-) diff --git a/doc/source/back-end/tutorialv2.rst b/doc/source/back-end/tutorialv2.rst index ba24459b13..8aa4431c34 100644 --- a/doc/source/back-end/tutorialv2.rst +++ b/doc/source/back-end/tutorialv2.rst @@ -5,39 +5,68 @@ Les tutoriels et articles v2.0 (ZEP 12) Vocabulaire et définitions ========================== -- **Contenu** (*content*): désigne, de manière générale, quelque chose qui peut être produit et édité sur ZdS, c'est à dire un article ou un tutoriel. Tout contenu sur ZdS possède un dossier qui lui est propre et dont le contenu est expliqué plus loin. Ce dossier contient des informations sur le contenu lui-même (*metadata*, par exemple une description, une licence, ...) et des textes, organisés dans une arborescence bien précise. -- **Article** : contenu, généralement cours, visant à faire découvrir un sujet plus qu'à l'expliquer (ex: découverte d'une nouvelle technique, "c'est toute une histoire", ...) ou a donner un état des lieux sur un sujet donné de manière concises (ex: statistiques sur le ZdS, nouvelle version d'un programme, actualité, ...) +- **Contenu** (*content*): désigne, de manière générale, quelque chose qui peut être produit et édité sur ZdS, c'est-à-dire un article ou un tutoriel. Tout contenu sur ZdS possède un dossier qui lui est propre et dont le contenu est expliqué plus loin. Ce dossier contient des informations sur le contenu lui-même (*metadata*, par exemple une description, une licence, ...) et des textes, organisés dans une arborescence bien précise. +- **Article** : contenu, généralement court, visant à faire découvrir un sujet plus qu'à l'expliquer (ex: découverte d'une nouvelle technique, "c'est toute une histoire", ...) ou à donner un état des lieux sur un sujet donné de manière concise (ex: statistiques sur ZdS, nouvelle version d'un programme, actualité...). - **Tutoriel** : contenu, en général plus long, qui a pour vocation d'apprendre quelque chose ou d'informer de manière plus complète sur des points précis. -- **GIT**: système de versionnage employé (entre autre) par ZdS. Il permet de faire cohexister différentes versions d'un contenu de manière simple et transparente pour l'auteur. -- **Version** : toute modification apportée sur un contenu (ou une de ces partie) génère une nouvelle version de celui-ci, qui est indentifiée par un *hash*, c'est à dire une chaine de caractère de 20 caractère de long (aussi appellée *sha* en référence à l'algorithme qui sert à les générer) et qui identifie de manière unique cette version parmi toutes celles que le contenu contient. ZdS peut également identifier certaines versions de manière spécifique : ainsi, on peut distinguer la version brouillon (*draft*), la version bêta (*beta*), la version en validation (*validation*) et la version publié (*public*). ZdS retient tout simplement les *hash* associés à ces différentes versions du tutoriel pour les identifier comme telles. -- Fichier **manifest.json** : fichier à la racine de tout contenu et qui décrit celui-ci de deux manières : par des informations factuelles sur le contenu en lui-même (*metadata*) et par son arborescence. Le contenu de ce fichier est détaillé plus loin. On retiendra qu'à chaque version correspond en général un fichier manifest.json donné dont le contenu peut fortement changer d'une version à l'autre. +- **GIT**: système de gestion de versions employé (entre autres) par ZdS. Il permet de faire coexister différentes versions d'un contenu de manière simple et transparente pour l'auteur. +- **Version** : toute modification apportée sur un contenu (ou une de ses parties) génère une nouvelle version de celui-ci, qui est indentifiée par un *hash*, c'est-à-dire une chaîne de caractères de 20 caractères de long (aussi appellée *sha* en référence à l'algorithme qui sert à les générer) et qui identifie de manière unique cette version parmi toutes celles que le contenu contient. ZdS peut également identifier certaines versions de manière spécifique : ainsi, on peut distinguer la version brouillon (*draft*), la version bêta (*beta*), la version en validation (*validation*) et la version publié (*public*). ZdS retient tout simplement les *hash* associés à ces différentes versions du tutoriel pour les identifier comme telles. +- Fichier **manifest.json** : fichier à la racine de tout contenu et qui décrit celui-ci de deux manières : par des informations factuelles sur le contenu en lui-même (*metadata*) et par son arborescence. Le contenu de ce fichier est détaillé plus loin. On retiendra qu'à chaque version correspond en général un fichier manifest.json, dont le contenu peut fortement changer d'une version à l'autre. - **Conteneur** (*container*) : sous-structure d'un contenu. Voir plus loin. - **Extrait** (*extract*) : base atomique (plus petite unité) d'un contenu. Voir plus loin. De la structure générale d'un contenu ===================================== -Un **contenu** est un conteneur avec des métadonnées (*metadata*, décrites ci-dessous). On peut également le désigner sous le nom de **conteneur principal** (il n'as pas de conteneur parent). Pour différencier articles et tutoriels, une de ces métadonnées est le type (*type*). - -Un **conteneur** (de manière générale) peut posséder un conteneur parent et des enfants (*children*). Ces enfants peuvent être eux-même des conteneurs ou des extraits. Il a donc pour rôle de regrouper différents éléments. Outre des enfants, un conteneur possède un titre (*title*), une introduction (*introduction*), une conclusion (*conclusion*). On notera qu'un conteneur ne peut pas posséder pour enfant des conteneur ET des extraits. - -Un **extrait** est une unité de texte. Il possède un titre (*title*) et un texte (*text*). - -Tout les textes sont formatés en *markdown* (dans la version définie par le ZdS, avec les ajouts). - -Conteneur et extraits sont des **objets** (*object*). Dès lors, ils possèdent tout deux un *slug* (litérallement, "limace") : il s'agit d'une chaine de caractère généré à partir du titre de l'objet et qui, tout en restant lisible par un être humain, le simplifie considérablement : un *slug* est uniquement composé de caractères alphanumériques minuscules et non-accentués (``[a-z0-9]*``) ainsi que des caractères ``-`` (tiret) et ``_`` (*underscore*). Ce *slug* a deux utilités : il est employé dans l'URL permetant d'accéder à l'objet et dans le nom de fichier/dossier employer pour le stocker. Dès lors, cette spécification **impose** que ce *slug* soit unique au sein du conteneur parent, et que le *slug* du contenu soit unique au sein de tout les contenu de ZdS (ce qui ne signifie pas que tout les slugs doivent être uniques, tant que ces deux règles sont respectées). +Un **contenu** est un conteneur avec des métadonnées (*metadata*, décrites +ci-dessous). On peut également le désigner sous le nom de +**conteneur principal** (il n'a pas de conteneur parent). Pour différencier +articles et tutoriels, une de ces métadonnées est le type (*type*). + +Un **conteneur** (de manière générale) peut posséder un conteneur parent et des +enfants (*children*). Ces enfants peuvent être eux-même des conteneurs ou des +extraits. Il a donc pour rôle de regrouper différents éléments. Outre des +enfants, un conteneur possède un titre (*title*), une introduction +(*introduction*) et une conclusion (*conclusion*). On notera qu'un conteneur ne +peut pas posséder pour enfants à la fois des conteneurs ET des extraits. + +Un **extrait** est une unité de texte. Il possède un titre (*title*) et un +texte (*text*). + +Tous les textes sont formatés en *markdown* (dans la version définie par ZdS, +avec les ajouts). + +Conteneurs et extraits sont des **objets** (*object*). Dès lors, ils possèdent +tous deux un *slug* (litéralement, "limace") : il s'agit d'une chaîne de +caractères générée à partir du titre de l'objet et qui, tout en restant lisible +par un être humain, le simplifie considérablement : un *slug* est uniquement +composé de caractères alphanumériques minuscules et non-accentués +(``[a-z0-9]*``) ainsi que des caractères ``-`` (tiret) et ``_`` (*underscore*). +Ce *slug* a deux utilités : il est employé dans l'URL permetant d'accéder à +l'objet et dans le nom de fichier/dossier employé pour le stocker. Dès lors, +cette spécification **impose** que ce *slug* soit unique au sein du conteneur +parent, et que le *slug* du contenu soit unique au sein de tous les contenus de +ZdS (ce qui ne signifie pas que tout les slugs doivent être uniques, tant que +ces deux règles sont respectées). .. note:: - À noter que l'*underscore* est conservé par compatibilité avec l'ancien système, les nouveaux *slugs* générés par le système d'édition de ZdS n'en contiendront pas. + À noter que l'*underscore* est conservé par compatibilité avec l'ancien + système, les nouveaux *slugs* générés par le système d'édition de ZdS + n'en contiendront pas. .. attention:: - Lors du déplacement d'un conteneur ou d'un extrait, les slugs sont modifiés de manière à ce qu'il n'y aie pas de colision. + Lors du déplacement d'un conteneur ou d'un extrait, les slugs sont modifiés + de manière à ce qu'il n'y ait pas de collision. - À noter que le slug doit être différent de celui donné au nom des introductions et des conclusions éventuelles. L'implémentation du ZdS considère que ceux-ci sont ``introduction`` et ``conclusion``, mais ce n'est pas obligatoire. + À noter que le slug doit être différent de celui donné aux noms des + introductions et conclusions éventuelles. L'implémentation de ZdS considère + que ceux-ci sont ``introduction`` et ``conclusion``, mais ce n'est pas + obligatoire. -En fonction de sa position dans l'arborescence du contenu, un conteneur peut aussi bien représenter le tutoriel/article lui-même (s'il est conteneur principal), une partie ou un chapitre. Ainsi, dans l'exemple suivant : +En fonction de sa position dans l'arborescence du contenu, un conteneur peut +aussi bien représenter le tutoriel/article lui-même (s'il est conteneur +principal), une partie ou un chapitre. Ainsi, dans l'exemple suivant : .. sourcecode:: text @@ -48,7 +77,8 @@ En fonction de sa position dans l'arborescence du contenu, un conteneur peut aus +-- Extrait 1 -le ``Conteneur 1`` sera rendu par ZdS comme étant un chapitre d'un (moyen-) tutoriel, et dans l'exemple suivant : +le ``Conteneur 1`` sera rendu par ZdS comme étant un chapitre d'un (moyen-) +tutoriel, alors que dans l'exemple suivant : .. sourcecode:: text @@ -65,17 +95,25 @@ le ``Conteneur 1`` sera rendu par ZdS comme étant un chapitre d'un (moyen-) tut +-- Extrait 1 -le ``Conteneur 1`` sera rendu par ZdS comme étant une partie d'un (big-) tutoriel, et ``Conteneur 2`` et ``Conteneur 3`` comme étant les chapitres de cette partie. +le ``Conteneur 1`` sera rendu par ZdS comme étant une partie d'un (big-) +tutoriel, et ``Conteneur 2`` et ``Conteneur 3`` comme étant les chapitres de +cette partie. -Les deux exemples donnés plus haut reprennent l'arboresence typique d'un contenu : Conteneur principal-[conteneur]~*n*~-extraits (ou *n* peut être nul). En fonction de la profondeur de l'arborescence (plus grande distance entre un conteneur enfant et le conteneur principal), le contenu sera nommé de manière différente. S'il s'agit d'un contenu de type tutoriel, on distinguera : +Les deux exemples donnés plus haut reprennent l'arboresence typique d'un +contenu : Conteneur principal-[conteneur]~*n*~-extraits (où *n* peut être nul). +En fonction de la profondeur de l'arborescence (plus grande distance entre un +conteneur enfant et le conteneur principal), le contenu sera nommé de manière +différente. S'il s'agit d'un contenu de type *tutoriel*, on distinguera : -+ **Mini-tutoriel** : le contenu n'as que des extraits pour enfant (au minimum 1) ; ++ **Mini-tutoriel** : le contenu n'a que des extraits pour enfants (au minimum un) ; + **Moyen-tutoriel** : le contenu a au moins un conteneur pour enfant. Ces conteneurs sont pour ZdS des chapitres ; -+ **Big-tutoriel** : le contenu a un ou plusieurs conteneur-enfants (considéré par ZdS comme des parties), dont au moins 1 possède un conteneur enfant (considérés par Zds comme des chapitres). ++ **Big-tutoriel** : le contenu a un ou plusieurs conteneur-enfants (considérés par ZdS comme des parties), dont au moins un possède un conteneur enfant (considérés par ZdS comme des chapitres). -Il n'est pas possible de créer une arborescence à plus de 2 niveaux (parce que ça n'as pas de sens). +Il n'est pas possible de créer une arborescence à plus de 2 niveaux (parce que +ça n'a pas de sens). -On considère qu'un article est l'équivalent d'un mini-tutoriel, mais dont le but est différent (voir ci-dessus). +On considère qu'un article est l'équivalent d'un mini-tutoriel, mais dont le +but est différent (voir ci-dessus). Aspects techniques et fonctionnels ================================== @@ -83,17 +121,27 @@ Aspects techniques et fonctionnels Métadonnées d'un contenu ------------------------ -On distingue actuelement deux types de métadonnées (*metadata*) : celles qui sont versionnées (et donc reprises dans le manifest.json) et celle qui ne le sont pas. La liste exhaustive de ces dernière (à l'heure actuelle) est la suivante : +On distingue actuellement deux types de métadonnées (*metadata*) : celles qui +sont versionnées (et donc reprises dans le manifest.json) et celles qui ne le +sont pas. La liste exhaustive de ces dernières (à l'heure actuelle) est la +suivante : -+ Les *hash* des différentes version du tutoriels (``sha_draft``, ``sha_beta``, ``sha_public`` et ``sha_validation``) ; ++ Les *hash* des différentes versions du tutoriel (``sha_draft``, ``sha_beta``, ``sha_public`` et ``sha_validation``) ; + Les auteurs du contenu ; -+ Les catégories auquel appartient le contenu ; ++ Les catégories auquelles appartient le contenu ; + La miniature ; -+ La source du contenu si elle n'as pas été rédigée sur ZdS mais importée avec une licence compatible ; ++ L'origine du contenu, s'il n'a pas été rédigé sur ZdS mais importé avec une licence compatible ; + La présence ou pas de JSFiddle dans le contenu ; + Différentes informations temporelles : date de création (``creation_date``), de publication (``pubdate``) et de dernière modification (``update_date``). -Ces différentes informations sont stockées dans la base de donnée, au travers du modèle ``PublishableContent``. Pour des raisons de facilité, certaines des métadonnées versionnées sont également stockée en base de donnée : le titre, le type de contenu, la licence et la description. En ce qui concerne la version de celle-ci, c'est TOUJOURS celle correspondant **à la version brouillon** qui sont stockées, il ne faut donc **en aucun cas** les employer pour résoudre une URL ou à travers une template correspondant à la version publiée. +Ces différentes informations sont stockées dans la base de données, au travers +du modèle ``PublishableContent``. Pour des raisons de facilité, certaines des +métadonnées versionnées sont également stockées en base de données : le titre, +le type de contenu, la licence et la description. En ce qui concerne la version +de cette dernière, c'est TOUJOURS celle correspondant +**à la version brouillon** qui est stockée. Il ne faut donc **en aucun cas** +les employer pour résoudre une URL ou à travers une template correspondant à la +version publiée. Les métadonnées versionnées sont stockées dans le fichier manifest.json @@ -101,20 +149,40 @@ Les métadonnées versionnées sont stockées dans le fichier manifest.json Le stockage en pratique ----------------------- -Comme énoncé plus haut, chaque contenu possède un dossier qui lui est propre (dont le nom est le slug du contenu), stocké dans l'endroit défini par la variable ``ZDS_APP['content']['repo_path']``. Dans ce dossier ce trouve le fichier manifest.json. +Comme énoncé plus haut, chaque contenu possède un dossier qui lui est propre +(dont le nom est le slug du contenu), stocké dans l'endroit défini par la +variable ``ZDS_APP['content']['repo_path']``. Dans ce dossier se trouve le +fichier manifest.json. -Pour chaque conteneur, un dossier est créé, qui contient les éventuels fichiers correspondants aux introduction, conclusion et différents extraits, ainsi que des dossiers pour les éventuels conteneurs enfants. Il s'agit de la forme d'un contenu tel que généré par ZdS en utilisant l'éditeur intégré. +Pour chaque conteneur, un dossier est créé, contenant les éventuels fichiers +correspondant aux introduction, conclusion et différents extraits, ainsi que +des dossiers pour les éventuels conteneurs enfants. Il s'agit de la forme d'un +contenu tel que généré par ZdS en utilisant l'éditeur en ligne. -Il est demandé de se conformer un maximum à cette structure pour éviter les mauvaises surprises en cas d'édition externe (voir ci-dessous). +Il est demandé de se conformer au maximum à cette structure pour éviter les +mauvaises surprises en cas d'édition externe (voir ci-dessous). Éventuelle édition externe -------------------------- -Actuellement, l'importation est possible sous forme principalement d'un POC à l'aide d'un fichier ZIP. Ce mécanisme doit être conservé mais peut être étendu : ne plus être lié à la base de donnée pour autre chose que les métadonnées du contenu externe permet une simplification considérable de l'édition hors-ligne (entre autre, la possibilité d'ajouter ou de supprimer comme bon semble à l'auteur). - -Au maximum, ce système tentera d'être compréhensif envers une arborescence qui serait différente de celle énoncée ci-dessous. Par contre **l'importation réorganisera les fichiers importés de la manière décrite ci-dessus**, afin de parer aux mauvaises surprises. - -Tout contenu qui ne correspond pas aux règles précisées ci-dessus ne sera pas ré-importable. Ne sera pas ré-importable non plus un contenu dont les fichiers indiqués dans le manifest.json n'existent pas ou sont incorrects. Seront supprimés les fichiers qui seraient inutiles (images, qui actuelement doivent être importées séparément dans une gallerie, autres fichiers supplémentaires, pour des raisons élémentaire de sécurité). +Actuellement, l'importation est possible principalement sous forme d'un POC à +l'aide d'un fichier ZIP. Ce mécanisme doit être conservé mais peut être étendu +: ne plus être lié à la base de données pour autre chose que les métadonnées du +contenu externe permet une simplification considérable de l'édition hors-ligne +(entre autres, la possibilité d'ajouter ou de supprimer comme bon le semble à +l'auteur). + +Au maximum, ce système tentera d'être compréhensif envers une arborescence qui +serait différente de celle énoncée ci-dessus. Par contre +**l'importation réorganisera les fichiers importés de la manière décrite ci-dessus**, +afin de parer aux mauvaises surprises. + +Tout contenu qui ne correspond pas aux règles précisées ci-dessus ne sera pas +ré-importable. Ne sera pas ré-importable non plus tout contenu dont les +fichiers indiqués dans le manifest.json n'existent pas ou sont incorrects. +Seront supprimés les fichiers qui seraient inutiles (images, qui actuellement +doivent être importées séparément dans une galerie, autres fichiers +supplémentaires) pour des raisons élémentaires de sécurité. Publication d'un contenu ("mise en production") =============================================== @@ -122,27 +190,31 @@ Publication d'un contenu ("mise en production") Processus de publication ------------------------ -Apès avoir passé les étapes de validations (`détaillées ailleurs <./tutorial.html#cycle-de-vie-des-tutoriels>`__), le contenu est près à être publié. Cette action +Apès avoir passé les étapes de validation +(`détaillées ailleurs <./tutorial.html#cycle-de-vie-des-tutoriels>`__), le +contenu est près à être publié. Cette action est effectuée par un membre du staff. Le but de la publication est double : permettre aux visiteurs de lire le contenu, mais aussi -d’effectuer certains traitements (détaillés par après) afin que celui-ci +d’effectuer certains traitements (détaillés ci-après) afin que celui-ci soit sous une forme qui soit plus rapidement affichable par ZdS. C’est -pourquoi ces contenus ne sont pas stockés au même endroit (voir ``ZDS_AP['content']['repo_public_path']``). +pourquoi ces contenus ne sont pas stockés au même endroit +(voir ``ZDS_AP['content']['repo_public_path']``). -La mise en production se passe comme suis : +La mise en production se passe comme suit : -1. S'il s'agit d'un nouveau contenu (jamais publié), un dossier dont le nom est le slug du contenu est créé. Dans le cas contraire, le contenu de ce dossier est entièrement effacé. -2. Le manifest.json correspondant à la version de validation (``sha_publication``) est copié dans ce dossier. Il servira principalement à valider les URLs, créer le sommaire et gérer le comportement des boutons "précédents" et "suivants" dans les conteneur dont les enfants sont des extraits (voir ci-dessous). -3. L'arborescence des dossiers est conservée pour les conteneur dont les enfants sont des conteneur, et leur éventuelles introduction et conclusion sont parsé en HTML. À l'inverse, pour les conteneurs dont les enfants sont des extraits, un fichier HTML unique est créé, reprenant de manière continue la forme parsée de l'éventuelle introduction, des différents extraits dans l'ordre et de l'éventuelle conclusion. -4. Le ``sha_public`` est mis à jour dans la base de donnée et l'objet ``Validation`` est changé de même. +1. S'il s'agit d'un nouveau contenu (jamais publié), un dossier dont le nom est le slug du contenu est créé. Dans le cas contraire, le contenu de ce dossier est entièrement effacé ; +2. Le manifest.json correspondant à la version de validation (``sha_publication``) est copié dans ce dossier. Il servira principalement à valider les URLs, créer le sommaire et gérer le comportement des boutons "précédent" et "suivant" dans les conteneurs dont les enfants sont des extraits (voir ci-dessous) ; +3. L'arborescence des dossiers est conservée pour les conteneurs dont les enfants sont des conteneurs, et leur éventuelles introduction et conclusion sont parsées en HTML. À l'inverse, pour les conteneurs dont les enfants sont des extraits, un fichier HTML unique est créé, reprenant de manière continue la forme parsée de l'éventuelle introduction, des différents extraits dans l'ordre et de l'éventuelle conclusion ; +4. Le ``sha_public`` est mis à jour dans la base de données et l'objet ``Validation`` est changé de même. Consultation d'un contenu publié -------------------------------- -On ne doit pas avoir à ce servir de GIT pour afficher la version publiée d'un contenu. +On ne doit pas avoir à se servir de GIT pour afficher la version publiée d'un +contenu. Dès lors, deux cas se présentent : -+ L'utilisateur consulte un conteneur dont les enfants sont eux-mêmes des conteneur (c'est à dire le conteneur principal ou une partie d'un big-tutoriel) : le manifest.json est employé pour générer le sommaire, comme c'est le cas actuelement, l'introduction et la conclusion sont également affichés. -+ L'utilisateur consulte un conteneur dont les enfants sont des extraits: le fichier HTML généré durant la mise en production est employé tel quel par la *template* correspondante, additionné de l'éventuelle possibilité de faire suivant/précédent (qui nécéssite la lecture du manifest.json) ++ L'utilisateur consulte un conteneur dont les enfants sont eux-mêmes des conteneurs (c'est-à-dire le conteneur principal ou une partie d'un big-tutoriel) : le manifest.json est employé pour générer le sommaire, comme c'est le cas actuellement. L'introduction et la conclusion sont également affichées. ++ L'utilisateur consulte un conteneur dont les enfants sont des extraits : le fichier HTML généré durant la mise en production est employé tel quel par le *template* correspondant, additionné de l'éventuelle possibilité de faire suivant/précédent (qui nécéssite la lecture du manifest.json). From e9267e9d0d39f5523b9e724d39a8d9d01799b5ed Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Tue, 3 Mar 2015 05:53:23 -0500 Subject: [PATCH 184/887] Permet de telecharger l'archive du contenu --- templates/tutorialv2/base.html | 6 ++-- zds/tutorialv2/mixins.py | 34 ++++++++++++++++++-- zds/tutorialv2/urls/urls_contents.py | 5 ++- zds/tutorialv2/views.py | 47 +++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/templates/tutorialv2/base.html b/templates/tutorialv2/base.html index 2c8ba30155..d50ac11c2f 100644 --- a/templates/tutorialv2/base.html +++ b/templates/tutorialv2/base.html @@ -53,8 +53,8 @@

    {% trans "Actions" %}

    {% endif %} {% block sidebar_blocks %}{% endblock %} - {% if tutorial %} - {% if tutorial.on_line or tutorial.in_beta and user in tutorial.authors.all or perms.tutorial.change_tutorial %} + {% if content %} + {% if content.on_line or content.in_beta and user in content.authors.all or perms.tutorial.change_tutorial %}
    diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index a2dec531bf..e5857ad6f2 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -10,14 +10,14 @@ from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory -text_content = u'Ceci est un texte bidon' +text_content = u'Ceci est un texte bidon, **avec markown**' class PublishableContentFactory(factory.DjangoModelFactory): FACTORY_FOR = PublishableContent - title = factory.Sequence(lambda n: 'Mon Tutoriel No{0}'.format(n)) - description = factory.Sequence(lambda n: 'Description du Tutoriel No{0}'.format(n)) + title = factory.Sequence(lambda n: 'Mon contenu No{0}'.format(n)) + description = factory.Sequence(lambda n: 'Description du contenu No{0}'.format(n)) type = 'TUTORIAL' creation_date = datetime.now() diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index c76e1f0929..1d76567397 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -476,6 +476,19 @@ def __init__(self, *args, **kwargs): type='submit') ) + def clean(self): + cleaned_data = super(AskValidationForm, self).clean() + + text = cleaned_data.get('text') + + if text is None or text.strip() == '': + self._errors['text'] = self.error_class( + [_(u'Vous devez écrire une réponse !')]) + if 'text' in cleaned_data: + del cleaned_data['text'] + + return cleaned_data + class AcceptValidationForm(forms.Form): @@ -483,7 +496,7 @@ class AcceptValidationForm(forms.Form): text = forms.CharField( label='', - required=False, + required=True, widget=forms.Textarea( attrs={ 'placeholder': _(u'Commentaire de publication.'), @@ -543,7 +556,7 @@ class RejectValidationForm(forms.Form): text = forms.CharField( label='', - required=False, + required=True, widget=forms.Textarea( attrs={ 'placeholder': _(u'Commentaire de rejet.'), @@ -552,15 +565,13 @@ class RejectValidationForm(forms.Form): ) ) - validation = None - def __init__(self, *args, **kwargs): - self.validation = kwargs.pop('instance', None) + validation = kwargs.pop('instance', None) super(RejectValidationForm, self).__init__(*args, **kwargs) self.helper = FormHelper() - self.helper.form_action = reverse('content:reject-validation', kwargs={'pk': self.validation.pk}) + self.helper.form_action = reverse('content:reject-validation', kwargs={'pk': validation.pk}) self.helper.form_method = 'post' self.helper.layout = Layout( @@ -571,6 +582,64 @@ def __init__(self, *args, **kwargs): type='submit')) ) + def clean(self): + cleaned_data = super(RejectValidationForm, self).clean() + + text = cleaned_data.get('text') + + if text is None or text.strip() == '': + self._errors['text'] = self.error_class( + [_(u'Vous devez écrire une réponse !')]) + if 'text' in cleaned_data: + del cleaned_data['text'] + + return cleaned_data + + +class RevokeValidationForm(forms.Form): + + version = forms.CharField(widget=forms.HiddenInput()) + + text = forms.CharField( + label='', + required=True, + widget=forms.Textarea( + attrs={ + 'placeholder': _(u'Pourquoi dépublier ce contenu ?'), + 'rows': '6' + } + ) + ) + + def __init__(self, *args, **kwargs): + content = kwargs.pop('instance', None) + + super(RevokeValidationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse('content:revoke-validation', kwargs={'pk': content.pk, 'slug': content.slug}) + self.helper.form_method = 'post' + + self.helper.layout = Layout( + CommonLayoutModalText(), + Field('version'), + StrictButton( + _(u'Dépublier'), + type='submit') + ) + + def clean(self): + cleaned_data = super(RevokeValidationForm, self).clean() + + text = cleaned_data.get('text') + + if text is None or text.strip() == '': + self._errors['text'] = self.error_class( + [_(u'Vous devez écrire une réponse !')]) + if 'text' in cleaned_data: + del cleaned_data['text'] + + return cleaned_data + class JsFiddleActivationForm(forms.Form): diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index a55ba81a16..53ee07ca93 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -232,6 +232,9 @@ def get_object(self, queryset=None): except ObjectDoesNotExist: raise Http404 + if obj is None: + raise Http404 + return obj def get(self, request, *args, **kwargs): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 70034c4ffe..4474e69fd4 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -765,9 +765,6 @@ def repo_update(self, title, text, commit_message='', do_commit=True): if title is None: raise PermissionDenied - if text is None: - raise PermissionDenied - repo = self.container.top_container().repository if title != self.title: @@ -788,14 +785,24 @@ def repo_update(self, title, text, commit_message='', do_commit=True): # edit text path = self.container.top_container().get_path() - self.text = self.get_path(relative=True) - f = open(os.path.join(path, self.text), "w") - f.write(text.encode('utf-8')) - f.close() + if text is not None: + self.text = self.get_path(relative=True) + f = open(os.path.join(path, self.text), "w") + f.write(text.encode('utf-8')) + f.close() + + repo.index.add([self.text]) + + elif self.text: + if os.path.exists(os.path.join(path, self.text)): + repo.index.remove([self.text]) + os.remove(os.path.join(path, self.text)) + + self.text = None # make it self.container.top_container().dump_json() - repo.index.add(['manifest.json', self.text]) + repo.index.add(['manifest.json']) if commit_message == '': commit_message = _(u'Modification de l\'extrait « {} », situé dans le conteneur « {} »')\ diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 4872ca19f9..7df9ca041b 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -274,12 +274,15 @@ def test_if_none(self): self.assertIsNotNone(new_part.introduction) self.assertIsNotNone(new_part.conclusion) - def extract_is_none(self): + def test_extract_is_none(self): """Test the case of a null extract""" - versioned = self.tuto.load_version() + article = PublishableContentFactory(type="ARTICLE") + versioned = article.load_version() + given_title = u'Peu importe, en fait, ça compte peu' some_text = u'Disparaitra aussi vite que possible' + # add a new extract with `None` for text version = versioned.repo_add_extract(given_title, None) @@ -288,7 +291,7 @@ def extract_is_none(self): self.assertIsNone(new_extract.text) # it remains when loading the manifest ! - versioned2 = self.tuto.load_version(sha=version) + versioned2 = article.load_version(sha=version) self.assertIsNotNone(versioned2) self.assertIsNone(versioned.children[-1].text) @@ -296,7 +299,7 @@ def extract_is_none(self): self.assertIsNone(new_extract.text) # it remains - versioned2 = self.tuto.load_version(sha=version) + versioned2 = article.load_version(sha=version) self.assertIsNotNone(versioned2) self.assertIsNone(versioned.children[-1].text) @@ -305,7 +308,7 @@ def extract_is_none(self): self.assertEqual(some_text, new_extract.get_text()) # now it change - versioned2 = self.tuto.load_version(sha=version) + versioned2 = article.load_version(sha=version) self.assertIsNotNone(versioned2) self.assertIsNotNone(versioned.children[-1].text) @@ -314,7 +317,7 @@ def extract_is_none(self): self.assertIsNone(new_extract.text) # it has changed - versioned2 = self.tuto.load_version(sha=version) + versioned2 = article.load_version(sha=version) self.assertIsNotNone(versioned2) self.assertIsNone(versioned.children[-1].text) diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index ab835a6d23..4cb18d30a3 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -1,6 +1,7 @@ # coding: utf-8 import os +import shutil from django.conf import settings from django.test import TestCase @@ -8,10 +9,10 @@ from zds.settings import BASE_DIR from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, LicenceFactory +from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, LicenceFactory, ExtractFactory from zds.gallery.factories import GalleryFactory -from zds.tutorialv2.utils import get_target_tagged_tree_for_container -# from zds.tutorialv2.models import Container, Extract, VersionedContent +from zds.tutorialv2.utils import get_target_tagged_tree_for_container, publish_content, unpublish_content +from zds.tutorialv2.models import PublishableContent, PublishedContent overrided_zds_app = settings.ZDS_APP overrided_zds_app['content']['repo_private_path'] = os.path.join(BASE_DIR, 'contents-private-test') @@ -58,3 +59,168 @@ def test_get_target_tagged_tree_for_container(self): self.assertFalse(paths[self.part1.get_path(True)], "can't be moved after or before himself") self.assertTrue(paths[part2.get_path(True)], "can be moved after or before part2") self.assertTrue(paths[part3.get_path(True)], "can be moved after or before part3") + + def test_publish_content(self): + """test and ensure the behavior of ``publish_content()`` and ``unpublish_content()``""" + + # 1. Article: + article = PublishableContentFactory(type='ARTICLE') + + article.authors.add(self.user_author) + article.gallery = GalleryFactory() + article.licence = self.licence + article.save() + + # populate the article + article_draft = article.load_version() + ExtractFactory(container=article_draft, db_object=article) + ExtractFactory(container=article_draft, db_object=article) + + self.assertEqual(len(article_draft.children), 2) + + # publish ! + article = PublishableContent.objects.get(pk=article.pk) + published = publish_content(article, article_draft) + + self.assertEqual(published.content, article) + self.assertEqual(published.content_pk, article.pk) + self.assertEqual(published.content_type, article.type) + self.assertEqual(published.content_public_slug, article_draft.slug) + self.assertEqual(published.sha_public, article.sha_draft) + + public = article.load_version(sha=published.sha_public, public=published) + self.assertIsNotNone(public) + self.assertTrue(public.PUBLIC) # its a PublicContent object ! + self.assertEqual(public.type, published.content_type) + self.assertEqual(public.current_version, published.sha_public) + + # test object created in database + self.assertEqual(PublishedContent.objects.filter(content=article).count(), 1) + published = PublishedContent.objects.filter(content=article).last() + + self.assertEqual(published.content_pk, article.pk) + self.assertEqual(published.content_public_slug, article_draft.slug) + self.assertEqual(published.content_type, article.type) + self.assertEqual(published.sha_public, public.current_version) + + # test creation of files: + self.assertTrue(os.path.isdir(published.get_prod_path())) + self.assertTrue(os.path.isfile(os.path.join(published.get_prod_path(), 'manifest.json'))) + self.assertTrue(os.path.isfile(public.get_prod_path())) # normally, an HTML file should exists + self.assertIsNone(public.introduction) # since all is in the HTML file, introduction does not exists anymore + self.assertIsNone(public.conclusion) + + # depublish it ! + unpublish_content(article) + self.assertEqual(PublishedContent.objects.filter(content=article).count(), 0) # published object disappear + self.assertFalse(os.path.exists(public.get_prod_path())) # article was removed + # ... For the next tests, I will assume that the unpublication works. + + # 2. Mini-tutorial → Not tested, because at this point, it's the same as an article (with a different metadata) + # 3. Medium-size tutorial + midsize_tuto = PublishableContentFactory(type='TUTORIAL') + + midsize_tuto.authors.add(self.user_author) + midsize_tuto.gallery = GalleryFactory() + midsize_tuto.licence = self.licence + midsize_tuto.save() + + # populate with 2 chapters (1 extract each) + midsize_tuto_draft = midsize_tuto.load_version() + chapter1 = ContainerFactory(parent=midsize_tuto_draft, db_objet=midsize_tuto) + ExtractFactory(container=chapter1, db_object=midsize_tuto) + chapter2 = ContainerFactory(parent=midsize_tuto_draft, db_objet=midsize_tuto) + ExtractFactory(container=chapter2, db_object=midsize_tuto) + + # publish it + midsize_tuto = PublishableContent.objects.get(pk=midsize_tuto.pk) + published = publish_content(midsize_tuto, midsize_tuto_draft) + + self.assertEqual(published.content, midsize_tuto) + self.assertEqual(published.content_pk, midsize_tuto.pk) + self.assertEqual(published.content_type, midsize_tuto.type) + self.assertEqual(published.content_public_slug, midsize_tuto_draft.slug) + self.assertEqual(published.sha_public, midsize_tuto.sha_draft) + + public = midsize_tuto.load_version(sha=published.sha_public, public=published) + self.assertIsNotNone(public) + self.assertTrue(public.PUBLIC) # its a PublicContent object + self.assertEqual(public.type, published.content_type) + self.assertEqual(public.current_version, published.sha_public) + + # test creation of files: + self.assertTrue(os.path.isdir(published.get_prod_path())) + self.assertTrue(os.path.isfile(os.path.join(published.get_prod_path(), 'manifest.json'))) + + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), public.introduction))) + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), public.conclusion))) + + self.assertEqual(len(public.children), 2) + for child in public.children: + self.assertTrue(os.path.isfile(child.get_prod_path())) # an HTML file for each chapter + self.assertIsNone(child.introduction) + self.assertIsNone(child.conclusion) + + # 4. Big tutorial: + bigtuto = PublishableContentFactory(type='TUTORIAL') + + bigtuto.authors.add(self.user_author) + bigtuto.gallery = GalleryFactory() + bigtuto.licence = self.licence + bigtuto.save() + + # populate with 2 part (1 chapter with 1 extract each) + bigtuto_draft = bigtuto.load_version() + part1 = ContainerFactory(parent=bigtuto_draft, db_objet=bigtuto) + chapter1 = ContainerFactory(parent=part1, db_objet=bigtuto) + ExtractFactory(container=chapter1, db_object=bigtuto) + part2 = ContainerFactory(parent=bigtuto_draft, db_objet=bigtuto) + chapter2 = ContainerFactory(parent=part2, db_objet=bigtuto) + ExtractFactory(container=chapter2, db_object=bigtuto) + + # publish it + bigtuto = PublishableContent.objects.get(pk=bigtuto.pk) + published = publish_content(bigtuto, bigtuto_draft) + + self.assertEqual(published.content, bigtuto) + self.assertEqual(published.content_pk, bigtuto.pk) + self.assertEqual(published.content_type, bigtuto.type) + self.assertEqual(published.content_public_slug, bigtuto_draft.slug) + self.assertEqual(published.sha_public, bigtuto.sha_draft) + + public = bigtuto.load_version(sha=published.sha_public, public=published) + self.assertIsNotNone(public) + self.assertTrue(public.PUBLIC) # its a PublicContent object + self.assertEqual(public.type, published.content_type) + self.assertEqual(public.current_version, published.sha_public) + + # test creation of files: + self.assertTrue(os.path.isdir(published.get_prod_path())) + self.assertTrue(os.path.isfile(os.path.join(published.get_prod_path(), 'manifest.json'))) + + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), public.introduction))) + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), public.conclusion))) + + self.assertEqual(len(public.children), 2) + for part in public.children: + self.assertTrue(os.path.isdir(part.get_prod_path())) # a directory for each part + # ... and an HTML file for introduction and conclusion + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), part.introduction))) + self.assertTrue(os.path.isfile(os.path.join(public.get_prod_path(), part.conclusion))) + + self.assertEqual(len(part.children), 1) + + for chapter in part.children: + # the HTML file is located in the good directory: + self.assertEqual(part.get_prod_path(), os.path.dirname(chapter.get_prod_path())) + self.assertTrue(os.path.isfile(chapter.get_prod_path())) # an HTML file for each chapter + self.assertIsNone(chapter.introduction) + self.assertIsNone(chapter.conclusion) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) + if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): + shutil.rmtree(settings.ZDS_APP['content']['repo_public_path']) + if os.path.isdir(settings.MEDIA_ROOT): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 9d94441a00..124e7723f7 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -15,7 +15,7 @@ from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory, \ SubCategoryFactory -from zds.tutorialv2.models import PublishableContent +from zds.tutorialv2.models import PublishableContent, Validation, PublishedContent from zds.gallery.factories import GalleryFactory from zds.forum.factories import ForumFactory, CategoryFactory from zds.forum.models import Topic, Post @@ -1913,6 +1913,774 @@ def test_diff_for_new_content(self): ) self.assertEqual(200, result.status_code) + def test_validation_workflow(self): + """test the different case of validation""" + + text_validation = u'Valide moi ce truc, s\'il te plait' + source = u'http://example.com' # thanks the IANA on that one ;-) + different_source = u'http://example.org' + text_accept = u'C\'est de la m***, mais ok, j\'accepte' + text_reject = u'Je refuse ce truc, arbitrairement !' + + # let's create a medium-size tutorial + midsize_tuto = PublishableContentFactory(type='TUTORIAL') + + midsize_tuto.authors.add(self.user_author) + midsize_tuto.gallery = GalleryFactory() + midsize_tuto.licence = self.licence + midsize_tuto.save() + + # populate with 2 chapters (1 extract each) + midsize_tuto_draft = midsize_tuto.load_version() + chapter1 = ContainerFactory(parent=midsize_tuto_draft, db_objet=midsize_tuto) + ExtractFactory(container=chapter1, db_object=midsize_tuto) + chapter2 = ContainerFactory(parent=midsize_tuto_draft, db_objet=midsize_tuto) + ExtractFactory(container=chapter2, db_object=midsize_tuto) + + # connect with author: + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # ask validation + self.assertEqual(Validation.objects.count(), 0) + + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_validation, + 'source': source, + 'version': midsize_tuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Validation.objects.count(), 1) + + self.assertEqual(PublishableContent.objects.get(pk=midsize_tuto.pk).source, source) # source is set + + validation = Validation.objects.get(content=midsize_tuto) + self.assertIsNotNone(validation) + + self.assertEqual(validation.comment_authors, text_validation) + self.assertEqual(validation.version, midsize_tuto_draft.current_version) + self.assertEqual(validation.status, 'PENDING') + + # ensure that author cannot publish himself + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 403) + + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_accept, + 'is_major': True, + 'source': u'' + }, + follow=False) + self.assertEqual(result.status_code, 403) + + self.assertEqual(Validation.objects.filter(content=midsize_tuto).last().status, 'PENDING') + + # logout, then login with guest + self.client.logout() + + result = self.client.get( + reverse('content:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}) + + '?version=' + validation.version, + follow=False) + self.assertEqual(result.status_code, 302) # no, public cannot access a tutorial in validation ... + + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + + result = self.client.get( + reverse('content:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}) + + '?version=' + validation.version, + follow=False) + self.assertEqual(result.status_code, 403) # ... Same for guest ... + + # then try with staff + self.client.logout() + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + result = self.client.get( + reverse('content:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}) + + '?version=' + validation.version, + follow=False) + self.assertEqual(result.status_code, 200) # ... But staff can, obviously ! + + # reserve tuto: + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'PENDING_V') + self.assertEqual(validation.validator, self.user_staff) + + # unreserve + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'PENDING') + self.assertEqual(validation.validator, None) + + # re-reserve + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'PENDING_V') + self.assertEqual(validation.validator, self.user_staff) + + # let's modify the tutorial and ask for a new validation : + ExtractFactory(container=chapter2, db_object=midsize_tuto) + midsize_tuto = PublishableContent.objects.get(pk=midsize_tuto.pk) + midsize_tuto_draft = midsize_tuto.load_version() + + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_validation, + 'source': source, + 'version': midsize_tuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Validation.objects.count(), 1) + + # ... Therefore, the validation object is in pending status again + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'PENDING') + self.assertEqual(validation.validator, None) + self.assertEqual(validation.version, midsize_tuto_draft.current_version) + + self.assertEqual(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation, + midsize_tuto_draft.current_version) + + self.assertEqual(PrivateTopic.objects.last().author, self.user_staff) # admin has received a PM + + # re-re-reserve (!) + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'PENDING_V') + self.assertEqual(validation.validator, self.user_staff) + + # ensure that author cannot publish himself + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_accept, + 'is_major': True, + 'source': u'' + }, + follow=False) + self.assertEqual(result.status_code, 403) + + self.assertEqual(Validation.objects.filter(content=midsize_tuto).last().status, 'PENDING_V') + + # reject it with staff ! + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + result = self.client.post( + reverse('content:reject-validation', kwargs={'pk': validation.pk}), + { + 'text': text_reject + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.get(content=midsize_tuto) + self.assertEqual(validation.status, 'REJECT') + self.assertEqual(validation.comment_validator, text_reject) + + self.assertIsNone(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation) + + self.assertEqual(PrivateTopic.objects.last().author, self.user_author) # author has received a PM + + # re-ask for validation + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_validation, + 'source': source, + 'version': midsize_tuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Validation.objects.filter(content=midsize_tuto).count(), 2) + + # a new object is created ! + validation = Validation.objects.filter(content=midsize_tuto).last() + self.assertEqual(validation.status, 'PENDING') + self.assertEqual(validation.validator, None) + self.assertEqual(validation.version, midsize_tuto_draft.current_version) + + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.filter(content=midsize_tuto).last() + self.assertEqual(validation.status, 'PENDING_V') + self.assertEqual(validation.validator, self.user_staff) + self.assertEqual(validation.version, midsize_tuto_draft.current_version) + + # accept + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_accept, + 'is_major': True, + 'source': different_source # because ;) + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.filter(content=midsize_tuto).last() + self.assertEqual(validation.status, 'ACCEPT') + self.assertEqual(validation.comment_validator, text_accept) + + self.assertIsNone(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation) + + self.assertEqual(PrivateTopic.objects.filter(author=self.user_author).count(), 2) + self.assertEqual(PrivateTopic.objects.last().author, self.user_author) # author has received another PM + + self.assertEqual(PublishedContent.objects.filter(content=midsize_tuto).count(), 1) + published = PublishedContent.objects.filter(content=midsize_tuto).first() + + self.assertEqual(published.content.source, different_source) + self.assertEqual(published.content_public_slug, midsize_tuto_draft.slug) + self.assertTrue(os.path.exists(published.get_prod_path())) + # ... another test cover the file creation and so all, lets skip this part + + # ensure that author cannot revoke his own publication + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + result = self.client.post( + reverse('content:revoke-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_reject, + 'version': published.sha_public + }, + follow=False) + self.assertEqual(result.status_code, 403) + self.assertEqual(Validation.objects.filter(content=midsize_tuto).last().status, 'ACCEPT') + + # revoke publication with staff + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + result = self.client.post( + reverse('content:revoke-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_reject, + 'version': published.sha_public + }, + follow=False) + self.assertEqual(result.status_code, 302) + + validation = Validation.objects.filter(content=midsize_tuto).last() + self.assertEqual(validation.status, 'PENDING') + + self.assertIsNotNone(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation) + + self.assertEqual(PublishedContent.objects.filter(content=midsize_tuto).count(), 0) + self.assertFalse(os.path.exists(published.get_prod_path())) + + self.assertEqual(PrivateTopic.objects.filter(author=self.user_author).count(), 3) + self.assertEqual(PrivateTopic.objects.last().author, self.user_author) # author has received another PM + + def test_public_access(self): + """Test that everybody have access to a content after its publication""" + + text_validation = u'Valide moi ce truc, please !' + text_publication = u'Aussi tôt dit, aussi tôt fait !' + + # 1. Article: + article = PublishableContentFactory(type='ARTICLE') + + article.authors.add(self.user_author) + article.gallery = GalleryFactory() + article.licence = self.licence + article.save() + + # populate the article + article_draft = article.load_version() + ExtractFactory(container=article_draft, db_object=article) + ExtractFactory(container=article_draft, db_object=article) + + # connect with author: + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # ask validation + self.assertEqual(Validation.objects.count(), 0) + + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': article.pk, 'slug': article.slug}), + { + 'text': text_validation, + 'source': '', + 'version': article_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # login with staff and publish + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + validation = Validation.objects.filter(content=article).last() + + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # accept + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_publication, + 'is_major': True, + 'source': u'' + }, + follow=False) + self.assertEqual(result.status_code, 302) + + published = PublishedContent.objects.filter(content=article).first() + self.assertIsNotNone(published) + + # test access to staff + result = self.client.get(reverse('article:view', kwargs={'pk': article.pk, 'slug': article_draft.slug})) + self.assertEqual(result.status_code, 200) + + # test access to public + self.client.logout() + result = self.client.get(reverse('article:view', kwargs={'pk': article.pk, 'slug': article_draft.slug})) + self.assertEqual(result.status_code, 200) + + # test access for guest user + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + result = self.client.get(reverse('article:view', kwargs={'pk': article.pk, 'slug': article_draft.slug})) + self.assertEqual(result.status_code, 200) + + # 2. middle-size tutorial (just to test the access to chapters) + midsize_tuto = PublishableContentFactory(type='TUTORIAL') + + midsize_tuto.authors.add(self.user_author) + midsize_tuto.gallery = GalleryFactory() + midsize_tuto.licence = self.licence + midsize_tuto.save() + + # populate the midsize_tuto + midsize_tuto_draft = midsize_tuto.load_version() + chapter1 = ContainerFactory(parent=midsize_tuto_draft, db_object=midsize_tuto) + ExtractFactory(container=chapter1, db_object=midsize_tuto) + + # connect with author: + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # ask validation + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto.slug}), + { + 'text': text_validation, + 'source': '', + 'version': midsize_tuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # login with staff and publish + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + validation = Validation.objects.filter(content=midsize_tuto).last() + + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # accept + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_publication, + 'is_major': True, + 'source': u'' + }, + follow=False) + self.assertEqual(result.status_code, 302) + + published = PublishedContent.objects.filter(content=midsize_tuto).first() + self.assertIsNotNone(published) + + # test access to staff + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': midsize_tuto.pk, + 'slug': midsize_tuto_draft.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # test access to public + self.client.logout() + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': midsize_tuto.pk, + 'slug': midsize_tuto_draft.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # test access for guest user + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': midsize_tuto.pk, 'slug': midsize_tuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': midsize_tuto.pk, + 'slug': midsize_tuto_draft.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # 3. a big tutorial (just to test the access to parts and chapters) + bigtuto = PublishableContentFactory(type='TUTORIAL') + + bigtuto.authors.add(self.user_author) + bigtuto.gallery = GalleryFactory() + bigtuto.licence = self.licence + bigtuto.save() + + # populate the bigtuto + bigtuto_draft = bigtuto.load_version() + part1 = ContainerFactory(parent=bigtuto_draft, db_object=bigtuto) + chapter1 = ContainerFactory(parent=part1, db_object=bigtuto) + ExtractFactory(container=chapter1, db_object=bigtuto) + + # connect with author: + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # ask validation + result = self.client.post( + reverse('content:ask-validation', kwargs={'pk': bigtuto.pk, 'slug': bigtuto.slug}), + { + 'text': text_validation, + 'source': '', + 'version': bigtuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # login with staff and publish + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + validation = Validation.objects.filter(content=bigtuto).last() + + result = self.client.post( + reverse('content:reserve-validation', kwargs={'pk': validation.pk}), + { + 'version': validation.version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # accept + result = self.client.post( + reverse('content:accept-validation', kwargs={'pk': validation.pk}), + { + 'text': text_publication, + 'is_major': True, + 'source': u'' + }, + follow=False) + self.assertEqual(result.status_code, 302) + + published = PublishedContent.objects.filter(content=bigtuto).first() + self.assertIsNotNone(published) + + # test access to staff + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # test access to public + self.client.logout() + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # test access for guest user + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 200) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 200) + + # just for the fun of it, lets then revoke publication + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + + result = self.client.post( + reverse('content:revoke-validation', kwargs={'pk': bigtuto.pk, 'slug': bigtuto.slug}), + { + 'text': u'Pour le fun', + 'version': bigtuto_draft.current_version + }, + follow=False) + self.assertEqual(result.status_code, 302) + + # now, let's get a whole bunch of good old fashioned 404 (and not 403 or 302 !!) + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 404) + + # test access to public + self.client.logout() + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 404) + + # test access for guest user + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + + result = self.client.get( + reverse('tutorial:view', kwargs={'pk': bigtuto.pk, 'slug': bigtuto_draft.slug})) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'container_slug': part1.slug + })) + self.assertEqual(result.status_code, 404) + + result = self.client.get( + reverse('tutorial:view-container', + kwargs={ + 'pk': bigtuto.pk, + 'slug': bigtuto_draft.slug, + 'parent_container_slug': part1.slug, + 'container_slug': chapter1.slug + })) + self.assertEqual(result.status_code, 404) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index c11edebc56..8a67db8c87 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -6,7 +6,8 @@ CreateContainer, DisplayContainer, EditContainer, CreateExtract, EditExtract, DeleteContainerOrExtract, \ ManageBetaContent, DisplayHistory, DisplayDiff, ValidationListView, ActivateJSFiddleInContent, \ AskValidationForContent, ReserveValidation, HistoryOfValidationDisplay, MoveChild, DownloadContent, \ - UpdateContentWithArchive, CreateContentFromArchive, RedirectContentSEO, AcceptValidation, RejectValidation + UpdateContentWithArchive, CreateContentFromArchive, RedirectContentSEO, AcceptValidation, RejectValidation, \ + RevokeValidation urlpatterns = patterns('', url(r'^$', ListContents.as_view(), name='index'), @@ -112,6 +113,9 @@ url(r'^valider/accepter/(?P\d+)/$', AcceptValidation.as_view(), name="accept-validation"), + url(r'^valider/depublier/(?P\d+)/(?P.+)/$', RevokeValidation.as_view(), + name="revoke-validation"), + url(r'^valider/liste/$', ValidationListView.as_view(), name="list-validation"), url(r'^(?P\d+)/(?P.+)/(?P\d+)/' diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index c6a54fbcba..8bc619fa06 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -354,3 +354,31 @@ def publish_content(db_object, versioned, is_major_update=True): public_version.save() return public_version + + +def unpublish_content(db_object): + """Remove the given content from the public view + + :param db_object: Database representation of the content + :type db_object: PublishableContent + :return; `True` if unpublished, `False otherwise` + """ + + try: + public_version = PublishedContent.objects.get(content=db_object) + + # clean files + old_path = public_version.get_prod_path() + + if os.path.exists(old_path): + shutil.rmtree(old_path) + + # remove public_version: + public_version.delete() + + return True + + except (ObjectDoesNotExist, IOError): + pass + + return False diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index baf0c8c0ae..1ee92f6fef 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -7,7 +7,7 @@ from urlparse import urlparse, parse_qs from django.template.loader import render_to_string from zds.forum.models import Forum -from zds.tutorialv2.forms import BetaForm, MoveElementForm +from zds.tutorialv2.forms import BetaForm, MoveElementForm, RevokeValidationForm from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError, get_target_tagged_tree from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic @@ -32,7 +32,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.core.files import File from django.core.urlresolvers import reverse from django.db import transaction @@ -63,7 +63,7 @@ from zds.tutorialv2.mixins import SingleContentViewMixin, SingleContentPostMixin, SingleContentFormViewMixin, \ SingleContentDetailViewMixin, SingleContentDownloadViewMixin, SingleOnlineContentDetailViewMixin, ContentTypeMixin from git import GitCommandError -from zds.tutorialv2.utils import publish_content, FailureDuringPublication +from zds.tutorialv2.utils import publish_content, FailureDuringPublication, unpublish_content class RedirectContentSEO(RedirectView): @@ -93,7 +93,7 @@ def get_queryset(self): :return: list of articles """ query_set = PublishableContent.objects.all().filter(authors__in=[self.request.user]) - # TODO: prefetch ! + # TODO: prefetch !tutorial.change_tutorial return query_set def get_context_data(self, **kwargs): @@ -197,22 +197,21 @@ def get_forms(self, context): form_js = JsFiddleActivationForm(initial={"js_support": self.object.js_support}) - form_ask_validation = AskValidationForm(content=self.versioned_object, - initial={ - "source": self.object.source, - 'version': self.sha - }) + context["formAskValidation"] = AskValidationForm( + content=self.versioned_object, initial={"source": self.object.source, 'version': self.sha}) if validation: context["formValid"] = AcceptValidationForm(instance=validation, initial={"source": self.object.source}) context["formReject"] = RejectValidationForm(instance=validation) + if self.versioned_object.sha_public: + context['formRevokeValidation'] = RevokeValidationForm( + instance=self.versioned_object, initial={'version': self.versioned_object.sha_public}) + context["validation"] = validation - context["formAskValidation"] = form_ask_validation context["formJs"] = form_js def get_context_data(self, **kwargs): - """Show the given tutorial if exists.""" context = super(DisplayContent, self).get_context_data(**kwargs) # check whether this tuto support js fiddle @@ -242,6 +241,10 @@ def get_context_data(self, **kwargs): # TODO: deal with messaging and stuff like this !! + if self.request.user.has_perm("tutorial.change_tutorial"): + context['formRevokeValidation'] = RevokeValidationForm( + instance=self.versioned_object, initial={'version': self.versioned_object.sha_public}) + return context @@ -1328,7 +1331,7 @@ def post(self, request, *args, **kwargs): validation.status = "PENDING" validation.save() messages.info(request, _(u"Ce contenu n'est plus réservé")) - return redirect(reverse("content:list_validation")) + return redirect(reverse("content:list-validation")) else: validation.validator = request.user validation.date_reserve = datetime.now() @@ -1373,11 +1376,18 @@ def get_form_kwargs(self): def form_valid(self, form): user = self.request.user - validation = form.validation + + try: + validation = Validation.objects.filter(pk=self.kwargs['pk']).last() + except ObjectDoesNotExist: + raise PermissionDenied if validation.validator != user: raise PermissionDenied + if validation.status != 'PENDING_V': + raise PermissionDenied + # reject validation: validation.comment_validator = form.cleaned_data['text'] validation.status = "REJECT" @@ -1496,6 +1506,69 @@ def form_valid(self, form): return super(AcceptValidation, self).form_valid(form) +class RevokeValidation(LoginRequiredMixin, PermissionRequiredMixin, SingleContentFormViewMixin): + """Unpublish a content and reverse the situation back to a pending validation""" + + permissions = ["tutorial.change_tutorial"] + form_class = RevokeValidationForm + + def get_form_kwargs(self): + kwargs = super(RevokeValidation, self).get_form_kwargs() + kwargs['instance'] = self.versioned_object + return kwargs + + def form_valid(self, form): + + versioned = self.versioned_object + + if form.cleaned_data['version'] != self.object.sha_public: + raise PermissionDenied + + try: + validation = Validation.objects.filter( + content=self.object, + version=self.object.sha_public).latest("date_proposition") + except ObjectDoesNotExist: + raise PermissionDenied + + unpublish_content(self.object) + + validation.status = "PENDING" + validation.date_validation = None + validation.save() + + self.object.sha_public = None + self.object.sha_validation = validation.version + self.object.pubdate = None + self.object.save() + + # send PM + msg = render_to_string( + 'tutorialv2/messages/validation_revoke.msg.html', + { + 'content': versioned, + 'url': versioned.get_absolute_url() + '?version=' + validation.version, + 'admin': self.request.user, + 'message_reject': '\n'.join(['> ' + a for a in form.cleaned_data['text'].split('\n')]) + }) + + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) + send_mp( + bot, + validation.content.authors.all(), + _(u"Dépublication de « {} »").format(validation.content.title), + _(u"Désolé ..."), + msg, + True, + direct=False + ) + + messages.success(self.request, _(u"Le tutoriel a bien été dépublié.")) + self.success_url = self.versioned_object.get_absolute_url() + "?version=" + validation.version + + return super(RevokeValidation, self).form_valid(form) + + class MoveChild(LoginRequiredMixin, SingleContentPostMixin, FormView): model = PublishableContent From a4b305998539909a7ead2581b21835373c0f12f2 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Mon, 20 Apr 2015 07:20:36 -0400 Subject: [PATCH 215/887] Correction T.U. --- zds/tutorialv2/tests/tests_views.py | 22 +++++++++++----------- zds/tutorialv2/views.py | 21 ++++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 124e7723f7..6d2e3e37fa 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -1960,7 +1960,7 @@ def test_validation_workflow(self): self.assertEqual(PublishableContent.objects.get(pk=midsize_tuto.pk).source, source) # source is set - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(content=midsize_tuto).last() self.assertIsNotNone(validation) self.assertEqual(validation.comment_authors, text_validation) @@ -2032,7 +2032,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING_V') self.assertEqual(validation.validator, self.user_staff) @@ -2045,7 +2045,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING') self.assertEqual(validation.validator, None) @@ -2058,7 +2058,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING_V') self.assertEqual(validation.validator, self.user_staff) @@ -2078,8 +2078,8 @@ def test_validation_workflow(self): self.assertEqual(result.status_code, 302) self.assertEqual(Validation.objects.count(), 1) - # ... Therefore, the validation object is in pending status again - validation = Validation.objects.get(content=midsize_tuto) + # ... Therefore, a new Validation object is created + validation = Validation.objects.filter(content=midsize_tuto).last() self.assertEqual(validation.status, 'PENDING') self.assertEqual(validation.validator, None) self.assertEqual(validation.version, midsize_tuto_draft.current_version) @@ -2098,7 +2098,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING_V') self.assertEqual(validation.validator, self.user_staff) @@ -2136,7 +2136,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.get(content=midsize_tuto) + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'REJECT') self.assertEqual(validation.comment_validator, text_reject) @@ -2170,7 +2170,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.filter(content=midsize_tuto).last() + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING_V') self.assertEqual(validation.validator, self.user_staff) self.assertEqual(validation.version, midsize_tuto_draft.current_version) @@ -2186,7 +2186,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.filter(content=midsize_tuto).last() + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'ACCEPT') self.assertEqual(validation.comment_validator, text_accept) @@ -2236,7 +2236,7 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.filter(content=midsize_tuto).last() + validation = Validation.objects.filter(pk=validation.pk).last() self.assertEqual(validation.status, 'PENDING') self.assertIsNotNone(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 1ee92f6fef..6014d7509a 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -32,7 +32,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.core.exceptions import PermissionDenied from django.core.files import File from django.core.urlresolvers import reverse from django.db import transaction @@ -1377,9 +1377,9 @@ def form_valid(self, form): user = self.request.user - try: - validation = Validation.objects.filter(pk=self.kwargs['pk']).last() - except ObjectDoesNotExist: + validation = Validation.objects.filter(pk=self.kwargs['pk']).last() + + if not validation: raise PermissionDenied if validation.validator != user: @@ -1440,6 +1440,9 @@ def form_valid(self, form): user = self.request.user validation = form.validation + if not validation: + raise PermissionDenied + if validation.validator != user: raise PermissionDenied @@ -1524,11 +1527,11 @@ def form_valid(self, form): if form.cleaned_data['version'] != self.object.sha_public: raise PermissionDenied - try: - validation = Validation.objects.filter( - content=self.object, - version=self.object.sha_public).latest("date_proposition") - except ObjectDoesNotExist: + validation = Validation.objects.filter( + content=self.object, + version=self.object.sha_public).latest("date_proposition") + + if not validation: raise PermissionDenied unpublish_content(self.object) From 6172f94f1eda604a89d59f19df2e730532bd9d61 Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 20 Apr 2015 14:04:18 +0200 Subject: [PATCH 216/887] Corrige le bug du jsfiddle --- zds/tutorialv2/tests/tests_views.py | 42 +++++++++++++++++++++++++++++ zds/tutorialv2/views.py | 7 ++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 6d2e3e37fa..cddf8e7643 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -31,6 +31,8 @@ class ContentTests(TestCase): def setUp(self): + self.staff = StaffProfileFactory().user + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' self.mas = ProfileFactory().user settings.ZDS_APP['member']['bot_account'] = self.mas.username @@ -2681,7 +2683,47 @@ def test_public_access(self): })) self.assertEqual(result.status_code, 404) + def test_js_fiddle_activation(self): + + login_check = self.client.login( + username=self.staff.username, + password='hostel77') + self.assertEqual(login_check, True) + result = self.client.post( + reverse('content:activate-jsfiddle'), + { + "pk": self.tuto.pk, + "js_support": True + }, follow=True) + self.assertEqual(result.status_code, 200) + updated = PublishableContent.objects.get(pk=self.tuto.pk) + self.assertTrue(updated.js_support) + result = self.client.post( + reverse('content:activate-jsfiddle'), + { + "pk": self.tuto.pk, + "js_support": False + }, follow=True) + self.assertEqual(result.status_code, 200) + updated = PublishableContent.objects.get(pk=self.tuto.pk) + self.assertFalse(updated.js_support) + self.client.logout() + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + result = self.client.post( + reverse('content:activate-jsfiddle'), + { + "pk": self.tuto.pk, + "js_support": True + }) + self.assertEqual(result.status_code, 403) + + def tearDown(self): + if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): shutil.rmtree(settings.ZDS_APP['content']['repo_private_path']) if os.path.isdir(settings.ZDS_APP['content']['repo_public_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 6014d7509a..16f48438a5 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1243,12 +1243,13 @@ class ActivateJSFiddleInContent(LoginRequiredMixin, PermissionRequiredMixin, For permissions = ["tutorial.change_tutorial"] form_class = JsFiddleActivationForm - http_method_names = ["POST"] + http_method_names = ["post"] def form_valid(self, form): """Change the js fiddle support of content and redirect to the view page """ - content = get_object_or_404(PublishableContent, pk=form.cleaned_data["content"]) - content.js_support = "js_support" in form.cleaned_data and form.cleaned_data["js_support"] + content = get_object_or_404(PublishableContent, pk=form.data["pk"]) + print form.data + content.js_support = "js_support" in form.cleaned_data and form.data["js_support"] == u"True" content.save() return redirect(content.load_version().get_absolute_url()) From 26dc19859b9fdfaa0df9bf81ca375ed465d0ca5a Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 20 Apr 2015 16:28:30 +0200 Subject: [PATCH 217/887] print de debug --- zds/tutorialv2/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 16f48438a5..b18aeddd58 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1248,7 +1248,6 @@ class ActivateJSFiddleInContent(LoginRequiredMixin, PermissionRequiredMixin, For def form_valid(self, form): """Change the js fiddle support of content and redirect to the view page """ content = get_object_or_404(PublishableContent, pk=form.data["pk"]) - print form.data content.js_support = "js_support" in form.cleaned_data and form.data["js_support"] == u"True" content.save() return redirect(content.load_version().get_absolute_url()) From 1bf7a07adbedec7980e3cb2c40bc59bad2ffef02 Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 20 Apr 2015 21:23:05 +0200 Subject: [PATCH 218/887] pep8 --- zds/tutorialv2/tests/tests_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index cddf8e7643..e83cf04821 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -2721,7 +2721,6 @@ def test_js_fiddle_activation(self): }) self.assertEqual(result.status_code, 403) - def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): From b91d0d2470b3ef6426d1ca8653e7bdbbe2ac4163 Mon Sep 17 00:00:00 2001 From: poulp Date: Tue, 21 Apr 2015 09:34:29 +0200 Subject: [PATCH 219/887] fix rss article et tuto --- .gitignore | 2 ++ zds/tutorialv2/feeds.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 046721e1e8..55f58921cd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ base.db /tutoriels-public /media /articles-data +contents-private/ +contents-public/ /tutoriels-private-test /tutoriels-public-test diff --git a/zds/tutorialv2/feeds.py b/zds/tutorialv2/feeds.py index 4b9bf71e56..002e47547e 100644 --- a/zds/tutorialv2/feeds.py +++ b/zds/tutorialv2/feeds.py @@ -28,18 +28,18 @@ def items(self): .prefetch_related("content__authors") if self.content_type is not None: - contents.filter(content_type=self.content_type) + contents = contents.filter(content_type=self.content_type) return contents.order_by('-publication_date')[:ZDS_APP['content']['feed_length']] def item_title(self, item): - return item.title + return item.content.title def item_pubdate(self, item): return item.publication_date def item_description(self, item): - return item.description + return item.content.description def item_author_name(self, item): authors_list = item.content.authors.all() From e35c24193ce96da8d209079c140c1d6e9067f681 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 11:29:36 +0200 Subject: [PATCH 220/887] Ajoute les tags des tutos dans le beta forum --- zds/tutorialv2/tests/tests_views.py | 7 +++++-- zds/tutorialv2/views.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index cddf8e7643..5c1f79315a 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -20,6 +20,8 @@ from zds.forum.factories import ForumFactory, CategoryFactory from zds.forum.models import Topic, Post from zds.mp.models import PrivateTopic +from django.utils.encoding import smart_text + overrided_zds_app = settings.ZDS_APP overrided_zds_app['content']['repo_private_path'] = os.path.join(BASE_DIR, 'contents-private-test') @@ -48,6 +50,7 @@ def setUp(self): self.tuto.authors.add(self.user_author) self.tuto.gallery = GalleryFactory() self.tuto.licence = self.licence + self.tuto.subcategory.add(self.subcategory) self.tuto.save() self.beta_forum = ForumFactory( @@ -540,7 +543,8 @@ def test_beta_workflow(self): beta_topic = PublishableContent.objects.get(pk=self.tuto.pk).beta_topic self.assertEqual(Post.objects.filter(topic=beta_topic).count(), 1) - + self.assertEqual(beta_topic.tags.count(), 1) + self.assertEqual(beta_topic.tags.first().title, smart_text(self.subcategory.title).lower()) # test access for public self.client.logout() @@ -2721,7 +2725,6 @@ def test_js_fiddle_activation(self): }) self.assertEqual(result.status_code, 403) - def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index b18aeddd58..2d3b48e258 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -10,6 +10,7 @@ from zds.tutorialv2.forms import BetaForm, MoveElementForm, RevokeValidationForm from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError, get_target_tagged_tree from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic +from zds.utils.models import Tag try: import ujson as json_reader @@ -64,6 +65,7 @@ SingleContentDetailViewMixin, SingleContentDownloadViewMixin, SingleOnlineContentDetailViewMixin, ContentTypeMixin from git import GitCommandError from zds.tutorialv2.utils import publish_content, FailureDuringPublication, unpublish_content +from django.utils.encoding import smart_text class RedirectContentSEO(RedirectView): @@ -1019,6 +1021,19 @@ def form_valid(self, form): if not topic: # if first time putting the content in beta, send a message on the forum and a PM forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + categories = self.object.subcategory.all() + names = [smart_text(category.title).lower() for category in categories] + existing_tags = Tag.objects.filter(title__in=names).all() + existing_tags_names =[tag.title for tag in existing_tags] + unexisting_tags = list(set(names) - set(existing_tags_names) ) + all_tags = [] + for tag in unexisting_tags: + new_tag = Tag() + new_tag.title = tag + new_tag.save() + all_tags.append(new_tag) + all_tags += existing_tags + create_topic(author=self.request.user, forum=forum, title=_(u"[beta][tutoriel]{0}").format(beta_version.title), @@ -1026,7 +1041,8 @@ def form_valid(self, form): text=msg, related_publishable_content=self.object) topic = self.object.beta_topic - + topic.tags = all_tags + topic.save() bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) msg_pm = render_to_string( 'tutorialv2/messages/beta_activate_pm.msg.html', From fe9ff3623a94f3b94bf0a736fa697bf2b4133eab Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 12:06:00 +0200 Subject: [PATCH 221/887] bugfix --- zds/tutorialv2/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 2d3b48e258..298603054d 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1041,7 +1041,7 @@ def form_valid(self, form): text=msg, related_publishable_content=self.object) topic = self.object.beta_topic - topic.tags = all_tags + topic.tags.add(all_tags) topic.save() bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) msg_pm = render_to_string( From 3c9361333d5867b8dc7933fcae5ab31d126efb80 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 13:09:41 +0200 Subject: [PATCH 222/887] =?UTF-8?q?d=C3=A9place=20la=20requ=C3=AAte=20pour?= =?UTF-8?q?=20les=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 298603054d..4cb09eaede 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1021,6 +1021,9 @@ def form_valid(self, form): if not topic: # if first time putting the content in beta, send a message on the forum and a PM forum = get_object_or_404(Forum, pk=settings.ZDS_APP['forum']['beta_forum_id']) + + # find tags + # TODO: make a util's function of it categories = self.object.subcategory.all() names = [smart_text(category.title).lower() for category in categories] existing_tags = Tag.objects.filter(title__in=names).all() @@ -1040,9 +1043,7 @@ def form_valid(self, form): subtitle=u"{}".format(beta_version.description), text=msg, related_publishable_content=self.object) - topic = self.object.beta_topic - topic.tags.add(all_tags) - topic.save() + bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) msg_pm = render_to_string( 'tutorialv2/messages/beta_activate_pm.msg.html', @@ -1058,6 +1059,18 @@ def form_valid(self, form): msg_pm, False) else: + categories = self.object.subcategory.all() + names = [smart_text(category.title).lower() for category in categories] + existing_tags = Tag.objects.filter(title__in=names).all() + existing_tags_names =[tag.title for tag in existing_tags] + unexisting_tags = list(set(names) - set(existing_tags_names) ) + all_tags = [] + for tag in unexisting_tags: + new_tag = Tag() + new_tag.title = tag + new_tag.save() + all_tags.append(new_tag) + all_tags += existing_tags if not already_in_beta: unlock_topic(topic) msg_post = render_to_string( @@ -1078,6 +1091,9 @@ def form_valid(self, form): send_post(topic, msg_post) self.object.save() + topic = self.object.beta_topic + topic.tags.add(all_tags) + topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) return super(ManageBetaContent, self).form_valid(form) From f6925e2b2765f2a7769b5f623ef39421f25eb73f Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 13:39:51 +0200 Subject: [PATCH 223/887] atomic decorator --- zds/tutorialv2/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4cb09eaede..f9dfaf5aad 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -11,6 +11,7 @@ from zds.tutorialv2.utils import try_adopt_new_child, TooDeepContainerError, get_target_tagged_tree from zds.utils.forums import send_post, unlock_topic, lock_topic, create_topic from zds.utils.models import Tag +from django.utils.decorators import method_decorator try: import ujson as json_reader @@ -980,6 +981,10 @@ class ManageBetaContent(LoggedWithReadWriteHability, SingleContentFormViewMixin) action = None + @method_decorator(transaction.atomic) + def dispatch(self, *args, **kwargs): + super(ManageBetaContent, self).dispatch(*args, **kwargs) + def form_valid(self, form): # check version: try: From 90c376e59382c54c457b8792151ef126c6170408 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 14:09:05 +0200 Subject: [PATCH 224/887] =?UTF-8?q?G=C3=A8re=20la=20longueur=20des=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 2 +- zds/tutorialv2/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 5c1f79315a..cc86ed4e49 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -544,7 +544,7 @@ def test_beta_workflow(self): beta_topic = PublishableContent.objects.get(pk=self.tuto.pk).beta_topic self.assertEqual(Post.objects.filter(topic=beta_topic).count(), 1) self.assertEqual(beta_topic.tags.count(), 1) - self.assertEqual(beta_topic.tags.first().title, smart_text(self.subcategory.title).lower()) + self.assertEqual(beta_topic.tags.first().title, smart_text(self.subcategory.title).lower()[:20]) # test access for public self.client.logout() diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index f9dfaf5aad..a59d6328b5 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1037,7 +1037,7 @@ def form_valid(self, form): all_tags = [] for tag in unexisting_tags: new_tag = Tag() - new_tag.title = tag + new_tag.title = tag[:20] new_tag.save() all_tags.append(new_tag) all_tags += existing_tags @@ -1072,7 +1072,7 @@ def form_valid(self, form): all_tags = [] for tag in unexisting_tags: new_tag = Tag() - new_tag.title = tag + new_tag.title = tag[:20] new_tag.save() all_tags.append(new_tag) all_tags += existing_tags From 54ab782eb7ebf177a6da224049c7f983518907fa Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 14:38:46 +0200 Subject: [PATCH 225/887] pep8 --- zds/tutorialv2/views.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index a59d6328b5..ec877237ec 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1008,9 +1008,7 @@ def form_valid(self, form): elif self.action == 'set': already_in_beta = self.object.in_beta() - if already_in_beta and self.object.sha_beta == sha_beta: - pass # no need to perform additional actions - else: + if not already_in_beta or self.object.sha_beta != sha_beta: self.object.sha_beta = sha_beta self.versioned_object.in_beta = True self.versioned_object.sha_beta = sha_beta @@ -1032,8 +1030,8 @@ def form_valid(self, form): categories = self.object.subcategory.all() names = [smart_text(category.title).lower() for category in categories] existing_tags = Tag.objects.filter(title__in=names).all() - existing_tags_names =[tag.title for tag in existing_tags] - unexisting_tags = list(set(names) - set(existing_tags_names) ) + existing_tags_names = [tag.title for tag in existing_tags] + unexisting_tags = list(set(names) - set(existing_tags_names)) all_tags = [] for tag in unexisting_tags: new_tag = Tag() @@ -1048,7 +1046,7 @@ def form_valid(self, form): subtitle=u"{}".format(beta_version.description), text=msg, related_publishable_content=self.object) - + topic = self.object.beta_topic bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) msg_pm = render_to_string( 'tutorialv2/messages/beta_activate_pm.msg.html', @@ -1067,8 +1065,8 @@ def form_valid(self, form): categories = self.object.subcategory.all() names = [smart_text(category.title).lower() for category in categories] existing_tags = Tag.objects.filter(title__in=names).all() - existing_tags_names =[tag.title for tag in existing_tags] - unexisting_tags = list(set(names) - set(existing_tags_names) ) + existing_tags_names = [tag.title for tag in existing_tags] + unexisting_tags = list(set(names) - set(existing_tags_names)) all_tags = [] for tag in unexisting_tags: new_tag = Tag() From b55ede02c7426779c4db4adc23ac9bf12130389e Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 15:06:20 +0200 Subject: [PATCH 226/887] =?UTF-8?q?ajout=20des=20tags=20=C3=A0=20la=20bdd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index ec877237ec..89f99411e5 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1095,7 +1095,9 @@ def form_valid(self, form): self.object.save() topic = self.object.beta_topic - topic.tags.add(all_tags) + topic.tags.clear() + for tag in all_tags: + topic.tags.add(tags) topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) return super(ManageBetaContent, self).form_valid(form) From 6e390344aeb9703739e576a520d4f98e71fc9aa7 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 15:33:05 +0200 Subject: [PATCH 227/887] typo --- zds/tutorialv2/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 89f99411e5..881599feee 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1097,7 +1097,7 @@ def form_valid(self, form): topic = self.object.beta_topic topic.tags.clear() for tag in all_tags: - topic.tags.add(tags) + topic.tags.add(tag) topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) return super(ManageBetaContent, self).form_valid(form) From f3e968757fe461ad2e8472184a74313d57199d03 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 16:18:33 +0200 Subject: [PATCH 228/887] redirige vers le contenu --- zds/tutorialv2/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 881599feee..2a551add00 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1100,7 +1100,7 @@ def form_valid(self, form): topic.tags.add(tag) topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) - return super(ManageBetaContent, self).form_valid(form) + return redirect(reverse(self.object.get_absolute_url())) class ListOnlineContents(ContentTypeMixin, ListView): From 9644e5de89432ae50aec86173f9daffb90b8c553 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 16:49:54 +0200 Subject: [PATCH 229/887] =?UTF-8?q?redirige=20vers=20le=20contenu=20m?= =?UTF-8?q?=C3=AAme=20si=20on=20enl=C3=A8ve=20la=20beta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox.ini | 9 +++++++++ zds/tutorialv2/views.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7a4b59af06..c38cfdb703 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,15 @@ commands = coverage run --source='.' {toxinidir}/manage.py test {posargs} flake8 +[testenv:back_mysql_tuto] +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-dev.txt + MySQL-python + ujson +commands = + coverage run --source='.' {toxinidir}/manage.py test {posargs} + flake8 + [testenv:back] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 2a551add00..835f3ce8df 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1100,7 +1100,9 @@ def form_valid(self, form): topic.tags.add(tag) topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) - return redirect(reverse(self.object.get_absolute_url())) + if self.object.is_beta(): + self.success_url = self.versioned_object.get_absolute_url_beta() + return redirect(self.success_url) class ListOnlineContents(ContentTypeMixin, ListView): From e31c11de66031971d03becf09532d74ed959bab9 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 17:33:36 +0200 Subject: [PATCH 230/887] typo --- zds/tutorialv2/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 835f3ce8df..8547eb28fe 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1100,7 +1100,7 @@ def form_valid(self, form): topic.tags.add(tag) topic.save() self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) - if self.object.is_beta(): + if self.object.is_beta(sha_beta): self.success_url = self.versioned_object.get_absolute_url_beta() return redirect(self.success_url) From e34b1304984dbc3aff0c738bbf26b5c9c6088191 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 22 Apr 2015 09:32:12 -0400 Subject: [PATCH 231/887] Fixe revocation de publication --- zds/tutorialv2/tests/tests_views.py | 4 +++- zds/tutorialv2/views.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index cc86ed4e49..11661a57b1 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -2242,7 +2242,9 @@ def test_validation_workflow(self): follow=False) self.assertEqual(result.status_code, 302) - validation = Validation.objects.filter(pk=validation.pk).last() + self.assertEqual(Validation.objects.filter(content=midsize_tuto).count(), 2) + + validation = Validation.objects.filter(content=midsize_tuto).last() self.assertEqual(validation.status, 'PENDING') self.assertIsNotNone(PublishableContent.objects.get(pk=midsize_tuto.pk).sha_validation) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8547eb28fe..4af70b896d 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1568,7 +1568,8 @@ def form_valid(self, form): validation = Validation.objects.filter( content=self.object, - version=self.object.sha_public).latest("date_proposition") + version=self.object.sha_public, + status='ACCEPT').last() if not validation: raise PermissionDenied From 840221c08a792b88409528bd364034ab639da91a Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 22 Apr 2015 12:50:25 -0400 Subject: [PATCH 232/887] Fixe probleme beta --- zds/tutorialv2/models.py | 5 ++++- zds/tutorialv2/views.py | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 4474e69fd4..8141d6a510 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -1267,7 +1267,10 @@ def get_commit_author(): if user: aut_user = str(user.pk) - aut_email = str(user.email) + aut_email = None + + if user.email: + aut_email = user.email else: aut_user = ZDS_APP['member']['bot_account'] diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 4af70b896d..5d86db1a84 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -983,7 +983,7 @@ class ManageBetaContent(LoggedWithReadWriteHability, SingleContentFormViewMixin) @method_decorator(transaction.atomic) def dispatch(self, *args, **kwargs): - super(ManageBetaContent, self).dispatch(*args, **kwargs) + return super(ManageBetaContent, self).dispatch(*args, **kwargs) def form_valid(self, form): # check version: @@ -997,6 +997,7 @@ def form_valid(self, form): # topic of the beta version: topic = self.object.beta_topic + # perform actions: if self.action == 'inactive': self.object.sha_beta = None @@ -1008,6 +1009,8 @@ def form_valid(self, form): elif self.action == 'set': already_in_beta = self.object.in_beta() + all_tags = [] + if not already_in_beta or self.object.sha_beta != sha_beta: self.object.sha_beta = sha_beta self.versioned_object.in_beta = True @@ -1032,7 +1035,6 @@ def form_valid(self, form): existing_tags = Tag.objects.filter(title__in=names).all() existing_tags_names = [tag.title for tag in existing_tags] unexisting_tags = list(set(names) - set(existing_tags_names)) - all_tags = [] for tag in unexisting_tags: new_tag = Tag() new_tag.title = tag[:20] @@ -1093,16 +1095,21 @@ def form_valid(self, form): ) send_post(topic, msg_post) + # finally set the tags on the topic + if topic: + topic.tags.clear() + for tag in all_tags: + topic.tags.add(tag) + topic.save() + self.object.save() - topic = self.object.beta_topic - topic.tags.clear() - for tag in all_tags: - topic.tags.add(tag) - topic.save() + self.success_url = self.versioned_object.get_absolute_url(version=sha_beta) + if self.object.is_beta(sha_beta): self.success_url = self.versioned_object.get_absolute_url_beta() - return redirect(self.success_url) + + return super(ManageBetaContent, self).form_valid(form) class ListOnlineContents(ContentTypeMixin, ListView): From 4619f8c46dabc1e0cb4c97dd94a6aad974d87298 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 15:51:37 +0200 Subject: [PATCH 233/887] =?UTF-8?q?essaie=20de=20parall=C3=A9liser=20les?= =?UTF-8?q?=20tuto=20dans=20les=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 71b80b8541..4a260f933f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,8 @@ python: - 2.7 env: - - TEST_APP="-e back_mysql" + - TEST_APP="-e back_mysql -- zds.member, zds.mp, zds.utils, zds.forum, zds.gallery, zds.pages" + - TEST_APP="-e back_mysql -- zds.article, zds.tutorial, zds.tutorialv2 - TEST_APP="-e front" notifications: From 12c7fb5c061cc6970936376afad2b231a6799c33 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 16:14:32 +0200 Subject: [PATCH 234/887] typo --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a260f933f..d053c90f9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ python: - 2.7 env: - - TEST_APP="-e back_mysql -- zds.member, zds.mp, zds.utils, zds.forum, zds.gallery, zds.pages" - - TEST_APP="-e back_mysql -- zds.article, zds.tutorial, zds.tutorialv2 + - TEST_APP="-e back_mysql -- zds.member zds.mp zds.utils zds.forum zds.gallery zds.pages" + - TEST_APP="-e back_mysql -- zds.article zds.tutorial zds.tutorialv2 - TEST_APP="-e front" notifications: From d7ff9463912730e9152a50b8f7c8e81abe4f1c7a Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 22 Apr 2015 16:42:33 +0200 Subject: [PATCH 235/887] ajoute un nouvel environnement --- .travis.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d053c90f9f..2f53616b8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: env: - TEST_APP="-e back_mysql -- zds.member zds.mp zds.utils zds.forum zds.gallery zds.pages" - - TEST_APP="-e back_mysql -- zds.article zds.tutorial zds.tutorialv2 + - TEST_APP="-e back_mysql_tuto -- zds.article zds.tutorial zds.tutorialv2" - TEST_APP="-e front" notifications: diff --git a/tox.ini b/tox.ini index c38cfdb703..cac6b25520 100644 --- a/tox.ini +++ b/tox.ini @@ -57,4 +57,4 @@ commands = flake8 {posargs} max-line-length = 120 exclude = .tox,.venv,build,dist,doc,migrations,urls.py,settings.py,settings_prod.py,settings_test.py # Ignore N802 (functions in lowercase) because the setUp() function in tests -ignore = N802 \ No newline at end of file +ignore = N802 From 04d150809bbc20be373f75b770ab8aca8d897b0f Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Wed, 22 Apr 2015 22:50:44 -0400 Subject: [PATCH 236/887] Support ancienne version manifest.json --- zds/tutorialv2/models.py | 58 +++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 8141d6a510..3a9b4758d9 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -925,7 +925,7 @@ def __init__(self, current_version, _type, title, slug, slug_repository=''): else: self.slug_repository = slug - if os.path.exists(self.get_path()): + if self.slug != '' and os.path.exists(self.get_path()): self.repository = Repo(self.get_path()) def __unicode__(self): @@ -1121,8 +1121,7 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): if 'licence' in json: versioned.licence = Licence.objects.filter(code=json['licence']).first() else: - versioned.licence = \ - Licence.objects.filter(pk=settings.ZDS_APP['content']['default_license_pk']).first() + versioned.licence = Licence.objects.filter(pk=settings.ZDS_APP['content']['default_license_pk']).first() if 'introduction' in json: versioned.introduction = json['introduction'] @@ -1132,9 +1131,12 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): # then, fill container with children fill_containers_from_json(json, versioned) else: - # minimum fallback for version 1.0 + # MINIMUM (!) fallback for version 1.0 if "type" in json: - _type = "TUTORIAL" + if json['type'] == 'article': + _type = 'ARTICLE' + else: + _type = "TUTORIAL" else: _type = "ARTICLE" @@ -1149,7 +1151,51 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): versioned.introduction = json["introduction"] if "conclusion" in json: versioned.conclusion = json["conclusion"] - # as it is just minimum fallback, we do not even try to parse old PART/CHAPTER hierarchy + if 'licence' in json: + versioned.licence = Licence.objects.filter(code=json['licence']).first() + else: + versioned.licence = Licence.objects.filter(pk=settings.ZDS_APP['content']['default_license_pk']).first() + + if _type == 'ARTICLE': + extract = Extract(json['title'], '') + if 'text' in json: + extract.text = json['text'] # probably "text.md" ! + versioned.add_extract(extract, generate_slug=True) + + else: # it's a tutorial + if json['type'] == 'MINI' and 'chapter' in json and 'extracts' in json['chapter']: + for extract in json['chapter']['extracts']: + new_extract = Extract(extract['title'], '{}_{}'.format(extract['pk'], slugify(extract['title']))) + if 'text' in extract: + new_extract.text = extract['text'] + versioned.add_extract(new_extract, generate_slug=False) + + elif json['type'] == 'BIG' and 'parts' in json: + for part in json['parts']: + new_part = Container(part['title'], '{}_{}'.format(part['pk'], slugify(part['title']))) + if 'introduction' in part: + new_part.introduction = part['introduction'] + if 'conclusion' in part: + new_part.conclusion = part['conclusion'] + versioned.add_container(new_part, generate_slug=False) + + if 'chapters' in part: + for chapter in part['chapters']: + new_chapter = Container( + chapter['title'], '{}_{}'.format(chapter['pk'], slugify(chapter['title']))) + if 'introduction' in chapter: + new_chapter.introduction = chapter['introduction'] + if 'conclusion' in chapter: + new_chapter.conclusion = chapter['conclusion'] + new_part.add_container(new_chapter, generate_slug=False) + + if 'extracts' in chapter: + for extract in chapter['extracts']: + new_extract = Extract( + extract['title'], '{}_{}'.format(extract['pk'], slugify(extract['title']))) + if 'text' in extract: + new_extract.text = extract['text'] + new_chapter.add_extract(new_extract, generate_slug=False) return versioned From cb21acfd6e4a47f415859454f089840ace73d60c Mon Sep 17 00:00:00 2001 From: artragis Date: Thu, 23 Apr 2015 14:11:55 +0200 Subject: [PATCH 237/887] =?UTF-8?q?D=C3=A9but=20du=20travail=20sur=20les?= =?UTF-8?q?=20r=C3=A9actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/content_online.html | 59 ++++ zds/tutorialv2/forms.py | 8 +- zds/tutorialv2/mixins.py | 5 + zds/tutorialv2/models.py | 14 +- zds/tutorialv2/urls/urls_contents.py | 6 +- zds/tutorialv2/views.py | 309 ++++++------------ 6 files changed, 184 insertions(+), 217 deletions(-) diff --git a/templates/tutorialv2/view/content_online.html b/templates/tutorialv2/view/content_online.html index 7317b3e624..ebf7313dcd 100644 --- a/templates/tutorialv2/view/content_online.html +++ b/templates/tutorialv2/view/content_online.html @@ -128,3 +128,62 @@

    {% blocktrans %}Administration{% endblocktrans %}< {% include "misc/social_buttons.part.html" with link=content.get_absolute_url_online text=content.title %} {% endblock %} + +{% block content_after %} +

    + {% if content.get_note_count > 0 %} + + {{ content.get_note_count }} + + {% trans "commentaire" %}{{ content.get_note_count|pluralize }} + {% else %} + {% trans "Aucun commentaire" %} + {% endif %} +

    + + + {% include "misc/pagination.part.html" with position="top" topic=tutorial is_online=True anchor="comments" %} + + + {% for message in reactions %} + {% captureas edit_link %} + {% url "content:add-reaction" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas cite_link %} + {% url "content:add-reaction" %}?pk={{ tutorial.pk }}&cite={{ message.pk }} + {% endcaptureas %} + + {% captureas upvote_link %} + {% url "content:up-vote" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas downvote_link %} + {% url "content:down-vote" %}?message={{ message.pk }} + {% endcaptureas %} + + {% captureas alert_solve_link %} + + {% endcaptureas %} + + {% if forloop.first and nb > 1 %} + {% set True as is_repeated_message %} + {% else %} + {% set False as is_repeated_message %} + {% endif %} + + + {% include "misc/message.part.html" with perms_change=perms.tutorial.change_note topic=content comment_schema=True %} + {% endfor %} + + + {% include "misc/pagination.part.html" with position="bottom" topic=tutorial is_online=True anchor="comments" %} + + + + {% captureas form_action %} + {% url 'content:add-reaction' %}?pk={{ content.pk }} + {% endcaptureas %} + + {% include "misc/message_form.html" with member=user topic=content %} +{% endblock %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 1d76567397..f455b9c13f 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -387,11 +387,11 @@ class NoteForm(forms.Form): ) ) - def __init__(self, tutorial, user, *args, **kwargs): + def __init__(self, content, user, *args, **kwargs): super(NoteForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = reverse( - 'zds.tutorial.views.answer') + '?tutorial=' + str(tutorial.pk) + 'content:add-reaction') + '?pk=' + str(content.pk) self.helper.form_method = 'post' self.helper.layout = Layout( @@ -399,7 +399,7 @@ def __init__(self, tutorial, user, *args, **kwargs): Hidden('last_note', '{{ last_note_pk }}'), ) - if tutorial.antispam(user): + if content.antispam(user): if 'text' not in self.initial: self.helper['text'].wrap( Field, @@ -407,7 +407,7 @@ def __init__(self, tutorial, user, *args, **kwargs): u'au moins 15 minutes entre deux messages consécutifs ' u'afin de limiter le flood.'), disabled=True) - elif tutorial.is_locked: + elif content.is_locked: self.helper['text'].wrap( Field, placeholder=_(u'Ce tutoriel est verrouillé.'), diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index 53ee07ca93..c4288e3024 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -23,6 +23,7 @@ class SingleContentViewMixin(object): sha = None is_public = False must_redirect = False + denied_if_lock = False def get_object(self, queryset=None): if self.prefetch_all: @@ -50,6 +51,10 @@ def get_object(self, queryset=None): if self.authorized_for_staff and self.request.user.has_perm('tutorial.change_tutorial'): return obj raise PermissionDenied + + if self.denied_if_lock and self.is_public and obj.is_locked: + raise PermissionDenied + return obj def get_versioned_object(self): diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 4474e69fd4..78206a2200 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -1536,7 +1536,7 @@ def get_note_count(self): """ :return : umber of notes in the tutorial. """ - return ContentReaction.objects.filter(tutorial__pk=self.pk).count() + return ContentReaction.objects.filter(content__pk=self.pk).count() def get_last_note(self): """ @@ -1745,9 +1745,19 @@ class Meta: verbose_name_plural = 'Contenu lus' content = models.ForeignKey(PublishableContent, db_index=True) - note = models.ForeignKey(ContentReaction, db_index=True) + note = models.ForeignKey(ContentReaction, db_index=True, is_null=True) user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + """ + Save this model but check that if we have not a related note it is because the user is content author. + """ + if not self.user in self.content.authors.all() and self.note is None: + raise ValueError("Must be related to a note or be an author") + + return super(ContentRead, self).save(force_insert, force_update, using, update_fields) + def __unicode__(self): return u''.format(self.content, self.user, self.note.pk) diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index 8a67db8c87..91b4d6548c 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -7,7 +7,7 @@ ManageBetaContent, DisplayHistory, DisplayDiff, ValidationListView, ActivateJSFiddleInContent, \ AskValidationForContent, ReserveValidation, HistoryOfValidationDisplay, MoveChild, DownloadContent, \ UpdateContentWithArchive, CreateContentFromArchive, RedirectContentSEO, AcceptValidation, RejectValidation, \ - RevokeValidation + RevokeValidation, SendNoteFormView, UpvoteReaction, DownvoteReaction urlpatterns = patterns('', url(r'^$', ListContents.as_view(), name='index'), @@ -24,6 +24,10 @@ url(r'^telecharger/(?P\d+)/(?P.+)/$', DownloadContent.as_view(), name='download-zip'), + # reactions: + url(r'^reactions/ajouter/$', SendNoteFormView.as_view(), name="add-reaction"), + url(r'^reactions/upvote/$', UpvoteReaction.as_view(), name="up-vote"), + url(r'^reactions/downvote/$', DownvoteReaction.as_view(), name="down-vote"), # create: url(r'^nouveau/$', CreateContent.as_view(), name='create'), diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 8547eb28fe..f3a9f273f9 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -48,7 +48,7 @@ from zds.tutorialv2.forms import ContentForm, ContainerForm, ExtractForm, NoteForm, AskValidationForm, \ AcceptValidationForm, RejectValidationForm, JsFiddleActivationForm, ImportContentForm, ImportNewContentForm from models import PublishableContent, Container, Validation, ContentReaction, init_new_repo, get_content_from_json, \ - BadManifestError, Extract, default_slug_pool, PublishedContent + BadManifestError, Extract, default_slug_pool, PublishedContent, ContentRead from utils import search_container_or_404, search_extract_or_404 from zds.gallery.models import Gallery, UserGallery, Image from zds.member.decorator import can_write_and_read_now, LoginRequiredMixin, LoggedWithReadWriteHability @@ -1705,137 +1705,113 @@ def form_valid(self, form): return redirect(child.get_absolute_url()) -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def reject_tutorial(request): - """Staff reject tutorial of an author.""" +class SendNoteFormView(LoggedWithReadWriteHability, SingleContentFormViewMixin, FormView): + is_public = True + denied_if_lock = True + form_class = NoteForm - # Retrieve current tutorial; + def form_valid(self, form): - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") + if self.object.antispam(self.request.user): + raise PermissionDenied + if "message" in self.request.GET: - if request.user == validation.validator: - validation.comment_validator = request.POST["text"] - validation.status = "REJECT" - validation.date_validation = datetime.now() - validation.save() + if not self.request.GET["message"].isdigit(): + raise Http404 + reaction = ContentReaction.objects\ + .filter(pk=int(self.request.GET["message"]), author=self.request.user) + if reaction is None: + raise Http404 + else: + reaction = ContentReaction() + reaction.related_content = self.object + reaction.update_content(form.cleaned_data["text"]) + reaction.pubdate = datetime.now() + reaction.position = self.object.get_note_count() + 1 + reaction.ip_address = get_client_ip(self.request) + reaction.author = self.request.user + + reaction.save() + self.object.last_note = reaction + self.object.save() + read_note = ContentRead() + read_note.content = self.object + read_note.user = self.request.user + read_note.note = reaction + read_note.save() + self.success_url = reaction.get_absolute_url() + return super(SendNoteFormView, self).form_valid(form) - # Remove sha_validation because we rejected this version of the tutorial. - - tutorial.sha_validation = None - tutorial.pubdate = None - tutorial.save() - messages.info(request, _(u"Le tutoriel a bien été refusé.")) - comment_reject = '\n'.join(['> ' + line for line in validation.comment_validator.split('\n')]) - # send feedback - msg = ( - _(u'Désolé, le zeste **{0}** n\'a malheureusement ' - u'pas passé l’étape de validation. Mais ne désespère pas, ' - u'certaines corrections peuvent surement être faite pour ' - u'l’améliorer et repasser la validation plus tard. ' - u'Voici le message que [{1}]({2}), ton validateur t\'a laissé:\n\n`{3}`\n\n' - u'N\'hésite pas a lui envoyer un petit message pour discuter ' - u'de la décision ou demander plus de détail si tout cela te ' - u'semble injuste ou manque de clarté.') - .format(tutorial.title, - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), - comment_reject)) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Refus de Validation : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le refuser.")) - return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) +class UpvoteReaction(LoginRequiredMixin, FormView): -@can_write_and_read_now -@login_required -@require_POST -@permission_required("tutorial.change_tutorial", raise_exception=True) -def valid_tutorial(request): - """Staff valid tutorial of an author.""" + add_class = CommentLike + """ + :var add_class: The model class where the vote will be added + """ - # Retrieve current tutorial; + remove_class = CommentDislike + """ + :var remove_class: The model class where the vote will be removed if exists + """ - try: - tutorial_pk = request.POST["tutorial"] - except KeyError: - raise Http404 - tutorial = get_object_or_404(PublishableContent, pk=tutorial_pk) - validation = Validation.objects.filter( - tutorial__pk=tutorial_pk, - version=tutorial.sha_validation).latest("date_proposition") - - if request.user == validation.validator: - (output, err) = mep(tutorial, tutorial.sha_validation) - messages.info(request, output) - messages.error(request, err) - validation.comment_validator = request.POST["text"] - validation.status = "ACCEPT" - validation.date_validation = datetime.now() - validation.save() + add_like = 1 + """ + :var add_like: The value that will be added to like total + """ + + add_dislike = 0 + """ + :var add_dislike: The value that will be added to the dislike total + """ + + def post(self, request, *args, **kwargs): + if "message" not in self.request.GET or not self.request.GET["message"].isdigit(): + raise Http404 + note_pk = int(self.request.GET["message"]) + note = get_object_or_404(ContentReaction, pk=note_pk) + resp = {} + user = self.request.user + if note.author.pk != user.pk: + + # Making sure the user is allowed to do that + + if self.add_class.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() == 0: + like = self.add_class() + like.user = user + like.comments = note + note.like += self.add_like + note.dislike += self.add_dislike + note.save() + like.save() + if self.remove_class.objects.filter(user__pk=user.pk, + comments__pk=note_pk).count() > 0: + self.remove_class.objects.filter( + user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.dislike = note.dislike - self.add_like + note.like = note.like - self.add_dislike + note.save() + else: + self.add_class.objects.filter(user__pk=user.pk, + comments__pk=note_pk).all().delete() + note.like = note.like - self.add_like + note.dislike = note.dislike - self.add_dislike + note.save() + resp["upvotes"] = note.like + resp["downvotes"] = note.dislike + if request.is_ajax(): + return HttpResponse(json_writer.dumps(resp)) + else: + return redirect(note.get_absolute_url()) - # Update sha_public with the sha of validation. We don't update sha_draft. - # So, the user can continue to edit his tutorial in offline. - - if request.POST.get('is_major', False) or tutorial.sha_public is None or tutorial.sha_public == '': - tutorial.pubdate = datetime.now() - tutorial.sha_public = validation.version - tutorial.source = request.POST["source"] - tutorial.sha_validation = None - tutorial.save() - messages.success(request, _(u"Le tutoriel a bien été validé.")) - - # send feedback - - msg = ( - _(u'Félicitations ! Le zeste [{0}]({1}) ' - u'a été publié par [{2}]({3}) ! Les lecteurs du monde entier ' - u'peuvent venir l\'éplucher et réagir a son sujet. ' - u'Je te conseille de rester a leur écoute afin ' - u'd\'apporter des corrections/compléments.' - u'Un Tutoriel vivant et a jour est bien plus lu ' - u'qu\'un sujet abandonné !') - .format(tutorial.title, - settings.ZDS_APP['site']['url'] + tutorial.get_absolute_url_online(), - validation.validator.username, - settings.ZDS_APP['site']['url'] + validation.validator.profile.get_absolute_url(), )) - bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) - send_mp( - bot, - tutorial.authors.all(), - _(u"Publication : {0}").format(tutorial.title), - "", - msg, - True, - direct=False, - ) - return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) - else: - messages.error(request, - _(u"Vous devez avoir réservé ce tutoriel " - u"pour pouvoir le valider.")) - return redirect(tutorial.get_absolute_url() + "?version=" + validation.version) + +class DownvoteReaction(UpvoteReaction): + add_class = CommentDislike + remove_class = CommentLike + add_like = 0 + add_dislike = 1 @can_write_and_read_now @@ -2505,90 +2481,3 @@ def edit_note(request): form = NoteForm(g_tutorial, request.user, initial={"text": note.text}) form.helper.form_action = reverse("zds.tutorial.views.edit_note") + "?message=" + str(note_pk) return render(request, "tutorial/comment/edit.html", {"note": note, "tutorial": g_tutorial, "form": form}) - - -@can_write_and_read_now -@login_required -def like_note(request): - """Like a note.""" - try: - note_pk = request.GET["message"] - except KeyError: - raise Http404 - resp = {} - note = get_object_or_404(ContentReaction, pk=note_pk) - - user = request.user - if note.author.pk != request.user.pk: - - # Making sure the user is allowed to do that - - if CommentLike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() == 0: - like = CommentLike() - like.user = user - like.comments = note - note.like = note.like + 1 - note.save() - like.save() - if CommentDislike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() > 0: - CommentDislike.objects.filter( - user__pk=user.pk, - comments__pk=note_pk).all().delete() - note.dislike = note.dislike - 1 - note.save() - else: - CommentLike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).all().delete() - note.like = note.like - 1 - note.save() - resp["upvotes"] = note.like - resp["downvotes"] = note.dislike - if request.is_ajax(): - return HttpResponse(json_writer.dumps(resp)) - else: - return redirect(note.get_absolute_url()) - - -@can_write_and_read_now -@login_required -def dislike_note(request): - """Dislike a note.""" - - try: - note_pk = request.GET["message"] - except KeyError: - raise Http404 - resp = {} - note = get_object_or_404(ContentReaction, pk=note_pk) - user = request.user - if note.author.pk != request.user.pk: - - # Making sure the user is allowed to do that - - if CommentDislike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() == 0: - dislike = CommentDislike() - dislike.user = user - dislike.comments = note - note.dislike = note.dislike + 1 - note.save() - dislike.save() - if CommentLike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() > 0: - CommentLike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).all().delete() - note.like = note.like - 1 - note.save() - else: - CommentDislike.objects.filter(user__pk=user.pk, - comments__pk=note_pk).all().delete() - note.dislike = note.dislike - 1 - note.save() - resp["upvotes"] = note.like - resp["downvotes"] = note.dislike - if request.is_ajax(): - return HttpResponse(json_writer.dumps(resp)) - else: - return redirect(note.get_absolute_url()) From 0e2816e33e8b1b5c0cf9e44b907e43d2566fa814 Mon Sep 17 00:00:00 2001 From: artragis Date: Thu, 23 Apr 2015 14:30:11 +0200 Subject: [PATCH 238/887] =?UTF-8?q?typo=20dans=20le=20mod=C3=A8le=20+=20aj?= =?UTF-8?q?out=20de=20la=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0003_auto_20150423_1429.py | 26 +++++++++++++++++++ zds/tutorialv2/models.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 zds/tutorialv2/migrations/0003_auto_20150423_1429.py diff --git a/zds/tutorialv2/migrations/0003_auto_20150423_1429.py b/zds/tutorialv2/migrations/0003_auto_20150423_1429.py new file mode 100644 index 0000000000..bb67ca9d4d --- /dev/null +++ b/zds/tutorialv2/migrations/0003_auto_20150423_1429.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tutorialv2', '0002_auto_20150417_0445'), + ] + + operations = [ + migrations.AlterField( + model_name='contentread', + name='note', + field=models.ForeignKey(to='tutorialv2.ContentReaction', null=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='publishedcontent', + name='sha_public', + field=models.CharField(db_index=True, max_length=80, null=True, verbose_name=b'Sha1 de la version publi\xc3\xa9e', blank=True), + preserve_default=True, + ), + ] diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 11ce21ea10..27ef1094f9 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -1794,7 +1794,7 @@ class Meta: verbose_name_plural = 'Contenu lus' content = models.ForeignKey(PublishableContent, db_index=True) - note = models.ForeignKey(ContentReaction, db_index=True, is_null=True) + note = models.ForeignKey(ContentReaction, db_index=True, null=True) user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) def save(self, force_insert=False, force_update=False, using=None, From 32cc5206e29e0055a30a775af1a377c1d1e7717a Mon Sep 17 00:00:00 2001 From: artragis Date: Thu, 23 Apr 2015 14:46:06 +0200 Subject: [PATCH 239/887] pep8 --- zds/tutorialv2/models.py | 5 ++--- zds/tutorialv2/views.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/zds/tutorialv2/models.py b/zds/tutorialv2/models.py index 27ef1094f9..6ceeab0be6 100644 --- a/zds/tutorialv2/models.py +++ b/zds/tutorialv2/models.py @@ -1797,12 +1797,11 @@ class Meta: note = models.ForeignKey(ContentReaction, db_index=True, null=True) user = models.ForeignKey(User, related_name='content_notes_read', db_index=True) - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): """ Save this model but check that if we have not a related note it is because the user is content author. """ - if not self.user in self.content.authors.all() and self.note is None: + if self.user not in self.content.authors.all() and self.note is None: raise ValueError("Must be related to a note or be an author") return super(ContentRead, self).save(force_insert, force_update, using, update_fields) diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index 396cc5b1ea..ad9caf1e0b 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1785,7 +1785,7 @@ def post(self, request, *args, **kwargs): # Making sure the user is allowed to do that if self.add_class.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() == 0: + comments__pk=note_pk).count() == 0: like = self.add_class() like.user = user like.comments = note @@ -1794,7 +1794,7 @@ def post(self, request, *args, **kwargs): note.save() like.save() if self.remove_class.objects.filter(user__pk=user.pk, - comments__pk=note_pk).count() > 0: + comments__pk=note_pk).count() > 0: self.remove_class.objects.filter( user__pk=user.pk, comments__pk=note_pk).all().delete() @@ -1803,7 +1803,7 @@ def post(self, request, *args, **kwargs): note.save() else: self.add_class.objects.filter(user__pk=user.pk, - comments__pk=note_pk).all().delete() + comments__pk=note_pk).all().delete() note.like = note.like - self.add_like note.dislike = note.dislike - self.add_dislike note.save() From ddd7f30f95cb60e4bb31772074cc60d7815943f1 Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 24 Apr 2015 17:03:34 +0200 Subject: [PATCH 240/887] TU des notes --- zds/tutorialv2/factories.py | 25 ++++++++++++++++++++++++- zds/tutorialv2/mixins.py | 25 ++++++++++++++++++------- zds/tutorialv2/tests/tests_views.py | 24 ++++++++++++++++++++++-- zds/tutorialv2/views.py | 7 ++++++- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index e5857ad6f2..0054179fd9 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -9,6 +9,7 @@ from zds.tutorialv2.models import PublishableContent, Validation, ContentReaction, Container, Extract from zds.utils.models import SubCategory, Licence from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.tutorialv2.utils import publish_content text_content = u'Ceci est un texte bidon, **avec markown**' @@ -23,8 +24,13 @@ class PublishableContentFactory(factory.DjangoModelFactory): @classmethod def _prepare(cls, create, **kwargs): - publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) + auths = [] + if "author_list" in kwargs: + auths = kwargs.pop("author_list") + publishable_content = super(PublishableContentFactory, cls)._prepare(create, **kwargs) + for auth in auths: + publishable_content.authors.add(auth) publishable_content.gallery = GalleryFactory() for author in publishable_content.authors.all(): UserGalleryFactory(user=author, gallery=publishable_content.gallery) @@ -91,6 +97,23 @@ def _prepare(cls, create, **kwargs): return note +class PublishedContentFactory(PublishableContentFactory): + + @classmethod + def _prepare(cls, create, **kwargs): + """create a new PublishableContent and then publish it. + .. attention: + this method does **not** send you the PublisedContent object that is generated during the publication, + you will have to fetch it by your own means + + :param create: + :param kwargs: + :return: The generated publishable content. + """ + content = super(PublishedContentFactory, cls)._prepare(create, **kwargs) + publish_content(content, content.load_version(), True) + return content + class SubCategoryFactory(factory.DjangoModelFactory): FACTORY_FOR = SubCategory diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index c4288e3024..54d87e39d2 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -27,11 +27,18 @@ class SingleContentViewMixin(object): def get_object(self, queryset=None): if self.prefetch_all: - queryset = PublishableContent.objects \ - .select_related("licence") \ - .prefetch_related("authors") \ - .prefetch_related("subcategory") \ - .filter(pk=self.kwargs["pk"]) + if "pk" in self.request.GET and "pk" not in self.kwargs: + queryset = PublishableContent.objects \ + .select_related("licence") \ + .prefetch_related("authors") \ + .prefetch_related("subcategory") \ + .filter(pk=self.request.GET["pk"]) + else: + queryset = PublishableContent.objects \ + .select_related("licence") \ + .prefetch_related("authors") \ + .prefetch_related("subcategory") \ + .filter(pk=self.kwargs["pk"]) obj = queryset.first() else: @@ -46,7 +53,8 @@ def get_object(self, queryset=None): .filter(old_pk=self.kwargs["pk"], slug=self.kwargs["slug"]) obj = queryset.first() self.must_redirect = True - + if self.is_public and not self.must_redirect: + self.sha = obj.sha_public if self.must_be_author and self.request.user not in obj.authors.all(): if self.authorized_for_staff and self.request.user.has_perm('tutorial.change_tutorial'): return obj @@ -74,7 +82,10 @@ def get_versioned_object(self): # if beta, user can also access to it is_beta = self.object.is_beta(sha) - if self.request.user not in self.object.authors.all() and not is_beta: + if self.object.is_public(sha): + pass + elif self.request.user not in self.object.authors.all() and not is_beta and self.must_be_author: + if not self.request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 11661a57b1..889256ef8d 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -14,8 +14,8 @@ from zds.settings import BASE_DIR from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, LicenceFactory, \ - SubCategoryFactory -from zds.tutorialv2.models import PublishableContent, Validation, PublishedContent + SubCategoryFactory, PublishedContentFactory +from zds.tutorialv2.models import PublishableContent, Validation, PublishedContent, ContentReaction from zds.gallery.factories import GalleryFactory from zds.forum.factories import ForumFactory, CategoryFactory from zds.forum.models import Topic, Post @@ -2727,6 +2727,26 @@ def test_js_fiddle_activation(self): }) self.assertEqual(result.status_code, 403) + def test_add_note(self): + tuto = PublishedContentFactory(author_list=[self.user_author]) + published_obj = PublishedContent.objects\ + .filter(content_pk=tuto.pk, content_public_slug=tuto.slug, content_type=tuto.type)\ + .prefetch_related('content')\ + .prefetch_related("content__authors")\ + .prefetch_related("content__subcategory")\ + .first() + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + + result = self.client.post(reverse("content:add-reaction")+"?pk=" + str(tuto.pk), { + "text": u"Ce tuto est tellement cool :p \\o/ éè" + }) + self.assertEqual(result.status_code, 200) + self.assertEqual(ContentReaction.objects.count(), 1) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/views.py b/zds/tutorialv2/views.py index ad9caf1e0b..31a1241df7 100644 --- a/zds/tutorialv2/views.py +++ b/zds/tutorialv2/views.py @@ -1713,11 +1713,16 @@ def form_valid(self, form): return redirect(child.get_absolute_url()) -class SendNoteFormView(LoggedWithReadWriteHability, SingleContentFormViewMixin, FormView): +class SendNoteFormView(LoggedWithReadWriteHability, SingleContentFormViewMixin): is_public = True denied_if_lock = True + must_be_author = False + only_draft_version = False form_class = NoteForm + def get_form(self, form_class): + return NoteForm(self.object, self.request.user, *self.args, **self.kwargs) + def form_valid(self, form): if self.object.antispam(self.request.user): From 4d2f57f4fcb9acbf462f2d02dd7bf14b69a5ff02 Mon Sep 17 00:00:00 2001 From: Pierre Beaujean Date: Fri, 24 Apr 2015 18:07:17 -0400 Subject: [PATCH 241/887] Re-implemente la ZEP-03 --- templates/tutorialv2/index_online.html | 4 +- templates/tutorialv2/validation/index.html | 8 +- templates/tutorialv2/view/help.html | 232 +++++++++++++-------- zds/tutorialv2/tests/tests_views.py | 181 ++++++++++++++++ zds/tutorialv2/urls/urls_contents.py | 4 +- zds/tutorialv2/views.py | 61 ++++-- 6 files changed, 381 insertions(+), 109 deletions(-) diff --git a/templates/tutorialv2/index_online.html b/templates/tutorialv2/index_online.html index c9c4276f30..eb0ea19c8f 100644 --- a/templates/tutorialv2/index_online.html +++ b/templates/tutorialv2/index_online.html @@ -71,10 +71,10 @@

    - {% trans "Nouveau contenu" %} + {% trans "Créer un nouveau contenu" %} - + {% trans "Aider les auteurs" %} diff --git a/templates/tutorialv2/validation/index.html b/templates/tutorialv2/validation/index.html index ec3ad79a68..da189ad5b8 100644 --- a/templates/tutorialv2/validation/index.html +++ b/templates/tutorialv2/validation/index.html @@ -131,22 +131,22 @@

    {{ headlinesub|safe }}

    {% trans "Filtres" %}