From 62230fcfdb794aba5a64899b76862c51ab771d4d Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 26 Sep 2015 18:45:44 +0200 Subject: [PATCH 01/55] =?UTF-8?q?Permet=20de=20migrer=20tout=20le=20monde?= =?UTF-8?q?=20m=C3=AAme=20quand=20un=20tuto=20foire.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit avec try/catch + trace dans la sortie standard (print) --- .../management/commands/migrate_to_zep12.py | 224 +++++++++--------- 1 file changed, 114 insertions(+), 110 deletions(-) diff --git a/zds/tutorialv2/management/commands/migrate_to_zep12.py b/zds/tutorialv2/management/commands/migrate_to_zep12.py index 0a9b1a3961..b53c28b54d 100644 --- a/zds/tutorialv2/management/commands/migrate_to_zep12.py +++ b/zds/tutorialv2/management/commands/migrate_to_zep12.py @@ -378,125 +378,129 @@ def migrate_tuto(tutos, title="Exporting mini tuto"): for i in progressbar(xrange(len(tutos)), title, 100): current = tutos[i] - if not os.path.exists(current.get_path(False)): - sys.stderr.write('Invalid physical path to repository « {} », skipping\n'.format(current.get_path(False))) - continue - exported = PublishableContent() - exported.slug = current.slug - exported.type = "TUTORIAL" - exported.title = current.title - exported.sha_draft = current.sha_draft - exported.sha_beta = current.sha_beta - exported.sha_validation = current.sha_validation - exported.licence = current.licence - exported.update_date = current.update - exported.creation_date = current.create_at - exported.description = current.description - exported.js_support = current.js_support - exported.source = current.source - exported.pubdate = current.pubdate - exported.save() - try: - clean_commit = copy_and_clean_repo(current.get_path(False), exported.get_repo_path(False)) - except InvalidGitRepositoryError as e: - exported.delete() - sys.stderr.write('Repository in « {} » is invalid, skipping\n'.format(e)) - continue + if not os.path.exists(current.get_path(False)): + sys.stderr.write( + 'Invalid physical path to repository « {} », skipping\n'.format(current.get_path(False))) + continue + exported = PublishableContent() + exported.slug = current.slug + exported.type = "TUTORIAL" + exported.title = current.title + exported.sha_draft = current.sha_draft + exported.sha_beta = current.sha_beta + exported.sha_validation = current.sha_validation + exported.licence = current.licence + exported.update_date = current.update + exported.creation_date = current.create_at + exported.description = current.description + exported.js_support = current.js_support + exported.source = current.source + exported.pubdate = current.pubdate + exported.save() - if clean_commit: - exported.sha_draft = clean_commit + try: + clean_commit = copy_and_clean_repo(current.get_path(False), exported.get_repo_path(False)) + except InvalidGitRepositoryError as e: + exported.delete() + sys.stderr.write('Repository in « {} » is invalid, skipping\n'.format(e)) + continue - # save clean up in old module to avoid any trouble - current.sha_draft = clean_commit - current.save() + if clean_commit: + exported.sha_draft = clean_commit - exported.gallery = current.gallery - exported.image = current.image - [exported.subcategory.add(category) for category in current.subcategory.all()] - [exported.helps.add(help) for help in current.helps.all()] - [exported.authors.add(author) for author in current.authors.all()] - exported.save() + # save clean up in old module to avoid any trouble + current.sha_draft = clean_commit + current.save() - # now, re create the manifest.json - versioned = exported.load_version() + exported.gallery = current.gallery + exported.image = current.image + [exported.subcategory.add(category) for category in current.subcategory.all()] + [exported.helps.add(help) for help in current.helps.all()] + [exported.authors.add(author) for author in current.authors.all()] + exported.save() - # this loop is there because of old .tuto import that failed with their chapter intros - for container in versioned.traverse(True): - if container.parent is None: - continue - # in old .tuto file chapter intro are represented as chapter_slug/introduction.md - # instead of part_slug/chapter_slug/introduction.md - corrected_intro_path = file_join(container.get_path(relative=False), "introduction.md") - corrected_ccl_path = file_join(container.get_path(relative=False), "conclusion.md") - if container.get_path(True) not in container.introduction: - if file_exists(corrected_intro_path): - container.introduction = file_join(container.get_path(relative=True), "introduction.md") - else: - container.introduction = None - if container.get_path(True) not in container.conclusion: - if file_exists(corrected_ccl_path): - container.conclusion = file_join(container.get_path(relative=True), "conclusion.md") - else: - container.conclusion = None + # now, re create the manifest.json + versioned = exported.load_version() + + # this loop is there because of old .tuto import that failed with their chapter intros + for container in versioned.traverse(True): + if container.parent is None: + continue + # in old .tuto file chapter intro are represented as chapter_slug/introduction.md + # instead of part_slug/chapter_slug/introduction.md + corrected_intro_path = file_join(container.get_path(relative=False), "introduction.md") + corrected_ccl_path = file_join(container.get_path(relative=False), "conclusion.md") + if container.get_path(True) not in container.introduction: + if file_exists(corrected_intro_path): + container.introduction = file_join(container.get_path(relative=True), "introduction.md") + else: + container.introduction = None + if container.get_path(True) not in container.conclusion: + if file_exists(corrected_ccl_path): + container.conclusion = file_join(container.get_path(relative=True), "conclusion.md") + else: + container.conclusion = None - versioned.licence = exported.licence - versioned.type = "TUTORIAL" - versioned.dump_json() - versioned.repository.index.add(['manifest.json']) # index new manifest before commit - exported.sha_draft = versioned.commit_changes(u"Migration version 2") + versioned.licence = exported.licence + versioned.type = "TUTORIAL" + versioned.dump_json() + versioned.repository.index.add(['manifest.json']) # index new manifest before commit + exported.sha_draft = versioned.commit_changes(u"Migration version 2") - exported.old_pk = current.pk - exported.save() - # export beta forum post - former_topic = Topic.objects.filter(key=current.pk).first() - if former_topic is not None: - former_topic.related_publishable_content = exported - - former_topic.save() - former_first_post = former_topic.first_post() - text = former_first_post.text - text = text.replace(current.get_absolute_url_beta(), exported.get_absolute_url_beta()) - former_first_post.update_content(text) - former_first_post.save() - exported.beta_topic = former_topic - exported.save() - # extract notes - reacts = Note.objects.filter(tutorial__pk=current.pk)\ - .select_related("author")\ - .order_by("pubdate")\ - .all() - migrate_validation(exported, TutorialValidation.objects.filter(tutorial__pk=current.pk)) - if current.last_note: - export_comments(reacts, exported, TutorialRead, current.last_note.pk) - if current.sha_public is not None and current.sha_public != "": - published = publish_content(exported, exported.load_version(current.sha_public), False) - exported.pubdate = current.pubdate - exported.sha_public = current.sha_public - exported.public_version = published + exported.old_pk = current.pk exported.save() - published.content_public_slug = exported.slug - published.publication_date = current.pubdate - - published.save() - # set mapping - map_previous = PublishedContent() - map_previous.content_public_slug = current.slug - map_previous.content_pk = current.pk - map_previous.content_type = 'TUTORIAL' - map_previous.must_redirect = True # will send HTTP 301 if visited ! - map_previous.content = exported - map_previous.save() - # fix strange notification bug - authors = list(exported.authors.all()) - reads_to_delete = ContentRead.objects\ - .filter(content=exported)\ - .exclude(user__pk__in=ContentReaction.objects - .filter(related_content=exported) - .exclude(author__in=authors) - .values_list("author__pk", flat=True)) - for read in reads_to_delete.all(): - read.delete() + # export beta forum post + former_topic = Topic.objects.filter(key=current.pk).first() + if former_topic is not None: + former_topic.related_publishable_content = exported + + former_topic.save() + former_first_post = former_topic.first_post() + text = former_first_post.text + text = text.replace(current.get_absolute_url_beta(), exported.get_absolute_url_beta()) + former_first_post.update_content(text) + former_first_post.save() + exported.beta_topic = former_topic + exported.save() + # extract notes + reacts = Note.objects.filter(tutorial__pk=current.pk)\ + .select_related("author")\ + .order_by("pubdate")\ + .all() + migrate_validation(exported, TutorialValidation.objects.filter(tutorial__pk=current.pk)) + if current.last_note: + export_comments(reacts, exported, TutorialRead, current.last_note.pk) + if current.sha_public is not None and current.sha_public != "": + published = publish_content(exported, exported.load_version(current.sha_public), False) + exported.pubdate = current.pubdate + exported.sha_public = current.sha_public + exported.public_version = published + exported.save() + published.content_public_slug = exported.slug + published.publication_date = current.pubdate + published.save() + # set mapping + map_previous = PublishedContent() + map_previous.content_public_slug = current.slug + map_previous.content_pk = current.pk + map_previous.content_type = 'TUTORIAL' + map_previous.must_redirect = True # will send HTTP 301 if visited ! + map_previous.content = exported + map_previous.save() + # fix strange notification bug + authors = list(exported.authors.all()) + reads_to_delete = ContentRead.objects\ + .filter(content=exported)\ + .exclude(user__pk__in=ContentReaction.objects + .filter(related_content=exported) + .exclude(author__in=authors) + .values_list("author__pk", flat=True)) + for read in reads_to_delete.all(): + read.delete() + except Exception as e: + sys.stderr.write( + current.title + u" d'identifiant " + str(current.pk) + u" n'a pas été migré à cause de : " + str(e)) @transaction.atomic From 69c09b54f211672eaccd5c7087139d7eed9e1f78 Mon Sep 17 00:00:00 2001 From: Eskimon Date: Sun, 27 Sep 2015 23:21:41 +0200 Subject: [PATCH 02/55] =?UTF-8?q?Met=20=C3=A0=20jour=20pillow=20pour=20des?= =?UTF-8?q?=20raisons=20de=20debian8=20et=20libjpeg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 84c7e03ed9..623ffcd24a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ python-memcached==1.54 lxml==3.4.4 factory-boy==2.4.1 pygeoip==0.3.2 -pillow==2.8.1 +pillow==2.9.0 gitpython==1.0.1 https://github.com/zestedesavoir/Python-ZMarkdown/archive/2.6.0-zds.7.zip easy-thumbnails==2.2 From 65ce993765bd651b354a6ce78ef19259f5276320 Mon Sep 17 00:00:00 2001 From: Eskimon Date: Mon, 28 Sep 2015 10:50:12 +0200 Subject: [PATCH 03/55] Fix template since latex update --- assets/tex/template.tex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/tex/template.tex b/assets/tex/template.tex index 8c98f8d726..927cded407 100644 --- a/assets/tex/template.tex +++ b/assets/tex/template.tex @@ -16,6 +16,9 @@ $endif$ \else % if luatex or xelatex \ifxetex + % This will have to be removed some day : http://tex.stackexchange.com/questions/269786/unicode-math-broken + \expandafter\let\csname xetex_suppressfontnotfounderror:D\endcsname + \suppressfontnotfounderror \usepackage{mathspec} \usepackage{xltxtra,xunicode} \else From abf0b544e8bc09c34f7a3cea6a445ca3a94e0e7a Mon Sep 17 00:00:00 2001 From: Eskimon Date: Mon, 28 Sep 2015 11:01:16 +0200 Subject: [PATCH 04/55] Fix install doc for libjpeg --- doc/source/install/backend-linux-install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/install/backend-linux-install.rst b/doc/source/install/backend-linux-install.rst index aca0ef7a93..5ef2be01f9 100644 --- a/doc/source/install/backend-linux-install.rst +++ b/doc/source/install/backend-linux-install.rst @@ -25,13 +25,13 @@ Assurez vous que les dépendances suivantes soient résolues : - libz-dev (peut être libz1g-dev sur système 64bits) - python-sqlparse - libffi : ``apt-get install libffi-dev`` -- libjpeg8 libjpeg8-dev libfreetype6 libfreetype6-dev : ``apt-get install libjpeg8 libjpeg8-dev libfreetype6 libfreetype6-dev`` +- libjpeg62-turbo libjpeg62-turbo-dev libfreetype6 libfreetype6-dev : ``apt-get install libjpeg62-turbo libjpeg62-turbo-dev libfreetype6 libfreetype6-dev`` Ou, en une ligne, .. sourcecode:: bash - apt-get install git python-dev python-setuptools '^geoip(-bin)?$' libgeoip-dev libxml2-dev python-lxml libxslt-dev libz-dev python-sqlparse libjpeg8 libjpeg8-dev libfreetype6 libfreetype6-dev libffi-dev + apt-get install git python-dev python-setuptools '^geoip(-bin)?$' libgeoip-dev libxml2-dev python-lxml libxslt-dev libz-dev python-sqlparse libjpeg62-turbo libjpeg62-turbo-dev libfreetype6 libfreetype6-dev libffi-dev easy_install pip tox Installation et configuration de `virtualenv` From be7b17faab4b721a59bf4f3187fcc27a091c6f25 Mon Sep 17 00:00:00 2001 From: Eskimon Date: Mon, 28 Sep 2015 11:48:57 +0200 Subject: [PATCH 05/55] Fix some stuff in deploy script --- scripts/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 7bf1dcde86..a2b8388bb4 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -49,7 +49,7 @@ git checkout -b $1 # Update application data source ../bin/activate -pip install --upgrade --use-mirrors -r requirements.txt +pip install --upgrade -r requirements.txt python manage.py migrate python manage.py compilemessages # Collect all staticfiles from dist/ and python packages to static/ From 263b09238ac8e001a2a38a059d5ad2c3abd67306 Mon Sep 17 00:00:00 2001 From: Eskimon Date: Mon, 28 Sep 2015 12:52:18 +0200 Subject: [PATCH 06/55] =?UTF-8?q?Ajoute=20des=20migrations=20oubli=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0003_auto_20150928_1249.py | 22 +++++++++++++++++++ .../migrations/0002_auto_20150928_1249.py | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 zds/gallery/migrations/0003_auto_20150928_1249.py create mode 100644 zds/tutorial/migrations/0002_auto_20150928_1249.py diff --git a/zds/gallery/migrations/0003_auto_20150928_1249.py b/zds/gallery/migrations/0003_auto_20150928_1249.py new file mode 100644 index 0000000000..dec660ac84 --- /dev/null +++ b/zds/gallery/migrations/0003_auto_20150928_1249.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import zds.gallery.models +import easy_thumbnails.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0002_auto_20150409_2122'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='physical', + field=easy_thumbnails.fields.ThumbnailerImageField(max_length=200, upload_to=zds.gallery.models.image_path), + preserve_default=True, + ), + ] diff --git a/zds/tutorial/migrations/0002_auto_20150928_1249.py b/zds/tutorial/migrations/0002_auto_20150928_1249.py new file mode 100644 index 0000000000..6fcb01644d --- /dev/null +++ b/zds/tutorial/migrations/0002_auto_20150928_1249.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tutorial', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='chapter', + name='image', + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name=b'Image du chapitre', blank=True, to='gallery.Image', null=True), + preserve_default=True, + ), + ] From 8ababfb66187533e3792d9dd2a189a1320ef1b2c Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 28 Sep 2015 20:59:47 +0200 Subject: [PATCH 07/55] =?UTF-8?q?Fix=20#3009=20:=20une=20mauvaise=20pk=20g?= =?UTF-8?q?=C3=A9n=C3=A8re=20une=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views/views_published.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views/views_published.py b/zds/tutorialv2/views/views_published.py index 0f48b01ad8..bc2209b3d4 100644 --- a/zds/tutorialv2/views/views_published.py +++ b/zds/tutorialv2/views/views_published.py @@ -374,8 +374,11 @@ def get(self, request, *args, **kwargs): return StreamingHttpResponse(json_writer.dumps({"text": text}, ensure_ascii=False)) else: self.quoted_reaction_text = text - - return super(SendNoteFormView, self).get(request, *args, **kwargs) + try: + return super(SendNoteFormView, self).get(request, *args, **kwargs) + except MustRedirect: # if someone changed the pk arguments, and reached a "must redirect" public + # object + raise Http404("Not found public content with pk " + str(self.request.GET.get("pk", 0))) def post(self, request, *args, **kwargs): From ab17cb79cf41ae49f24818aa262031b6e31472e0 Mon Sep 17 00:00:00 2001 From: artragis Date: Tue, 29 Sep 2015 20:06:25 +0200 Subject: [PATCH 08/55] =?UTF-8?q?rend=20le=20follow=20=C3=A0=20l'auteur=20?= =?UTF-8?q?de=20la=20b=C3=A9ta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views/views_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 260bf95d6a..d724af813a 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -1359,7 +1359,7 @@ def form_valid(self, form): for author in self.object.authors.all(): if author.pk is not self.request.user.pk: follow(topic, author) - mark_read(topic, author) + mark_read(topic, author) bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account']) msg_pm = render_to_string( From c167b1f1e2d1b52eb8191d8372d2d7552454c4d7 Mon Sep 17 00:00:00 2001 From: artragis Date: Tue, 29 Sep 2015 20:20:35 +0200 Subject: [PATCH 09/55] ajoute un TU --- zds/tutorialv2/tests/tests_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index f3149d74c6..9ceedfc8e5 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -2656,8 +2656,9 @@ def test_warn_typo(self): self.assertEqual(302, response.status_code) sha_beta = PublishableContent.objects.get(pk=tuto.pk).sha_beta self.assertEqual(sha_draft, sha_beta) - tuto = PublishableContent.objects.get(pk=tuto.pk) + # checks the user follow it + self.assertEqual(TopicRead.objects.filter(topic__pk=tuto.beta_topic.pk).count(), 1) versioned = tuto.load_version(sha_beta) # check if author get error when warning typo on its own tutorial From 2bbd942b1a28eb105ad830cfd58d90d2438146b7 Mon Sep 17 00:00:00 2001 From: pierre-24 Date: Tue, 29 Sep 2015 20:53:12 +0200 Subject: [PATCH 10/55] =?UTF-8?q?Rend=20impossible=20de=20citer=20un=20tex?= =?UTF-8?q?te=20cach=C3=A9=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 17 +++++++++++++++++ zds/tutorialv2/views/views_published.py | 3 +++ 2 files changed, 20 insertions(+) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index f3149d74c6..e27a5dc2ac 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4321,7 +4321,24 @@ def test_hide_reaction(self): self.assertEqual(reaction.text_hidden, text_hidden[:80]) self.assertEqual(reaction.editor, self.user_staff) + # test that someone else is not abble to quote the text + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + + result = self.client.get( + reverse("content:add-reaction") + u'?pk={}&cite={}'.format(self.tuto.pk, reaction.pk), follow=False) + self.assertEqual(result.status_code, 403) # unable to quote a reaction if hidden + # then, unhide it ! + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + result = self.client.post( reverse('content:show-reaction', args=[reaction.pk]), follow=False) diff --git a/zds/tutorialv2/views/views_published.py b/zds/tutorialv2/views/views_published.py index bc2209b3d4..346807dfd3 100644 --- a/zds/tutorialv2/views/views_published.py +++ b/zds/tutorialv2/views/views_published.py @@ -366,6 +366,9 @@ def get(self, request, *args, **kwargs): reaction = ContentReaction.objects.filter(pk=cited_pk).first() + if not reaction.is_visible: + raise PermissionDenied + if reaction: text = '\n'.join('> ' + line for line in reaction.text.split('\n')) text += "\nSource: [{}]({})".format(reaction.author.username, reaction.get_absolute_url()) From 3109d361c709c8e0f4d6da8b98de24ab80c0a20f Mon Sep 17 00:00:00 2001 From: pierre-24 Date: Tue, 29 Sep 2015 21:15:56 +0200 Subject: [PATCH 11/55] =?UTF-8?q?Utilise=20syst=C3=A9matiquement=20la=20fo?= =?UTF-8?q?nction=20`slugify`=20de=20`uuslug`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (pour éviter les incohérences dans le module, même si la rétro-compatibilité est assurée) --- .../management/commands/migrate_to_zep12.py | 2 +- .../commands/upgrade_manifest_to_v2.py | 2 +- zds/tutorialv2/models/models_versioned.py | 2 +- zds/tutorialv2/tests/tests_views.py | 16 ++++- zds/tutorialv2/utils.py | 67 ++++++++++++++----- zds/tutorialv2/views/views_contents.py | 2 +- 6 files changed, 69 insertions(+), 22 deletions(-) diff --git a/zds/tutorialv2/management/commands/migrate_to_zep12.py b/zds/tutorialv2/management/commands/migrate_to_zep12.py index b53c28b54d..dad70ef1e2 100644 --- a/zds/tutorialv2/management/commands/migrate_to_zep12.py +++ b/zds/tutorialv2/management/commands/migrate_to_zep12.py @@ -26,7 +26,7 @@ from django.core.management.base import BaseCommand from django.db import transaction from zds.gallery.models import Gallery, UserGallery, Image -from zds.utils import slugify +from uuslug import slugify from zds.utils.models import CommentLike, CommentDislike from datetime import datetime diff --git a/zds/tutorialv2/management/commands/upgrade_manifest_to_v2.py b/zds/tutorialv2/management/commands/upgrade_manifest_to_v2.py index 56359785d7..a58cf59467 100644 --- a/zds/tutorialv2/management/commands/upgrade_manifest_to_v2.py +++ b/zds/tutorialv2/management/commands/upgrade_manifest_to_v2.py @@ -1,7 +1,7 @@ from zds.tutorialv2.models.models_versioned import Extract, VersionedContent, Container from django.core.management.base import BaseCommand from zds.utils.models import Licence -from zds.utils import slugify +from uuslug import slugify import os try: diff --git a/zds/tutorialv2/models/models_versioned.py b/zds/tutorialv2/models/models_versioned.py index e9edc4f8f7..433e57419b 100644 --- a/zds/tutorialv2/models/models_versioned.py +++ b/zds/tutorialv2/models/models_versioned.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from zds.settings import ZDS_APP from zds.tutorialv2.utils import default_slug_pool, export_content, get_commit_author, InvalidOperationError -from zds.utils import slugify +from uuslug import slugify from zds.utils.misc import compute_hash from zds.tutorialv2.utils import get_blob diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index f3149d74c6..02737092e2 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -3602,7 +3602,7 @@ def test_no_invalid_titles(self): 'subcategory': self.subcategory.pk, } - disallowed_titles = [u'-', u'_', u'__', u'-_-', u'$', u'@', u'&', u'{}', u' '] + disallowed_titles = [u'-', u'_', u'__', u'-_-', u'$', u'@', u'&', u'{}', u' ', u'...'] for title in disallowed_titles: dic['title'] = title @@ -3611,6 +3611,20 @@ def test_no_invalid_titles(self): self.assertEqual(PublishableContent.objects.all().count(), 1) self.assertFalse(result.context['form'].is_valid()) + # Due to the internal use of `unicodedata.normalize()` by uuslug, some unicode characters are translated, and + # therefor gives allowed titles, let's ensure that ! + # (see https://docs.python.org/2/library/unicodedata.html#unicodedata.normalize and + # https://github.com/un33k/python-slugify/blob/master/slugify/slugify.py#L117 for implementation !) + allowed_titles = [u'€€', u'£€'] + prev_count = 1 + + for title in allowed_titles: + dic['title'] = title + result = self.client.post(reverse('content:create-tutorial'), dic, follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotEqual(PublishableContent.objects.all().count(), prev_count) + prev_count += 1 + 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 dca6c877ed..5c369fdf63 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -24,7 +24,9 @@ from zds.tutorialv2 import REPLACE_IMAGE_PATTERN from zds import settings from zds.settings import ZDS_APP -from zds.utils import get_current_user, slugify +from zds.utils import get_current_user +from uuslug import slugify +from zds.utils import slugify as old_slugify from zds.utils.models import Licence from zds.utils.templatetags.emarkdown import emarkdown @@ -792,16 +794,17 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): 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_raise_on_empty(extract['title']))) + new_extract = Extract( + extract['title'], '{}_{}'.format(extract['pk'], slugify_raise_on_empty(extract['title'], True))) 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_raise_on_empty(part['title']))) + new_part = Container( + part['title'], '{}_{}'.format(part['pk'], slugify_raise_on_empty(part['title'], True))) + if 'introduction' in part: new_part.introduction = part['introduction'] if 'conclusion' in part: @@ -811,8 +814,9 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): if 'chapters' in part: for chapter in part['chapters']: new_chapter = Container( - chapter['title'], '{}_{}'.format(chapter['pk'], - slugify_raise_on_empty(chapter['title']))) + chapter['title'], + '{}_{}'.format(chapter['pk'], slugify_raise_on_empty(chapter['title'], True))) + if 'introduction' in chapter: new_chapter.introduction = chapter['introduction'] if 'conclusion' in chapter: @@ -822,8 +826,9 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): if 'extracts' in chapter: for extract in chapter['extracts']: new_extract = Extract( - extract['title'], '{}_{}'.format(extract['pk'], - slugify_raise_on_empty(extract['title']))) + extract['title'], + '{}_{}'.format(extract['pk'], slugify_raise_on_empty(extract['title'], True))) + if 'text' in extract: new_extract.text = extract['text'] new_chapter.add_extract(new_extract, generate_slug=False) @@ -835,18 +840,42 @@ class InvalidSlugError(ValueError): pass -def slugify_raise_on_empty(title): - """use uuslug to generate a slug but if the title is incorrect (only special chars so slug is empty)\ - we raise a ValueError +def check_slug(slug): + """If the title is incorrect (only special chars so slug is empty) + + :param slug: slug to test + :type slug; str + :return: `True` if slug is valid, false otherwise + :rtype: bool + """ + if slug.replace("-", "").replace("_", "") == "": + return False + + return True + + +def slugify_raise_on_empty(title, use_old_slugify=False): + """use uuslug to generate a slug but if the title is incorrect (only special chars so slug is empty), an exception + is raised + :param title: to be slugified title :type title: str + :param use_old_slugify: use the function `slugify()` defined in zds.utils instead of the one in uuslug. Usefull + for retro-compatibility with the old article/tutorial module, SHOULD NOT be used for the new one ! + :type use_old_slugify: bool :raise ValueError: on incorrect slug: :return: the slugified title :rtype: str """ - slug = slugify(title) - if slug.replace("-", "").replace("_", "") == "": - raise InvalidSlugError("slug is incorrect") + + if not use_old_slugify: + slug = slugify(title) + else: + slug = old_slugify(title) + + if not check_slug(slug): + raise InvalidSlugError(slug) + return slug @@ -866,7 +895,9 @@ def fill_containers_from_json(json_sub, parent): if child['object'] == 'container': slug = '' try: - slug = slugify_raise_on_empty(child['slug']) + slug = child['slug'] + if not check_slug(slug): + raise InvalidSlugError(slug) except (ValueError, KeyError): pass new_container = Container(child['title'], slug) @@ -883,7 +914,9 @@ def fill_containers_from_json(json_sub, parent): elif child['object'] == 'extract': slug = '' try: - slug = slugify_raise_on_empty(child['slug']) + slug = child['slug'] + if not check_slug(slug): + raise InvalidSlugError(slug) except (ValueError, KeyError): pass new_extract = Extract(child['title'], slug) diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 260bf95d6a..103a30b950 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -41,7 +41,7 @@ from zds.tutorialv2.utils import search_container_or_404, get_target_tagged_tree, search_extract_or_404, \ try_adopt_new_child, TooDeepContainerError, BadManifestError, get_content_from_json, init_new_repo, \ default_slug_pool, BadArchiveError, InvalidSlugError -from zds.utils import slugify +from uuslug import slugify from zds.utils.forums import send_post, lock_topic, create_topic, unlock_topic from zds.forum.models import Topic, TopicFollowed, follow, mark_read from zds.utils.models import Tag, HelpWriting From 4541383fff15d247e4261b0a04b0c7267a6e410b Mon Sep 17 00:00:00 2001 From: pierre-24 Date: Wed, 30 Sep 2015 21:25:44 +0200 Subject: [PATCH 12/55] =?UTF-8?q?Accc=C3=A8de=20=C3=A0=20l'historique=20?= =?UTF-8?q?=C3=A0=20partir=20de=20n'importe=20quelle=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/tutorialv2/view/content.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 5aee417fba..e9e390631f 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -321,7 +321,7 @@

  • - + {% trans "Historique des versions" %}
  • From e348326f7a58962451b6833fa9190ef394e6b03d Mon Sep 17 00:00:00 2001 From: Situphen Date: Sun, 27 Sep 2015 19:30:34 +0200 Subject: [PATCH 13/55] Corrige un oubli sur les pages listant tutoriels et articles --- templates/tutorialv2/index.html | 10 ++++++++-- templates/tutorialv2/index_online.html | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/templates/tutorialv2/index.html b/templates/tutorialv2/index.html index e9beeb39fc..40871b5099 100644 --- a/templates/tutorialv2/index.html +++ b/templates/tutorialv2/index.html @@ -56,7 +56,7 @@

    {% include "misc/paginator.html" with position="top" %} {% if tutorials != None %} -
    +
    {% for tutorial in tutorials %} {% if filter == 'public' %} {% include "tutorialv2/includes/content_item_type_tutoriel.part.html" with show_description=True public_tutorial=tutorial.public_version %} @@ -65,9 +65,12 @@

    {% include "tutorialv2/includes/content_item_type_tutoriel.part.html" with show_description=True type=type %} {% endif %} {% endfor %} +
    +
    +

    {% elif articles != None %} -
    +
    {% for article in articles %} {% if filter == 'public' %} {% include "tutorialv2/includes/content_item_type_article.part.html" with show_description=True public_article=article.public_version %} @@ -76,6 +79,9 @@

    {% include "tutorialv2/includes/content_item_type_article.part.html" with show_description=True type=type %} {% endif %} {% endfor %} +
    +
    +

    {% endif %} diff --git a/templates/tutorialv2/index_online.html b/templates/tutorialv2/index_online.html index 9f45747b3d..4657d47c50 100644 --- a/templates/tutorialv2/index_online.html +++ b/templates/tutorialv2/index_online.html @@ -56,7 +56,7 @@

    +
    {% for public_content in public_contents %} {% if public_content.content_type == "TUTORIAL" %} {% include "tutorialv2/includes/content_item_type_tutoriel.part.html" with public_tutorial=public_content online=True show_description=True %} @@ -64,6 +64,9 @@

    +
    +

    {% else %}

    From 4f2eb05176c350225bbadf3e9105fda5372247ae Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 09:58:17 +0200 Subject: [PATCH 14/55] =?UTF-8?q?change=20l'url=20pour=20cr=C3=A9er=20un?= =?UTF-8?q?=20article?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit french touch --- zds/tutorialv2/urls/urls_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index 0936aedf7a..5763f59646 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -58,7 +58,7 @@ # create: url(r'^nouveau-tutoriel/$', CreateContent.as_view(created_content_type="TUTORIAL"), name='create-tutorial'), - url(r'^nouveau-article/$', + url(r'^nouvel-article/$', CreateContent.as_view(created_content_type="ARTICLE"), name='create-article'), url(r'^nouveau-conteneur/(?P\d+)/(?P.+)/(?P.+)/$', CreateContainer.as_view(), From 2882c371708b875427d10eb9042e370790ec198a Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 10:01:16 +0200 Subject: [PATCH 15/55] L'utilisateur s'affiche dans le mp de beta --- zds/tutorialv2/views/views_contents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 4700e07ec8..8277c192fc 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -1367,7 +1367,8 @@ def form_valid(self, form): { 'content': beta_version, 'type': _type, - 'url': settings.ZDS_APP['site']['url'] + topic.get_absolute_url() + 'url': settings.ZDS_APP['site']['url'] + topic.get_absolute_url(), + 'user': self.request.user } ) send_mp(bot, From b17e1782e3a9718a8d986c8085cfc3f70f3396ec Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 12:03:36 +0200 Subject: [PATCH 16/55] resolve merge conflict --- zds/tutorialv2/tests/tests_utils.py | 22 +++++++++++++++++++++- zds/tutorialv2/utils.py | 11 +++++++++++ zds/tutorialv2/views/views_published.py | 4 ++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index d64d2ef9c2..804db7ade4 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -16,7 +16,7 @@ PublishedContentFactory from zds.gallery.factories import UserGalleryFactory from zds.tutorialv2.utils import get_target_tagged_tree_for_container, publish_content, unpublish_content, \ - get_target_tagged_tree_for_extract, retrieve_and_update_images_links + get_target_tagged_tree_for_extract, retrieve_and_update_images_links, last_participation_is_old from zds.tutorialv2.models.models_database import PublishableContent, PublishedContent, ContentReaction, ContentRead from django.core.management import call_command from zds.tutorial.factories import BigTutorialFactory, MiniTutorialFactory, PublishedMiniTutorial, NoteFactory, \ @@ -578,6 +578,26 @@ def test_generate_pdf(self): self.assertFalse(os.path.exists(pdf_path)) self.assertFalse(os.path.exists(pdf_path2)) # so no PDF is generated ! + def test_last_participation_is_old(self): + article = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") + newUser = ProfileFactory().user + reac = ContentReaction(author=self.user_author, position=1, related_content=article) + reac.update_content("I will find you. And I Will Kill you.") + reac.save() + article.last_note = reac + article.save() + + self.assertFalse(last_participation_is_old(article, newUser)) + ContentRead(user=self.user_author, note=reac, content=article).save() + reac = ContentReaction(author=newUser, position=2, related_content=article) + reac.update_content("I will find you. And I Will Kill you.") + reac.save() + article.last_note = reac + article.save() + ContentRead(user=newUser, note=reac, content=article).save() + self.assertFalse(last_participation_is_old(article, newUser)) + self.assertTrue(last_participation_is_old(article, self.user_author)) + 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 5c369fdf63..226e659ef5 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -131,6 +131,17 @@ def never_read(content, user=None): return True +def last_participation_is_old(content, user): + from zds.tutorialv2.models.models_database import ContentRead, ContentReaction + if user is None or not user.is_authenticated(): + return False + if ContentReaction.objects.filter(author__pk=user.pk, related_content__pk=content.pk).count() == 0: + return False + return ContentRead.objects\ + .filter(note__pk=content.last_note.pk, content__pk=content.pk, user__pk=user.pk)\ + .count() == 0 + + def mark_read(content, user=None): """Mark the last tutorial note as read for the user. diff --git a/zds/tutorialv2/views/views_published.py b/zds/tutorialv2/views/views_published.py index 346807dfd3..1f74ca6f3e 100644 --- a/zds/tutorialv2/views/views_published.py +++ b/zds/tutorialv2/views/views_published.py @@ -20,7 +20,7 @@ from zds.tutorialv2.mixins import SingleOnlineContentDetailViewMixin, SingleOnlineContentViewMixin, DownloadViewMixin, \ ContentTypeMixin, SingleOnlineContentFormViewMixin, MustRedirect from zds.tutorialv2.models.models_database import PublishableContent, PublishedContent, ContentReaction -from zds.tutorialv2.utils import search_container_or_404, never_read, mark_read +from zds.tutorialv2.utils import search_container_or_404, mark_read, last_participation_is_old from zds.utils.models import CommentDislike, CommentLike, SubCategory, Alert from zds.utils.mps import send_mp from zds.utils.paginator import make_pagination, ZdSPagingListView @@ -137,7 +137,7 @@ def get_context_data(self, **kwargs): context['isantispam'] = self.object.antispam() # handle reactions: - if never_read(self.object): + if last_participation_is_old(self.object, self.request.user): mark_read(self.object) return context From 1cfbfccaa5b87446e5d66709559e14dc2207d3e0 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 13:19:23 +0200 Subject: [PATCH 17/55] fix les vieux tests mal fichus. --- zds/tutorialv2/tests/tests_views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 28111fd8eb..fc3e9195bb 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4190,7 +4190,8 @@ def test_add_note(self): self.client.get(reverse("tutorial:view", args=[self.tuto.pk, self.tuto.slug])).status_code, 200) reads = ContentRead.objects.filter(user=self.user_staff).all() - self.assertEqual(len(reads), 1) + # simple visit does not trigger follow + self.assertEqual(len(reads), 0) self.assertEqual(reads[0].content.pk, self.tuto.pk) self.assertEqual(reads[0].note.pk, reactions[0].pk) @@ -4868,8 +4869,8 @@ def test_last_reactions(self): # visit tutorial and read the two notes: result = self.client.get(reverse('tutorial:view', kwargs={'pk': tuto.pk, 'slug': tuto.slug})) self.assertEqual(result.status_code, 200) - - self.assertEqual(ContentRead.objects.filter(user=self.user_staff).count(), 1) + # simple visit does not trigger follow + self.assertEqual(ContentRead.objects.filter(user=self.user_staff).count(), 0) tuto = PublishableContent.objects.get(pk=self.tuto.pk) self.assertEqual(tuto.last_read_note(), reactions[1]) # now reactions are read From e1a1b4455d8871aa747f157e9de4c5ba3e6be99e Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 14:57:16 +0200 Subject: [PATCH 18/55] fix test --- zds/tutorialv2/tests/tests_views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index fc3e9195bb..9d4d915383 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4192,8 +4192,6 @@ def test_add_note(self): reads = ContentRead.objects.filter(user=self.user_staff).all() # simple visit does not trigger follow self.assertEqual(len(reads), 0) - self.assertEqual(reads[0].content.pk, self.tuto.pk) - self.assertEqual(reads[0].note.pk, reactions[0].pk) # login with author self.assertEqual( @@ -4789,7 +4787,7 @@ def test_last_reactions(self): tuto = PublishableContent.objects.get(pk=self.tuto.pk) - reactions = ContentReaction.objects.filter(related_content=self.tuto).all() + reactions = list(ContentReaction.objects.filter(related_content=self.tuto).all()) self.assertEqual(len(reactions), 2) self.assertEqual(ContentRead.objects.filter(user=self.user_author).count(), 1) # reaction read @@ -4874,7 +4872,7 @@ def test_last_reactions(self): tuto = PublishableContent.objects.get(pk=self.tuto.pk) self.assertEqual(tuto.last_read_note(), reactions[1]) # now reactions are read - self.assertEqual(tuto.first_unread_note(), reactions[1]) + self.assertEqual(tuto.first_unread_note(), reactions[-1]) def tearDown(self): From 2f1bf7e634b688965224f1d296e3150cdba0cdb9 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 15:26:30 +0200 Subject: [PATCH 19/55] the test was quite incorrect --- zds/tutorialv2/tests/tests_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 9d4d915383..a59f3be524 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4871,7 +4871,7 @@ def test_last_reactions(self): self.assertEqual(ContentRead.objects.filter(user=self.user_staff).count(), 0) tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[1]) # now reactions are read + self.assertEqual(tuto.last_read_note(), tuto.last_note) # now reactions are read self.assertEqual(tuto.first_unread_note(), reactions[-1]) def tearDown(self): From 15a3e16f6caa5f77c155f270017e552be3cdd58f Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 19:50:01 +0200 Subject: [PATCH 20/55] =?UTF-8?q?test=20inutile=20car=20r=C3=A9p=C3=A9titi?= =?UTF-8?q?f?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index a59f3be524..13e44268af 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4848,32 +4848,6 @@ def test_last_reactions(self): result = self.client.get(reverse('zds.pages.views.index')) # go to whatever page self.assertEqual(result.status_code, 200) - # login with staff - self.assertEqual( - self.client.login( - username=self.user_staff.username, - password='hostel77'), - True) - - result = self.client.get(reverse('zds.pages.views.index')) # go to whatever page - self.assertEqual(result.status_code, 200) - - self.assertEqual(ContentRead.objects.filter(user=self.user_staff).count(), 0) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[0]) # if never read, last note=first note - self.assertEqual(tuto.first_unread_note(), reactions[0]) - - # visit tutorial and read the two notes: - result = self.client.get(reverse('tutorial:view', kwargs={'pk': tuto.pk, 'slug': tuto.slug})) - self.assertEqual(result.status_code, 200) - # simple visit does not trigger follow - self.assertEqual(ContentRead.objects.filter(user=self.user_staff).count(), 0) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), tuto.last_note) # now reactions are read - self.assertEqual(tuto.first_unread_note(), reactions[-1]) - def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): From cbc35942cf65219154ab912c8c57d1ae668357a6 Mon Sep 17 00:00:00 2001 From: artragis Date: Sat, 3 Oct 2015 09:02:48 +0200 Subject: [PATCH 21/55] =?UTF-8?q?fix=20#3012=20:=20erreur=20404=20si=20mes?= =?UTF-8?q?sage=20non=20trouv=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/views/views_published.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zds/tutorialv2/views/views_published.py b/zds/tutorialv2/views/views_published.py index 1f74ca6f3e..cfa27df6e7 100644 --- a/zds/tutorialv2/views/views_published.py +++ b/zds/tutorialv2/views/views_published.py @@ -445,6 +445,8 @@ def get_form_kwargs(self): .prefetch_related("author")\ .filter(pk=int(self.request.GET["message"]))\ .first() + if self.reaction is None: + raise Http404("Message asked and not found") kwargs['reaction'] = self.reaction else: raise Http404("The 'message' parameter must be a digit.") From ced82f78d6e3e1c39a5d64045b9d0b8b5a2cf8ec Mon Sep 17 00:00:00 2001 From: artragis Date: Sun, 4 Oct 2015 10:41:02 +0200 Subject: [PATCH 22/55] ajoute un test unitaire. --- zds/tutorialv2/tests/tests_views.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 13e44268af..9c99a17d4f 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4787,7 +4787,7 @@ def test_last_reactions(self): tuto = PublishableContent.objects.get(pk=self.tuto.pk) - reactions = list(ContentReaction.objects.filter(related_content=self.tuto).all()) + reactions = ContentReaction.objects.filter(related_content=self.tuto).all() self.assertEqual(len(reactions), 2) self.assertEqual(ContentRead.objects.filter(user=self.user_author).count(), 1) # reaction read @@ -4848,6 +4848,23 @@ def test_last_reactions(self): result = self.client.get(reverse('zds.pages.views.index')) # go to whatever page self.assertEqual(result.status_code, 200) + def test_note_with_bad_param(self): + self.assertEqual( + self.client.login( + username=self.user_staff.username, + password='hostel77'), + True) + url_template = reverse("content:update-reaction") + "?pk={}&message={}" + result = self.client.get(url_template.format(self.tuto.pk, 454545665895123)) + self.assertEqual(404, result.status_code) + reaction = ContentReaction(related_content=self.tuto, author=self.user_guest, position=1) + reaction.update_content("blah") + reaction.save() + self.tuto.last_note = reaction + self.tuto.save() + result = self.client.get(url_template.format(861489632, reaction.pk)) + self.assertEqual(404, result.status_code) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): From ad5e33d279b9996f6e5e3d257ed054eef3c03457 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 30 Sep 2015 15:39:40 +0200 Subject: [PATCH 23/55] =?UTF-8?q?v=C3=A9rifie=20la=20coh=C3=A9rence=20du?= =?UTF-8?q?=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/models/models_database.py | 7 +++-- zds/tutorialv2/tests/tests_utils.py | 40 +++++++++++++++++++++++- zds/tutorialv2/utils.py | 37 +++++++++++++++++----- zds/tutorialv2/views/views_contents.py | 3 +- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/zds/tutorialv2/models/models_database.py b/zds/tutorialv2/models/models_database.py index 2fd5a0816c..16b31407be 100644 --- a/zds/tutorialv2/models/models_database.py +++ b/zds/tutorialv2/models/models_database.py @@ -308,7 +308,7 @@ def load_version(self, sha=None, public=None): sha = self.sha_draft else: sha = self.sha_public - + max_title_length = PublishableContent._meta.get_field("title").max_length if public and isinstance(public, PublishedContent): # use the public (altered and not versioned) repository path = public.get_prod_path() slug = public.content_public_slug @@ -321,7 +321,8 @@ def load_version(self, sha=None, public=None): manifest = open(os.path.join(path, 'manifest.json'), 'r') json = json_reader.loads(manifest.read()) - versioned = get_content_from_json(json, public.sha_public, slug, public=True) + versioned = get_content_from_json(json, public.sha_public, + slug, public=True, max_title_len=max_title_length) else: # draft version, use the repository (slower, but allows manipulation) path = self.get_repo_path() @@ -338,7 +339,7 @@ def load_version(self, sha=None, public=None): raise BadManifestError( _(u'Une erreur est survenue lors de la lecture du manifest.json, est-ce du JSON ?')) - versioned = get_content_from_json(json, sha, self.slug) + versioned = get_content_from_json(json, sha, self.slug, max_title_len=max_title_length) self.insert_data_in_versioned(versioned) return versioned diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index 804db7ade4..b115b7f6df 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -15,8 +15,10 @@ from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, LicenceFactory, ExtractFactory, \ PublishedContentFactory from zds.gallery.factories import UserGalleryFactory +from zds.tutorialv2.models.models_versioned import Container from zds.tutorialv2.utils import get_target_tagged_tree_for_container, publish_content, unpublish_content, \ - get_target_tagged_tree_for_extract, retrieve_and_update_images_links, last_participation_is_old + get_target_tagged_tree_for_extract, retrieve_and_update_images_links, last_participation_is_old, \ + InvalidSlugError, BadManifestError, get_content_from_json from zds.tutorialv2.models.models_database import PublishableContent, PublishedContent, ContentReaction, ContentRead from django.core.management import call_command from zds.tutorial.factories import BigTutorialFactory, MiniTutorialFactory, PublishedMiniTutorial, NoteFactory, \ @@ -598,6 +600,42 @@ def test_last_participation_is_old(self): self.assertFalse(last_participation_is_old(article, newUser)) self.assertTrue(last_participation_is_old(article, self.user_author)) + def testParseBadManifest(self): + base_content = PublishableContentFactory(author_list=[self.user_author]) + versioned = base_content.load_version() + versioned.add_container(Container(u"un peu plus près de 42")) + versioned.dump_json() + manifest = os.path.join(versioned.get_path(), "manifest.json") + dictionary = json_reader.load(open(manifest)) + + old_title = dictionary['title'] + + # first bad title + dictionary['title'] = 81 * ['a'] + self.assertRaises(BadManifestError, + get_content_from_json, dictionary, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) + dictionary['title'] = "".join(dictionary['title']) + self.assertRaises(BadManifestError, + get_content_from_json, dictionary, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) + dictionary['title'] = '...' + self.assertRaises(InvalidSlugError, + get_content_from_json, dictionary, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) + + dictionary['title'] = old_title + dictionary['children'][0]['title'] = 81 * ['a'] + self.assertRaises(BadManifestError, + get_content_from_json, dictionary, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) + + dictionary['children'][0]['title'] = "bla" + dictionary['children'][0]['slug'] = "..." + self.assertRaises(InvalidSlugError, + get_content_from_json, dictionary, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) + 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 226e659ef5..035e97360f 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -31,6 +31,16 @@ from zds.utils.templatetags.emarkdown import emarkdown +def all_is_string_appart_from_children(dict_representation): + """check all keys are string appart from the children key + :param dict_representation: the json decoded dictionary + :type dict_representation: dict + :return: + :rtype: bool + """ + return all([isinstance(value, basestring) for key, value in dict_representation.items() if key != "children"]) + + def search_container_or_404(base_content, kwargs_array): """ :param base_content: the base Publishable content we will use to retrieve the container @@ -727,7 +737,7 @@ def __init__(self, *args, **kwargs): super(BadManifestError, self).__init__(*args, **kwargs) -def get_content_from_json(json, sha, slug_last_draft, public=False): +def get_content_from_json(json, sha, slug_last_draft, public=False, max_title_len=80): """Transform the JSON formated data into ``VersionedContent`` :param json: JSON data from a `manifest.json` file @@ -740,7 +750,14 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): from zds.tutorialv2.models.models_versioned import Container, Extract, VersionedContent, PublicContent if 'version' in json and json['version'] == 2: + json["version"] = "2" + if not all_is_string_appart_from_children(json): + json['version'] = 2 + raise BadManifestError("manifest is not well formated") + json['version'] = 2 # create and fill the container + if len(json['title']) > max_title_len: + raise BadManifestError("Title must be a string of less than {} chars".format(max_title_len)) slugify_raise_on_empty(json['title']) json_slug = slugify_raise_on_empty(json['slug']) if not public: @@ -771,6 +788,7 @@ def get_content_from_json(json, sha, slug_last_draft, public=False): fill_containers_from_json(json, versioned) else: # MINIMUM (!) fallback for version 1.0 + if "type" in json: if json['type'] == 'article': _type = 'ARTICLE' @@ -878,7 +896,9 @@ def slugify_raise_on_empty(title, use_old_slugify=False): :return: the slugified title :rtype: str """ - + slug = slugify(title) + if not isinstance(slug, basestring): + raise InvalidSlugError("slug is incorrect") if not use_old_slugify: slug = slugify(title) else: @@ -902,14 +922,16 @@ def fill_containers_from_json(json_sub, parent): from zds.tutorialv2.models.models_versioned import Container, Extract if 'children' in json_sub: + for child in json_sub['children']: + if not all_is_string_appart_from_children(child): + raise BadManifestError(u"Manifest is not well formed on container " + str(json_sub['title'])) if child['object'] == 'container': slug = '' try: slug = child['slug'] - if not check_slug(slug): - raise InvalidSlugError(slug) - except (ValueError, KeyError): + slugify_raise_on_empty(slug) + except KeyError: pass new_container = Container(child['title'], slug) if 'introduction' in child: @@ -926,9 +948,8 @@ def fill_containers_from_json(json_sub, parent): slug = '' try: slug = child['slug'] - if not check_slug(slug): - raise InvalidSlugError(slug) - except (ValueError, KeyError): + slugify_raise_on_empty(slug) + except KeyError: pass new_extract = Extract(child['title'], slug) diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 8277c192fc..f7bd3196bf 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -504,7 +504,8 @@ def extract_content_from_zip(zip_archive): _(u'Une erreur est survenue durant la lecture du manifest, ' u'vérifiez qu\'il s\'agit de JSON correctement formaté')) try: - versioned = get_content_from_json(json_, None, '') + versioned = get_content_from_json(json_, None, '', + max_title_len=PublishableContent._meta.get_field('title').max_length) except BadManifestError as e: raise BadArchiveError(e.message) except InvalidSlugError: From 128e31976a12b0fc848e413d3b7f9c00800b266a Mon Sep 17 00:00:00 2001 From: artragis Date: Tue, 29 Sep 2015 20:47:02 +0200 Subject: [PATCH 24/55] =?UTF-8?q?emp=C3=AAche=20d'=C3=A9diter=20un=20messa?= =?UTF-8?q?ge=20qui=20ne=20nous=20appartient=20pas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parce que c'est pas poli --- zds/tutorialv2/views/views_published.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/views/views_published.py b/zds/tutorialv2/views/views_published.py index cfa27df6e7..9fbfdc3ee4 100644 --- a/zds/tutorialv2/views/views_published.py +++ b/zds/tutorialv2/views/views_published.py @@ -445,8 +445,11 @@ def get_form_kwargs(self): .prefetch_related("author")\ .filter(pk=int(self.request.GET["message"]))\ .first() - if self.reaction is None: - raise Http404("Message asked and not found") + if not self.reaction: + raise Http404("Not such a reaction : " + self.request.GET["message"]) + if self.reaction.author.pk != self.request.user.pk and not self.is_staff: + raise PermissionDenied() + kwargs['reaction'] = self.reaction else: raise Http404("The 'message' parameter must be a digit.") From 54dd8ab54f5fb123bb145df4b7a0928e97a71eaf Mon Sep 17 00:00:00 2001 From: artragis Date: Fri, 2 Oct 2015 13:22:46 +0200 Subject: [PATCH 25/55] ajoute un TU --- zds/tutorialv2/tests/tests_views.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index 9c99a17d4f..fcdad08ee6 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4787,7 +4787,7 @@ def test_last_reactions(self): tuto = PublishableContent.objects.get(pk=self.tuto.pk) - reactions = ContentReaction.objects.filter(related_content=self.tuto).all() + reactions = list(ContentReaction.objects.filter(related_content=self.tuto).all()) self.assertEqual(len(reactions), 2) self.assertEqual(ContentRead.objects.filter(user=self.user_author).count(), 1) # reaction read @@ -4865,6 +4865,29 @@ def test_note_with_bad_param(self): result = self.client.get(url_template.format(861489632, reaction.pk)) self.assertEqual(404, result.status_code) + def test_cant_edit_not_owned_note(self): + article = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") + newUser = ProfileFactory().user + newReaction = ContentReaction(related_content=article, position=1) + newReaction.update_content("I will find you. And I will Kill you.") + newReaction.author = self.user_guest + + newReaction.save() + self.assertEqual( + self.client.login( + username=newUser.username, + password='hostel77'), + True) + resp = self.client.get( + reverse('content:update-reaction') + "?message={}&pk={}".format(newReaction.pk, article.pk)) + self.assertEqual(403, resp.status_code) + resp = self.client.post( + reverse('content:update-reaction') + "?message={}&pk={}".format(newReaction.pk, article.pk), + { + 'text': "I edited it" + }) + self.assertEqual(403, resp.status_code) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): From 33d8bf876f8a12123f07464bb8a394552c1549ff Mon Sep 17 00:00:00 2001 From: artragis Date: Mon, 5 Oct 2015 19:54:03 +0200 Subject: [PATCH 26/55] =?UTF-8?q?emp=C3=AAche=20d'acc=C3=A8der=20au=20cont?= =?UTF-8?q?enu=20priv=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit même quand le sha_private == sha_public --- zds/tutorialv2/mixins.py | 3 ++- zds/tutorialv2/tests/tests_views.py | 10 ++++++++++ zds/tutorialv2/urls/urls_contents.py | 11 ++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index 29cbe7ab93..4c64918bfa 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -51,6 +51,7 @@ class SingleContentViewMixin(object): is_author = False only_draft_version = True must_redirect = False + public_is_prioritary = True def get_object(self, queryset=None): """ Get database representation of the content by its `pk`, then check permissions @@ -112,7 +113,7 @@ def get_versioned_object(self): # if beta or public version, user can also access to it is_beta = self.object.is_beta(self.sha) - is_public = self.object.is_public(self.sha) + is_public = self.object.is_public(self.sha) and self.public_is_prioritary if not is_beta and not is_public and not self.is_author: if not self.is_staff or (not self.authorized_for_staff and self.must_be_author): diff --git a/zds/tutorialv2/tests/tests_views.py b/zds/tutorialv2/tests/tests_views.py index fcdad08ee6..3d3e9ac50d 100644 --- a/zds/tutorialv2/tests/tests_views.py +++ b/zds/tutorialv2/tests/tests_views.py @@ -4888,6 +4888,16 @@ def test_cant_edit_not_owned_note(self): }) self.assertEqual(403, resp.status_code) + def test_cant_view_private_even_if_draft_is_equal_to_public(self): + content = PublishedContentFactory(author_list=[self.user_author]) + self.assertEqual( + self.client.login( + username=self.user_guest.username, + password='hostel77'), + True) + resp = self.client.get(reverse("content:view", args=[content.pk, content.slug])) + self.assertEqual(403, resp.status_code) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['content']['repo_private_path']): diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index 5763f59646..3a29c6bd91 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -21,23 +21,24 @@ url(r'^aides/$', ContentsWithHelps.as_view(), name='helps'), url(r'^(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', - DisplayContainer.as_view(), + DisplayContainer.as_view(public_is_prioritary=False), name='view-container'), url(r'^(?P\d+)/(?P.+)/(?P.+)/$', - DisplayContainer.as_view(), + DisplayContainer.as_view(public_is_prioritary=False), name='view-container'), - url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(), name='view'), + url(r'^(?P\d+)/(?P.+)/$', DisplayContent.as_view(public_is_prioritary=False), + name='view'), url(r'^telecharger/(?P\d+)/(?P.+)/$', DownloadContent.as_view(), name='download-zip'), # beta: url(r'^beta/(?P\d+)/(?P.+)/(?P.+)/(?P.+)/$', - DisplayBetaContainer.as_view(), + DisplayBetaContainer.as_view(public_is_prioritary=False), name='beta-view-container'), url(r'^beta/(?P\d+)/(?P.+)/(?P.+)/$', - DisplayBetaContainer.as_view(), + DisplayBetaContainer.as_view(public_is_prioritary=False), name='beta-view-container'), url(r'^beta/(?P\d+)/(?P.+)/$', DisplayBetaContent.as_view(), name='beta-view'), From 98deca074e1052ba52f6b1ae0260dd0d0c1ab532 Mon Sep 17 00:00:00 2001 From: artragis Date: Wed, 30 Sep 2015 20:12:38 +0200 Subject: [PATCH 27/55] =?UTF-8?q?corrige=20l'affichage=20du=20titre=20des?= =?UTF-8?q?=20articles=20publi=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../includes/content_item_type_article.part.html | 12 ++++++++++-- .../includes/content_item_type_tutoriel.part.html | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/templates/tutorialv2/includes/content_item_type_article.part.html b/templates/tutorialv2/includes/content_item_type_article.part.html index c4828a2eb6..57675114cc 100644 --- a/templates/tutorialv2/includes/content_item_type_article.part.html +++ b/templates/tutorialv2/includes/content_item_type_article.part.html @@ -19,6 +19,14 @@ {% endif %} {% endcaptureas %} +{% captureas content_title %} + {% if public_article and online %} + {{ public_article.title }} + {% else %} + {{ article.title }} + {% endif %} +{% endcaptureas %} + {# Authors (by X, Y and Z) ; can't have multiple whitespaces because of the title ! #} {% captureas authors_text %} {% for author in article.authors.all %}{% if forloop.first %}{% trans "par" %}{% elif forloop.last %} {% trans "et" %}{% else %},{% endif %} {% if author == user %}{% trans "vous" %}{% else %}{{ author.username }}{% endif %}{% endfor %} @@ -32,9 +40,9 @@