diff --git a/assets/scss/_all-supports.scss b/assets/scss/_all-supports.scss index 7f59097dee..1410b5d4a3 100644 --- a/assets/scss/_all-supports.scss +++ b/assets/scss/_all-supports.scss @@ -48,39 +48,41 @@ background: #062E41; display: none; - p { + span { + display: inline-block; margin: 0; padding: 7px 0; color: #EEE; + line-height: 23px; + } - a { - display: inline-block; - color: #EEE; - padding: 4px 13px; - margin-left: 15px; - background: $blue; - text-decoration: none; + a { + display: inline-block; + color: #EEE; + padding: 4px 13px; + margin-left: 15px; + background: $blue; + text-decoration: none; - &:hover, - &:focus { - background: #EEE; - color: $blue; - } + &:hover, + &:focus { + background: #EEE; + color: $blue; } + } - button { - display: inline-block; - background: none; - border: none; - text-decoration: underline; - margin: 0; - padding: 0; - color: #EEE; + #reject-cookies { + display: inline-block; + background: none; + border: none; + text-decoration: underline; + margin: 0; + padding: 0; + color: #EEE; - &:hover, - &:focus { - text-decoration: none; - } + &:hover, + &:focus { + text-decoration: none; } } @@ -91,6 +93,7 @@ padding: 4px 15px; border: none; transition: background .15s, color .15s; + margin-top: 3px; &:hover, &:focus { @@ -2320,8 +2323,13 @@ table { clear: both; padding-top: 1px; - & > p:first-child { - margin-top: 7px; + & > div { + & > p:first-child { + margin-top: 7px; + } + & > figure:first-child { + margin-top: 8px; + } } .message-hidden-content { @@ -2360,6 +2368,10 @@ table { text-indent: 20px; } + .member-item { + margin: 0; + } + textarea { margin: 10px 0 10px -1px; background-color: transparent; @@ -2417,9 +2429,12 @@ table { } } } - span:not(.has-vote) { - border-bottom: none; - opacity: .5; + .upvote, + .downvote { + &:not(.has-vote) { + border-bottom: none; + opacity: .5; + } } .tick { @@ -2515,7 +2530,8 @@ table { } a, - span, + .upvote, + .downvote, button { display: block; float: left; @@ -2535,7 +2551,8 @@ table { } } a, - span, + .upvote, + .downvote, button.ico-after { border-bottom: 1px solid #D2D5D6; text-decoration: none; diff --git a/templates/article/view.html b/templates/article/view.html index 13de5eb1b0..b1565dbc84 100644 --- a/templates/article/view.html +++ b/templates/article/view.html @@ -14,6 +14,31 @@ +{% block meta_image %}{% spaceless %} + {% if article.image %} + {{ article.image.article_illu.url }} + {% else %} + {{ block.super }} + {% endif %} +{% endspaceless %}{% endblock %} + + + +{% block opengraph %} + + + {% if article.pubdate %} + + {% endif %} + + + {% for tag in tags.all %} + + {% endfor %} +{% endblock %} + + + {% block breadcrumb %}
  • {{ article.title }}
  • {% endblock %} diff --git a/templates/base.html b/templates/base.html index bd057037e5..d201ef6668 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,8 +45,8 @@ - {% captureas image %} - {{ request.META.HTTP_HOST }}{% block image %}{% spaceless %} + {% captureas meta_image %} + {{ request.META.HTTP_HOST }}{% block meta_image %}{% spaceless %} {% static "images/apple-touch-icon-144x144-precomposed.png" %} {% endspaceless %}{% endblock %} {% endcaptureas %} @@ -57,8 +57,8 @@ - - + + {% block opengraph %} {% endblock %} @@ -72,7 +72,7 @@ - + {# Stylesheets #} diff --git a/templates/home.html b/templates/home.html index ca2ab9f2bc..9e71f19a7e 100644 --- a/templates/home.html +++ b/templates/home.html @@ -40,7 +40,7 @@

    Zeste de Savoir, la connaissance pour tous et sans pépins

    Tous les membres peuvent écrire et publier des tutoriels et articles sur le site. Pour assurer la qualité et la pédagogie du contenu, l'équipe du site valide chaque cours avant publication.

    - Tout cela est entièrement gratuit et garanti sans publicité, le site est géré et financé par une association a but non lucratif. + Tout cela est entièrement gratuit et garanti sans publicité, le site est géré et financé par une association à but non lucratif.

    diff --git a/templates/misc/message.part.html b/templates/misc/message.part.html index d643f45cd5..17711f8ab1 100644 --- a/templates/misc/message.part.html +++ b/templates/misc/message.part.html @@ -284,7 +284,7 @@ {% if message.like > 0 %}has-vote{% endif %} {% if not user.is_authenticated and message.like > message.dislike %}voted{% endif %}" {% if message.like > 0 %} - title="{{ message.like }} personnes ont trouvé ce message utile" + title="{{ message.like }} personne{{ message.like|pluralize }} {% if message.like > 1 %}ont{% else %}a{% endif %} trouvé ce message utile" {% endif %} > + @@ -298,7 +298,7 @@ {% if message.dislike > 0 %}has-vote{% endif %} {% if not user.is_authenticated and message.like < message.dislike %}voted{% endif %}" {% if message.dislike > 0 %} - title="{{ message.dislike }} personnes n'ont pas trouvé ce message utile" + title="{{ message.dislike }} personne{{ message.dislike|pluralize }} n'{% if message.dislike > 1 %}ont{% else %}a{% endif %} pas trouvé ce message utile" {% endif %} > - diff --git a/templates/tutorial/chapter/edit.html b/templates/tutorial/chapter/edit.html index c18be0a24a..3db72a8c76 100644 --- a/templates/tutorial/chapter/edit.html +++ b/templates/tutorial/chapter/edit.html @@ -40,5 +40,10 @@

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

    + 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/tutorial/chapter/view_online.html b/templates/tutorial/chapter/view_online.html index aee1ff44cb..a9f5dc7a36 100644 --- a/templates/tutorial/chapter/view_online.html +++ b/templates/tutorial/chapter/view_online.html @@ -9,6 +9,22 @@ +{% block meta_image %}{% spaceless %} + {% if tutorial.image %} + {{ tutorial.image.physical.tutorial_illu.url }} + {% else %} + {{ block.super }} + {% endif %} +{% endspaceless %}{% endblock %} + + + +{% block opengraph %} + {% include "tutorial/includes/opengraph.part.html" %} +{% endblock %} + + + {% block breadcrumb %}
  • {{ tutorial.title }}
  • Éditer le tutoriel
  • @@ -34,5 +31,10 @@

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

    + 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/tutorial/tutorial/import.html b/templates/tutorial/tutorial/import.html index 6dac964765..ccf831b52f 100644 --- a/templates/tutorial/tutorial/import.html +++ b/templates/tutorial/tutorial/import.html @@ -24,13 +24,26 @@

    {% block content %} +

    + Vous êtes l'auteur d'un cours sur le SdZ ? Nous l'avons récupéré pour vous ! Il est disponible hors-ligne et n'attend plus que votre accord pour être publié. Il vous suffit d'en faire la demande à un membre du Staff via le forum ou directement par MP. +

    + +

    Envoi de fichiers .tuto

    +

    + Attention, le fichier attendu n'est pas un .tuto du Site du Zéro, mais un format intermédiaire qui contient du Markdown et non du zCode. Contactez un membre du Staff pour demander la conversion de votre fichier. +

    +

    + Le fichier zip est celui qui contient les ressources de votre tutoriel. Il est également issu du convertisseur Markdown > zCode. +

    {% crispy form %} -
    {% if old_tutos %}

    Récupérer un tutoriel du Site du Zéro

    +

    + Ci-dessous se trouve la liste des tutoriels que vous aviez rédigé sur le Site du Zéro et que nous avons récupéré. Vous pouvez les importer pour continuer leur rédaction et les soumettre à la validation. Si vous pensez qu'il en manque un, vous pouvez contacter un membre du Staff. +

    diff --git a/templates/tutorial/tutorial/view_online.html b/templates/tutorial/tutorial/view_online.html index 12495742e2..4fdcb85d06 100644 --- a/templates/tutorial/tutorial/view_online.html +++ b/templates/tutorial/tutorial/view_online.html @@ -19,7 +19,7 @@ -{% block image %}{% spaceless %} +{% block meta_image %}{% spaceless %} {% if tutorial.image %} {{ tutorial.image.physical.tutorial_illu.url }} {% else %} diff --git a/zds/article/models.py b/zds/article/models.py index 319afe9f34..29fe606c76 100644 --- a/zds/article/models.py +++ b/zds/article/models.py @@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models from math import ceil +from git import Repo import os import string import uuid @@ -25,15 +26,11 @@ from zds.utils import get_current_user from zds.utils import slugify -from zds.utils.articles import export_article +from zds.utils.articles import export_article, get_blob from zds.utils.models import SubCategory, Comment, Licence from django.core.urlresolvers import reverse -IMAGE_MAX_WIDTH = 480 -IMAGE_MAX_HEIGHT = 100 - - def image_path(instance, filename): """Return path to an image.""" ext = filename.split('.')[-1] @@ -141,6 +138,30 @@ def load_json(self, path=None, online=False): else: return None + def load_json_for_public(self): + repo = Repo(self.get_path()) + manarticle = get_blob(repo.commit(self.sha_public).tree, 'manifest.json') + data = json_reader.loads(manarticle) + + return data + + def load_dic(self, article_version): + article_version['pk'] = self.pk + article_version['slug'] = slugify(article_version['title']) + article_version['image'] = self.image + article_version['pubdate'] = self.pubdate + article_version['is_locked'] = self.is_locked + article_version['sha_draft'] = self.sha_draft + article_version['sha_validation'] = self.sha_validation + article_version['sha_public'] = self.sha_public + article_version['get_reaction_count'] = self.get_reaction_count + article_version['get_absolute_url'] = reverse('zds.article.views.view', + args=[self.pk, self.slug]) + article_version['get_absolute_url_online'] = reverse('zds.article.views.view_online', + args=[self.pk,slugify(article_version['title'])]) + + return article_version + def dump_json(self, path=None): if path is None: man_path = os.path.join(self.get_path(), 'manifest.json') diff --git a/zds/article/urls.py b/zds/article/urls.py index f1b2532f12..40c472c366 100644 --- a/zds/article/urls.py +++ b/zds/article/urls.py @@ -29,7 +29,7 @@ url(r'^nouveau/$', 'zds.article.views.new'), url(r'^editer/$', 'zds.article.views.edit'), url(r'^modifier/$', 'zds.article.views.modify'), - url(r'^recherche/(?P\d+)/$', + url(r'^recherche/(?P\d+)/$', 'zds.article.views.find_article'), diff --git a/zds/article/views.py b/zds/article/views.py index b08d78a184..742934eefa 100644 --- a/zds/article/views.py +++ b/zds/article/views.py @@ -55,20 +55,26 @@ def index(request): tag = None if tag is None: - article = Article.objects\ + articles = Article.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 articles in the subcategory specified. - article = Article.objects\ + articles = Article.objects\ .filter(sha_public__isnull=False, subcategory__in=[tag])\ .exclude(sha_public="").order_by('-pubdate')\ .all() + + article_versions = [] + for article in articles: + article_version = article.load_json_for_public() + article_version = article.load_dic(article_version) + article_versions.append(article_version) return render_template('article/index.html', { - 'articles': article, + 'articles': article_versions, 'tag': tag, }) @@ -83,10 +89,6 @@ def view(request, article_pk, article_slug): if not request.user.has_perm('article.change_article'): raise PermissionDenied - # The slug of the article must to be right. - if article_slug != slugify(article.title): - return redirect(article.get_absolute_url()) - # 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: @@ -105,17 +107,8 @@ def view(request, article_pk, article_slug): manifest = get_blob(repo.commit(sha).tree, 'manifest.json') article_version = json_reader.loads(manifest) - article_version['txt'] = get_blob( - repo.commit(sha).tree, - article_version['text']) - article_version['pk'] = article.pk - article_version['slug'] = article.slug - article_version['image'] = article.image - article_version['pubdate'] = article.pubdate - article_version['sha_draft'] = article.sha_draft - article_version['sha_validation'] = article.sha_validation - article_version['sha_public'] = article.sha_public - article_version['get_absolute_url_online'] = article.get_absolute_url_online() + article_version['txt'] = get_blob(repo.commit(sha).tree, article_version['text']) + article_version = article.load_dic(article_version) validation = Validation.objects.filter(article__pk=article.pk, version=sha)\ @@ -135,28 +128,14 @@ def view_online(request, article_pk, article_slug): """Show the given article if exists and is visible.""" article = get_object_or_404(Article, pk=article_pk) - # The slug of the article must to be right. - if article_slug != slugify(article.title): - return redirect(article.get_absolute_url_online()) - # Load the article. - article_version = article.load_json() - txt = open( - os.path.join( - article.get_path(), - article_version['text'] + - '.html'), - "r") + article_version = article.load_json_for_public() + txt = open(os.path.join(article.get_path(), + article_version['text'] + '.html'), + "r") article_version['txt'] = txt.read() txt.close() - article_version['pk'] = article.pk - article_version['slug'] = article.slug - article_version['image'] = article.image - article_version['pubdate'] = article.pubdate - article_version['is_locked'] = article.is_locked - article_version['get_reaction_count'] = article.get_reaction_count - article_version['get_absolute_url'] = article.get_absolute_url() - article_version['get_absolute_url_online'] = article.get_absolute_url_online() + article_version = article.load_dic(article_version) # If the user is authenticated if request.user.is_authenticated(): @@ -364,16 +343,23 @@ def edit(request): }) -def find_article(request, name): +def find_article(request, pk_user): """Find an article from his author.""" - user = get_object_or_404(User, pk=name) + user = get_object_or_404(User, pk=pk_user) articles = Article.objects\ .filter(authors__in=[user], sha_public__isnull=False).exclude(sha_public="")\ .order_by('-pubdate')\ .all() + + article_versions = [] + for article in articles: + article_version = article.load_json_for_public() + article_version = article.load_dic(article_version) + article_versions.append(article_version) + # Paginator return render_template('article/find.html', { - 'articles': articles, 'usr': user, + 'articles': article_versions, 'usr': user, }) diff --git a/zds/member/views.py b/zds/member/views.py index e6f664dbfb..bfc4e5da18 100644 --- a/zds/member/views.py +++ b/zds/member/views.py @@ -117,18 +117,30 @@ def details(request, user_name): fchart = os.path.join(img_path, "mod-{}.svg".format(str(usr.pk))) dot_chart.render_to_file(fchart) my_articles = Article.objects.filter(sha_public__isnull=False).order_by( - "-pubdate").filter(authors__in=[usr]).all() + "-pubdate").filter(authors__in=[usr]).all()[:5] my_tutorials = \ Tutorial.objects.filter(sha_public__isnull=False) \ .filter(authors__in=[usr]) \ .order_by("-pubdate" - ).all() + ).all()[:5] + + my_tuto_versions = [] + for my_tutorial in my_tutorials: + mandata = my_tutorial.load_json_for_public() + mandata = my_tutorial.load_dic(mandata) + my_tuto_versions.append(mandata) + my_article_versions = [] + for my_article in my_articles: + article_version = my_article.load_json_for_public() + article_version = my_article.load_dic(article_version) + my_article_versions.append(article_version) + my_topics = \ Topic.objects\ .filter(author=usr)\ .exclude(Q(forum__group__isnull=False) & ~Q(forum__group__in=request.user.groups.all()))\ .prefetch_related("author")\ - .order_by("-pubdate").all() + .order_by("-pubdate").all()[:5] form = OldTutoForm(profile) oldtutos = [] @@ -142,9 +154,9 @@ def details(request, user_name): "usr": usr, "profile": profile, "bans": bans, - "articles": my_articles[:5], - "tutorials": my_tutorials[:5], - "topics": my_topics[:5], + "articles": my_article_versions, + "tutorials": my_tuto_versions, + "topics": my_topics, "form": form, "old_tutos": oldtutos, }) diff --git a/zds/pages/views.py b/zds/pages/views.py index e372c970a7..14b0e961e8 100644 --- a/zds/pages/views.py +++ b/zds/pages/views.py @@ -29,20 +29,14 @@ def home(request): tutos = [] for tuto in get_last_tutorials(): data = tuto.load_json_for_public() - data['pk'] = tuto.pk - data['image'] = tuto.image - data['gallery'] = tuto.gallery - data['pubdate'] = tuto.pubdate - data['update'] = tuto.update - data['subcategory'] = tuto.subcategory - data['get_absolute_url_online'] = reverse( - 'zds.tutorial.views.view_tutorial_online', - args=[ - tuto.pk, - slugify( - data['title'])]) - + data = tuto.load_dic(data) tutos.append(data) + + articles = [] + for article in get_last_articles(): + data = article.load_json_for_public() + data = article.load_dic(data) + articles.append(data) try: with open(os.path.join(SITE_ROOT, 'quotes.txt'), 'r') as fh: @@ -51,9 +45,8 @@ def home(request): quote = u'Zeste de Savoir, la connaissance pour tous et sans pépins !' return render_template('home.html', { - 'last_topics': get_last_topics(request.user), 'last_tutorials': tutos, - 'last_articles': get_last_articles(), + 'last_articles': articles, 'quote': quote, }) diff --git a/zds/tutorial/forms.py b/zds/tutorial/forms.py index b5a7ea2ff2..add0efbae6 100644 --- a/zds/tutorial/forms.py +++ b/zds/tutorial/forms.py @@ -108,6 +108,7 @@ def __init__(self, *args, **kwargs): Field('image'), Field('introduction', css_class='md-editor'), Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), Field('subcategory'), Field('licence'), ButtonHolder( @@ -153,6 +154,7 @@ def __init__(self, *args, **kwargs): Field('title'), Field('introduction', css_class='md-editor'), Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), ButtonHolder( StrictButton( 'Valider', @@ -200,6 +202,7 @@ def __init__(self, *args, **kwargs): Field('image'), Field('introduction', css_class='md-editor'), Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), ButtonHolder( StrictButton( 'Valider', @@ -236,7 +239,8 @@ def __init__(self, *args, **kwargs): u'Contenu', Field('image'), Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor') + Field('conclusion', css_class='md-editor'), + Hidden('last_hash', '{{ last_hash }}'), ), ButtonHolder( Submit('submit', 'Valider') @@ -265,6 +269,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Field('title'), + Hidden('last_hash', '{{ last_hash }}'), CommonLayoutEditor() ) @@ -387,8 +392,9 @@ def __init__(self, *args, **kwargs): StrictButton( 'Confirmer', type='submit'), - Hidden('tutorial', '{{ tutorial.pk }}'), - Hidden('version', '{{ version }}'), ) + Hidden( + 'tutorial', '{{ tutorial.pk }}'), Hidden( + 'version', '{{ version }}'), ) class ValidForm(forms.Form): diff --git a/zds/tutorial/models.py b/zds/tutorial/models.py index a429fd318d..31171594b8 100644 --- a/zds/tutorial/models.py +++ b/zds/tutorial/models.py @@ -183,7 +183,9 @@ def get_prod_path(self): str(self.pk) + '_' + slugify(data['title'])) def load_dic(self, mandata): - mandata['get_absolute_url_online'] = self.get_absolute_url_online() + mandata['get_absolute_url_online'] = reverse('zds.tutorial.views.view_tutorial_online', + args=[self.pk, slugify(mandata["title"])]) + mandata['get_absolute_url_beta'] = self.get_absolute_url_beta() mandata['get_absolute_url'] = self.get_absolute_url() mandata['get_introduction_online'] = self.get_introduction_online() mandata['get_conclusion_online'] = self.get_conclusion_online() @@ -203,7 +205,9 @@ def load_dic(self, mandata): return mandata - def load_json_for_public(self): + 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(self.sha_public).tree, 'manifest.json') data = json_reader.loads(mantuto) @@ -241,13 +245,21 @@ def dump_json(self, path=None): json_data.write(data.encode('utf-8')) json_data.close() - def get_introduction(self): - path = os.path.join(self.get_path(), self.introduction) - intro = open(path, "r") - intro_contenu = intro.read() - intro.close() - - return intro_contenu.decode('utf-8') + 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") + tutorial_version = json_reader.loads(manifest) + if "introduction" in tutorial_version: + path_tuto = tutorial_version["introduction"] + + if path_tuto: + return get_blob(repo.commit(sha).tree, path_tuto) + else: + return None def get_introduction_online(self): intro = open( @@ -261,12 +273,21 @@ def get_introduction_online(self): return intro_contenu.decode('utf-8') - def get_conclusion(self): - conclu = open(os.path.join(self.get_path(), self.conclusion), "r") - conclu_contenu = conclu.read() - conclu.close() - - return conclu_contenu.decode('utf-8') + def get_conclusion(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") + tutorial_version = json_reader.loads(manifest) + if "introduction" in tutorial_version: + path_tuto = tutorial_version["conclusion"] + + if path_tuto: + return get_blob(repo.commit(sha).tree, path_tuto) + else: + return None def get_conclusion_online(self): conclu = open( @@ -522,16 +543,27 @@ def get_path(self, relative=False): else: return os.path.join(settings.REPO_PATH, self.tutorial.get_phy_slug(), self.get_phy_slug()) - def get_introduction(self): - intro = open( - os.path.join( - self.tutorial.get_path(), - self.introduction), - "r") - intro_contenu = intro.read() - intro.close() - - return intro_contenu.decode('utf-8') + def get_introduction(self, sha=None): + + tutorial = self.tutorial + + # find hash code + if sha is None: + sha = tutorial.sha_draft + repo = Repo(tutorial.get_path()) + + 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 part["pk"] == self.pk: + path_part = part["introduction"] + break + + if path_part: + return get_blob(repo.commit(sha).tree, path_part) + else: + return None def get_introduction_online(self): intro = open( @@ -545,16 +577,27 @@ def get_introduction_online(self): return intro_contenu.decode('utf-8') - def get_conclusion(self): - conclu = open( - os.path.join( - self.tutorial.get_path(), - self.conclusion), - "r") - conclu_contenu = conclu.read() - conclu.close() - - return conclu_contenu.decode('utf-8') + def get_conclusion(self, sha=None): + + tutorial = self.tutorial + + # find hash code + if sha is None: + sha = tutorial.sha_draft + repo = Repo(tutorial.get_path()) + + 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 part["pk"] == self.pk: + path_part = part["conclusion"] + break + + if path_part: + return get_blob(repo.commit(sha).tree, path_part) + else: + return None def get_conclusion_online(self): conclu = open( @@ -702,25 +745,34 @@ def get_path(self, relative=False): return chapter_path - def get_introduction(self): - if self.introduction: - if self.tutorial: - path = os.path.join( - self.tutorial.get_path(), - self.introduction) - else: - path = os.path.join( - self.part.tutorial.get_path(), - self.introduction) + def get_introduction(self, sha=None): - if os.path.isfile(path): - intro = open(path, "r") - intro_contenu = intro.read() - intro.close() - - return intro_contenu.decode('utf-8') - else: - return None + if self.tutorial: + tutorial = self.tutorial + else: + tutorial = self.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 chapter["pk"] == self.pk: + path_chap = chapter["introduction"] + break + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if chapter["pk"] == self.pk: + path_chap = chapter["introduction"] + + if path_chap: + return get_blob(repo.commit(sha).tree, path_chap) else: return None @@ -748,23 +800,34 @@ def get_introduction_online(self): else: return None - def get_conclusion(self): - if self.conclusion: - if self.tutorial: - path = os.path.join(self.tutorial.get_path(), self.conclusion) - else: - path = os.path.join( - self.part.tutorial.get_path(), - self.conclusion) - - if os.path.isfile(path): - conclu = open(path, "r") - conclu_contenu = conclu.read() - conclu.close() + def get_conclusion(self, sha=None): - return conclu_contenu.decode('utf-8') - else: - return None + if self.tutorial: + tutorial = self.tutorial + else: + tutorial = self.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 chapter["pk"] == self.pk: + path_chap = chapter["conclusion"] + break + if "chapter" in tutorial_version: + chapter = tutorial_version["chapter"] + if chapter["pk"] == self.pk: + path_chap = chapter["conclusion"] + + if path_chap: + return get_blob(repo.commit(sha).tree, path_chap) else: return None @@ -802,7 +865,9 @@ def update_children(self): self.introduction = os.path.join("introduction.md") self.conclusion = os.path.join("conclusion.md") self.save() + for extract in self.get_extracts(): + extract.text = extract.get_path(relative=True) extract.save() class Extract(models.Model): @@ -884,20 +949,38 @@ def get_prod_path(self): str(ext['pk']) + "_" + slugify(ext['title'])) \ + '.md.html' - def get_text(self): + def get_text(self, sha=None): + if self.chapter.tutorial: - path = os.path.join(self.chapter.tutorial.get_path(), self.text) + tutorial = self.chapter.tutorial else: - path = os.path.join( - self.chapter.part.tutorial.get_path(), - self.text) - - if os.path.isfile(path): - text = open(path, "r") - text_contenu = text.read() - text.close() - - return text_contenu.decode('utf-8') + tutorial = self.chapter.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"]: + path_ext = extract["text"] + break + + if path_ext: + return get_blob(repo.commit(sha).tree, path_ext) else: return None diff --git a/zds/tutorial/tests.py b/zds/tutorial/tests.py index 1e3493dd41..b54efefd4c 100644 --- a/zds/tutorial/tests.py +++ b/zds/tutorial/tests.py @@ -2,12 +2,13 @@ import os import shutil - +import HTMLParser from django.conf import settings from django.core import mail from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings +from django.utils import html from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory @@ -18,7 +19,7 @@ from zds.gallery.factories import GalleryFactory from zds.tutorial.models import Note, Tutorial, Validation, Extract, Part, Chapter from zds.utils.models import SubCategory, Licence, Alert - +from zds.utils.misc import compute_hash @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test')) @override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test')) @@ -476,7 +477,7 @@ def test_url_for_guest(self): def test_workflow_tuto(self): """Test workflow of tutorial.""" - + # logout before self.client.logout() # login with simple member @@ -508,7 +509,7 @@ def test_workflow_tuto(self): { 'title': u"Partie 1", 'introduction':u"Présentation", - 'conclusion': u"Fin de la présenation", + 'conclusion': u"Fin de la présentation", }, follow=False) self.assertEqual(result.status_code, 302) @@ -526,7 +527,7 @@ def test_workflow_tuto(self): p1.slug]), follow=True) self.assertContains(response=result, text = u"Présentation") - self.assertContains(response=result, text = u"Fin de la présenation") + self.assertContains(response=result, text = u"Fin de la présentation") #add part 2 result = self.client.post( @@ -540,7 +541,7 @@ def test_workflow_tuto(self): self.assertEqual(result.status_code, 302) self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 2) p2 = Part.objects.filter(tutorial=tuto).last() - + self.assertEqual(u"Analyse", p2.get_introduction()) #check view offline result = self.client.get( reverse( @@ -699,6 +700,108 @@ def test_workflow_tuto(self): follow=True) self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") + # add extract 1 of chapter 3 + result = self.client.post( + reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c3.pk), + { + 'title': u"Extrait 1", + 'text':"Prune", + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Extract.objects.filter(chapter=c3).count(), 1) + e1 = Extract.objects.filter(chapter=c3).last() + + # add extract 2 of chapter 3 + result = self.client.post( + reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c3.pk), + { + 'title': u"Extrait 2", + 'text':"Citron", + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Extract.objects.filter(chapter=c3).count(), 2) + e2 = Extract.objects.filter(chapter=c3).last() + + # add extract 3 of chapter 2 + result = self.client.post( + reverse('zds.tutorial.views.add_extract') + '?chapitre={}'.format(c2.pk), + { + 'title': u"Extrait 3", + 'text':"Kiwi", + }, + follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(Extract.objects.filter(chapter=c2).count(), 1) + e3 = Extract.objects.filter(chapter=c2).last() + + #check content edit part + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p1.pk), + follow=True) + self.assertContains(response=result, text = u"Présentation") + self.assertContains(response=result, text = u"Fin de la présentation") + + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p2.pk), + follow=True) + self.assertContains(response=result, text = u"Analyse") + self.assertContains(response=result, text = "Fin de l'analyse") + + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p3.pk), + follow=True) + self.assertContains(response=result, text = u"Expérimentation") + self.assertContains(response=result, text = u"est terminé") + + #check content edit chapter + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c1.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 1") + self.assertContains(response=result, text = u"Mon premier chapitre") + self.assertContains(response=result, text = u"Fin de mon premier chapitre") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c2.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 2") + self.assertContains(response=result, text = u"Mon deuxième chapitre") + self.assertContains(response=result, text = u"Fin de mon deuxième chapitre") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c3.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 2") + self.assertContains(response=result, text = u"Mon troisième chapitre homonyme") + self.assertContains(response=result, text = u"Fin de mon troisième chapitre") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c4.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 1") + self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") + + #check content edit extract + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e1.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 1") + self.assertContains(response=result, text = u"Prune") + + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e2.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 2") + self.assertContains(response=result, text = u"Citron") + + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e3.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 3") + self.assertContains(response=result, text = u"Kiwi") + #edit part 2 result = self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p2.pk), @@ -706,13 +809,15 @@ def test_workflow_tuto(self): 'title': u"Partie 2 : edition de titre", 'introduction': u"Expérimentation : edition d'introduction", 'conclusion': u"C'est terminé : edition de conlusion", + "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), + os.path.join(p2.tutorial.get_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text = u"Partie 2 : edition de titre") self.assertContains(response=result, text = u"Expérimentation : edition d'introduction") self.assertContains(response=result, text = u"C'est terminé : edition de conlusion") self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 3) - + #edit chapter 3 result = self.client.post( reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c3.pk), @@ -720,13 +825,16 @@ def test_workflow_tuto(self): 'title': u"Chapitre 3 : edition de titre", 'introduction': u"Edition d'introduction", 'conclusion': u"Edition de conlusion", + "last_hash": compute_hash([os.path.join(c3.get_path(),"introduction.md"), + os.path.join(c3.get_path(),"conclusion.md")]) }, follow=True) self.assertContains(response=result, text = u"Chapitre 3 : edition de titre") self.assertContains(response=result, text = u"Edition d'introduction") self.assertContains(response=result, text = u"Edition de conlusion") self.assertEqual(Chapter.objects.filter(part=p2.pk).count(), 3) - + p2 = Part.objects.filter(pk=p2.pk).first() + #edit part 2 result = self.client.post( reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p2.pk), @@ -734,6 +842,8 @@ def test_workflow_tuto(self): 'title': u"Partie 2 : seconde edition de titre", 'introduction': u"Expérimentation : seconde edition d'introduction", 'conclusion': u"C'est terminé : seconde edition de conlusion", + "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction), + os.path.join(p2.tutorial.get_path(), p2.conclusion)]) }, follow=True) self.assertContains(response=result, text = u"Partie 2 : seconde edition de titre") @@ -748,12 +858,93 @@ def test_workflow_tuto(self): 'title': u"Chapitre 2 : edition de titre", 'introduction': u"Edition d'introduction", 'conclusion': u"Edition de conlusion", + "last_hash": compute_hash([os.path.join(c2.get_path(),"introduction.md"), + os.path.join(c2.get_path(),"conclusion.md")]) }, follow=True) self.assertContains(response=result, text = u"Chapitre 2 : edition de titre") self.assertContains(response=result, text = u"Edition d'introduction") self.assertContains(response=result, text = u"Edition de conlusion") self.assertEqual(Chapter.objects.filter(part=p2.pk).count(), 3) + + #edit extract 2 + result = self.client.post( + reverse('zds.tutorial.views.edit_extract') + '?extrait={}'.format(e2.pk), + { + 'title': u"Extrait 2 : edition de titre", + 'text': u"Agrume", + "last_hash": compute_hash([os.path.join(e2.get_path())]) + }, + follow=True) + self.assertContains(response=result, text = u"Extrait 2 : edition de titre") + self.assertContains(response=result, text = u"Agrume") + + #check content edit part + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p1.pk), + follow=True) + self.assertContains(response=result, text = u"Présentation") + self.assertContains(response=result, text = u"Fin de la présentation") + + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p2.pk), + follow=True) + self.assertContains(response=result, text = u"Partie 2 : seconde edition de titre") + self.assertContains(response=result, text = "Expérimentation : seconde edition d'introduction") + self.assertContains(response=result, text = "C'est terminé : seconde edition de conlusion") + + result = self.client.get( + reverse('zds.tutorial.views.edit_part')+"?partie={}".format(p3.pk), + follow=True) + self.assertContains(response=result, text = u"Expérimentation") + self.assertContains(response=result, text = u"est terminé") + + #check content edit chapter + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c1.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 1") + self.assertContains(response=result, text = u"Mon premier chapitre") + self.assertContains(response=result, text = u"Fin de mon premier chapitre") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c2.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 2 : edition de titre") + self.assertContains(response=result, text = u"Edition d'introduction") + self.assertContains(response=result, text = u"Edition de conlusion") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c3.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 3 : edition de titre") + self.assertContains(response=result, text = u"Edition d'introduction") + self.assertContains(response=result, text = u"Edition de conlusion") + + result = self.client.get( + reverse('zds.tutorial.views.edit_chapter')+"?chapitre={}".format(c4.pk), + follow=True) + self.assertContains(response=result, text = u"Chapitre 1") + self.assertContains(response=result, text = u"Mon premier chapitre d'une autre partie") + + #check content edit extract + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e1.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 1") + self.assertContains(response=result, text = u"Prune") + + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e2.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 2 : edition de titre") + self.assertContains(response=result, text = u"Agrume") + + result = self.client.get( + reverse('zds.tutorial.views.edit_extract')+"?extrait={}".format(e3.pk), + follow=True) + self.assertContains(response=result, text = u"Extrait 3") + self.assertContains(response=result, text = u"Kiwi") #move chapter 1 against 2 result = self.client.post( @@ -889,52 +1080,111 @@ def test_workflow_tuto(self): follow=True) self.assertEqual(result.status_code, 404) - def test_available_tuto(self): - """ Test that all page of big tutorial is available""" - parts = self.bigtuto.get_parts() - for part in parts: - result = self.client.get(reverse( - 'zds.tutorial.views.view_part_online', - args=[ - self.bigtuto.pk, - self.bigtuto.slug, - part.pk, - part.slug]), - follow=True) - self.assertEqual(result.status_code, 200) - result = self.client.get(reverse( - 'zds.tutorial.views.view_part', - args=[ - self.bigtuto.pk, - self.bigtuto.slug, - part.pk, - part.slug]), - follow=True) - self.assertEqual(result.status_code, 200) - chapters = part.get_chapters() - for chapter in chapters: - result = self.client.get(reverse( - 'zds.tutorial.views.view_chapter_online', - args=[ - self.bigtuto.pk, - self.bigtuto.slug, - part.pk, - part.slug, - chapter.pk, - chapter.slug]), - follow=True) - self.assertEqual(result.status_code, 200) - result = self.client.get(reverse( - 'zds.tutorial.views.view_chapter', - args=[ - self.bigtuto.pk, - self.bigtuto.slug, - part.pk, - part.slug, - chapter.pk, - chapter.slug]), - follow=True) - self.assertEqual(result.status_code, 200) + def test_conflict_does_not_destroy(self): + """tests that simultaneous edition does not conflict""" + sub = SubCategory() + sub.title = "toto" + sub.save() + # logout before + self.client.logout() + # first, login with author : + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + # test tuto + (introduction_path, conclusion_path) =(os.path.join(self.bigtuto.get_path(),"introduction.md"), os.path.join(self.bigtuto.get_path(),"conclusion.md")) + hash = compute_hash([introduction_path, conclusion_path]) + self.client.post( + reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk), + { + 'title': self.bigtuto.title, + 'description': "nouvelle description", + 'subcategory': [sub.pk], + 'introduction': self.bigtuto.get_introduction() +" un essai", + 'conclusion': self.bigtuto.get_conclusion(), + 'last_hash': hash + }, follow= True) + conflict_result = self.client.post( + reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk), + { + 'title': self.bigtuto.title, + 'description': "nouvelle description", + 'subcategory': [sub.pk], + 'introduction': self.bigtuto.get_introduction() +" conflictual", + 'conclusion': self.bigtuto.get_conclusion(), + 'last_hash': hash + }, follow= False) + self.assertEqual(conflict_result.status_code, 200) + self.assertContains(response=conflict_result, text = u"nouvelle version") + + # test parts + + result = self.client.post( + reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(self.bigtuto.pk), + { + 'title': u"Partie 2", + 'introduction': u"Analyse", + 'conclusion': u"Fin de l'analyse", + }, + 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)]) + self.client.post( + reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk), + { + 'title': u"Partie 2 : edition de titre", + 'introduction': u"Expérimentation : edition d'introduction", + 'conclusion': u"C'est terminé : edition de conlusion", + "last_hash": hash + }, + follow=False) + conflict_result = self.client.post( + reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk), + { + 'title': u"Partie 2 : edition de titre", + 'introduction': u"Expérimentation : edition d'introduction conflit", + 'conclusion': u"C'est terminé : edition de conlusion", + "last_hash": hash + }, + follow=False) + self.assertEqual(conflict_result.status_code, 200) + self.assertContains(response=conflict_result, text = u"nouvelle version") + + # test chapter + result = self.client.post( + reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p1.pk), + { + 'title': u"Chapitre 1", + 'introduction':"Mon premier chapitre", + 'conclusion': "Fin de mon premier chapitre", + }, + 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")]) + self.client.post( + reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk), + { + 'title': u"Chapitre 3 : edition de titre", + 'introduction': u"Edition d'introduction", + 'conclusion': u"Edition de conlusion", + "last_hash": hash + }, + follow=True) + conflict_result = self.client.post( + reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk), + { + 'title': u"Chapitre 3 : edition de titre", + 'introduction': u"Edition d'introduction conflict", + 'conclusion': u"Edition de conlusion", + "last_hash": hash + }, + follow=True) + self.assertEqual(conflict_result.status_code, 200) + self.assertContains(response=conflict_result, text = u"nouvelle version") def test_url_for_member(self): """Test simple get request by simple member.""" @@ -1475,6 +1725,8 @@ def test_gallery_tuto_change_name(self): 'introduction': self.bigtuto.introduction, 'description': self.bigtuto.description, 'conclusion': self.bigtuto.conclusion, + 'last_hash': compute_hash([os.path.join(self.bigtuto.get_path(),"introduction.md"), + os.path.join(self.bigtuto.get_path(),"conclusion.md")]) }, follow=True) @@ -2166,6 +2418,8 @@ def test_edit_tuto(self): 'subcategory': [sub.pk], 'introduction': self.minituto.get_introduction(), 'conclusion': self.minituto.get_conclusion(), + 'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"), + os.path.join(self.minituto.get_path(),"conclusion.md")]) }, follow=False ) @@ -2183,6 +2437,8 @@ def test_edit_tuto(self): 'subcategory': [sub.pk], 'introduction': self.minituto.get_introduction(), 'conclusion': self.minituto.get_conclusion(), + 'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"), + os.path.join(self.minituto.get_path(),"conclusion.md")]) }, follow=False ) @@ -2508,6 +2764,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()]) }, follow=True) self.assertEqual(result.status_code, 200) diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py index aa7d672122..8070d47e6b 100644 --- a/zds/tutorial/views.py +++ b/zds/tutorial/views.py @@ -54,8 +54,18 @@ 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 +from zds.utils.misc import compute_hash, content_has_changed +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.""" @@ -79,7 +89,13 @@ def index(request): tutorials = Tutorial.objects.filter( sha_public__isnull=False, subcategory__in=[tag]).exclude(sha_public="").order_by("-pubdate").all() - return render_template("tutorial/index.html", {"tutorials": tutorials, "tag": tag}) + + tuto_versions = [] + for tutorial in tutorials: + mandata = tutorial.load_json_for_public() + mandata = tutorial.load_dic(mandata) + tuto_versions.append(mandata) + return render_template("tutorial/index.html", {"tutorials": tuto_versions, "tag": tag}) # Staff actions. @@ -209,10 +225,6 @@ def history(request, tutorial_pk, tutorial_slug): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied - # Make sure the URL is well-formed - - if not tutorial_slug == slugify(tutorial.title): - return redirect(tutorial.get_absolute_url()) repo = Repo(tutorial.get_path()) logs = repo.head.reference.log() logs = sorted(logs, key=attrgetter("time"), reverse=True) @@ -703,11 +715,10 @@ def view_tutorial(request, tutorial_pk, tutorial_slug): version=sha)\ .order_by("-date_proposition")\ .first() + formAskValidation = AskValidationForm() if tutorial.source: - formAskValidation = AskValidationForm(initial={"source": tutorial.source}) formValid = ValidForm(initial={"source": tutorial.source}) else: - formAskValidation = AskValidationForm() formValid = ValidForm() formReject = RejectForm() return render_template("tutorial/tutorial/view.html", { @@ -969,10 +980,29 @@ 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") 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(), + + }) + return render_template("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"] @@ -1037,7 +1067,7 @@ def edit_tutorial(request): "conclusion": tutorial.get_conclusion(), }) return render_template("tutorial/tutorial/edit.html", - {"tutorial": tutorial, "form": form}) + {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])}) # Parts. @@ -1309,7 +1339,8 @@ def edit_part(request): except KeyError: 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"): @@ -1318,7 +1349,18 @@ def edit_part(request): 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_template("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"] @@ -1347,8 +1389,12 @@ def edit_part(request): form = PartForm({"title": part.title, "introduction": part.get_introduction(), "conclusion": part.get_conclusion()}) - return render_template("tutorial/part/edit.html", {"part": part, - "form": form}) + return render_template("tutorial/part/edit.html", + { + "part": part, + "last_hash": compute_hash([introduction, conclusion]), + "form": form + }) # Chapters. @@ -1403,7 +1449,7 @@ def view_chapter( args=[ tutorial.pk, tutorial.slug, - part["pk"], + part_pk, part["slug"]]) part["tutorial"] = tutorial for chapter in part["chapters"]: @@ -1413,15 +1459,8 @@ def view_chapter( chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url"] = reverse( - "zds.tutorial.views.view_chapter", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"], - chapter["pk"], - chapter["slug"]]) + 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, @@ -1447,8 +1486,10 @@ def view_chapter( 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) + 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_template("tutorial/chapter/view.html", { "tutorial": tutorial, @@ -1497,7 +1538,7 @@ def view_chapter_online( args=[ tutorial.pk, tutorial.slug, - part["pk"], + part_pk, part["slug"]]) part["tutorial"] = mandata part["position_in_tutorial"] = cpt_p @@ -1509,15 +1550,8 @@ def view_chapter_online( chapter["type"] = "BIG" chapter["position_in_part"] = cpt_c chapter["position_in_tutorial"] = cpt_c * cpt_p - chapter["get_absolute_url_online"] = reverse( - "zds.tutorial.views.view_chapter_online", - args=[ - tutorial.pk, - tutorial.slug, - part["pk"], - part["slug"], - chapter["pk"], - chapter["slug"]]) + 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( @@ -1746,7 +1780,10 @@ def edit_chapter(request): 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 @@ -1755,6 +1792,16 @@ def edit_chapter(request): 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_template("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() @@ -1792,16 +1839,9 @@ def edit_chapter(request): ) return redirect(chapter.get_absolute_url()) else: - if chapter.part: - form = ChapterForm({"title": chapter.title, - "introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) - else: - - form = \ - EmbdedChapterForm({"introduction": chapter.get_introduction(), - "conclusion": chapter.get_conclusion()}) + form = render_chapter_form(chapter) return render_template("tutorial/chapter/edit.html", {"chapter": chapter, + "last_hash": compute_hash([introduction, conclusion]), "form": form}) @@ -1886,9 +1926,20 @@ def edit_extract(request): if not request.user.has_perm("tutorial.change_tutorial"): raise PermissionDenied + if request.method == "POST": data = request.POST - + if content_has_changed([extract.get_path()], data["last_hash"]): + form = form = ExtractForm(initial={ + "title": extract.title, + "text": extract.get_text()}) + return render_template("tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "new_version":True, + "form": form + }) # Using the « preview button » if "preview" in data: @@ -1937,8 +1988,12 @@ def edit_extract(request): else: form = ExtractForm({"title": extract.title, "text": extract.get_text()}) - return render_template("tutorial/extract/edit.html", {"extract": extract, - "form": form}) + return render_template("tutorial/extract/edit.html", + { + "extract": extract, + "last_hash": compute_hash([extract.get_path()]), + "form": form + }) @can_write_and_read_now @@ -2030,20 +2085,33 @@ def find_tuto(request, pk_user): type = request.GET["type"] except KeyError: type = None - u = get_object_or_404(User, pk=pk_user) + display_user = get_object_or_404(User, pk=pk_user) if type == "beta": tutorials = Tutorial.objects.all().filter( - authors__in=[u], + 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) + mandata = tutorial.load_dic(mandata) + tuto_versions.append(mandata) + return render_template("tutorial/member/beta.html", - {"tutorials": tutorials, "usr": u}) + {"tutorials": tuto_versions, "usr": display_user}) else: tutorials = Tutorial.objects.all().filter( - authors__in=[u], + 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() + mandata = tutorial.load_dic(mandata) + tuto_versions.append(mandata) - return render_template("tutorial/member/online.html", {"tutorials": tutorials, - "usr": u}) + return render_template("tutorial/member/online.html", {"tutorials": tuto_versions, + "usr": display_user}) def upload_images(images, tutorial): @@ -2828,7 +2896,6 @@ def MEP(tutorial, sha): get_url_images(md_file_contenu, tutorial.get_prod_path()) # convert to out format - out_file = open(os.path.join(tutorial.get_prod_path(), fichier), "w") if md_file_contenu is not None: out_file.write(markdown_to_out(md_file_contenu.encode("utf-8"))) @@ -2986,10 +3053,9 @@ def answer(request): raise PermissionDenied for line in note_cite.text.splitlines(): text = text + "> " + line + "\n" - text = u"{0}Source:[{1}]({2}{3})".format( + text = u"{0}Source:[{1}]({2})".format( text, note_cite.author.username, - settings.SITE_URL, note_cite.get_absolute_url()) form = NoteForm(tutorial, request.user, initial={"text": text}) return render_template("tutorial/comment/new.html", { diff --git a/zds/utils/misc.py b/zds/utils/misc.py index 21a01718e0..f3ddea168e 100644 --- a/zds/utils/misc.py +++ b/zds/utils/misc.py @@ -3,7 +3,7 @@ import os import string import uuid - +import hashlib THUMB_MAX_WIDTH = 80 THUMB_MAX_HEIGHT = 80 @@ -11,6 +11,22 @@ MEDIUM_MAX_WIDTH = 200 MEDIUM_MAX_HEIGHT = 200 +def compute_hash(filenames): + """returns a md5 hexdigest of group of files to check if they have change""" + md5_hash = hashlib.md5() + for filename in filenames: + file_handle = open(filename, 'rb') + must_continue = True + while must_continue: + read_bytes = file_handle.read(8096) + if not read_bytes: + must_continue = False + else: + md5_hash.update(read_bytes) + return md5_hash.hexdigest() + +def content_has_changed(filenames, md5): + return md5 != compute_hash(filenames) def image_path(instance, filename): """Return path to an image.""" diff --git a/zds/utils/tutorials.py b/zds/utils/tutorials.py index acf84fda96..fa53615985 100644 --- a/zds/utils/tutorials.py +++ b/zds/utils/tutorials.py @@ -9,7 +9,6 @@ from zds.utils import slugify - # Export-to-dict functions def export_chapter(chapter, export_all=True): from zds.tutorial.models import Extract @@ -289,3 +288,5 @@ def move(obj, new_pos, position_f, parent_f, children_fn): # All objects have been updated except the current one we want to move, so # we can do it now setattr(obj, position_f, new_pos) + +