diff --git a/templates/article/includes/warn_typo.part.html b/templates/article/includes/warn_typo.part.html new file mode 100644 index 0000000000..bee432cd1f --- /dev/null +++ b/templates/article/includes/warn_typo.part.html @@ -0,0 +1,33 @@ +{% load i18n %} + +{% if user.is_authenticated and on_line %} + + {% if user not in authors.all and authors_reachable|length >= 1 %} + + + {% trans "Signaler une faute dans l'article" %} + + + {% endif %} +{% endif %} \ No newline at end of file diff --git a/templates/article/view.html b/templates/article/view.html index 0e3a151c26..e6571395bd 100644 --- a/templates/article/view.html +++ b/templates/article/view.html @@ -113,8 +113,10 @@

{% block content %} {{ article.txt|safe }} - {% include "article/includes/pager.part.html" %} + + {% include "article/includes/warn_typo.part.html" with article=article authors=authors%} + {% endblock %} diff --git a/zds/article/tests/tests.py b/zds/article/tests/tests.py index ddb9ffae13..9310e33494 100644 --- a/zds/article/tests/tests.py +++ b/zds/article/tests/tests.py @@ -6,6 +6,9 @@ import datetime from zds.gallery.factories import ImageFactory, UserGalleryFactory, GalleryFactory +from django.contrib import messages +from django.contrib.auth.models import Group + try: import ujson as json_reader except: @@ -38,7 +41,6 @@ class ArticleTests(TestCase): def setUp(self): - settings.EMAIL_BACKEND = \ 'django.core.mail.backends.locmem.EmailBackend' self.mas = ProfileFactory().user @@ -102,6 +104,9 @@ def setUp(self): self.assertEquals(len(mail.outbox), 1) mail.outbox = [] + bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + bot.save() + def test_delete_image_on_change(self): """test que l'image est bien supprimée quand on la change""" @@ -878,6 +883,115 @@ def test_new_article(self): self.assertEqual(result.status_code, 200) self.assertEqual(Article.objects.filter(title='Create a new article test').count(), 1) + def test_warn_typo(self): + """ + Add a non-regression test about warning the author(s) of a typo in an article + """ + + typo_text = u'T\'as fait une faute, t\'es nul' + + # login with author + self.assertEqual( + self.client.login( + username=self.user_author.username, + password='hostel77'), + True) + + # check if author get error when warning typo on its own tutorial + result = self.client.post( + reverse('zds.article.views.warn_typo', args=[self.article.pk]), + { + 'explication': u'ceci est un test', + }, + follow=True) + self.assertEqual(result.status_code, 200) + + msgs = result.context['messages'] + last = None + for msg in msgs: + last = msg + self.assertEqual(last.level, messages.ERROR) + + # login with normal user + self.client.logout() + + self.assertEqual( + self.client.login( + username=self.user.username, + password='hostel77'), + True) + + # check if user can warn typo in tutorial + result = self.client.post( + reverse('zds.article.views.warn_typo', args=[self.article.pk]), + { + 'explication': typo_text, + }, + follow=True) + self.assertEqual(result.status_code, 200) + + msgs = result.context['messages'] + last = None + for msg in msgs: + last = msg + self.assertEqual(last.level, messages.SUCCESS) + + # check PM : + sent_pm = PrivateTopic.objects.filter(author=self.user.pk).last() + self.assertIn(self.user_author, sent_pm.participants.all()) # author is in participants + self.assertIn(typo_text, sent_pm.last_message.text) # typo is in message + self.assertIn(self.article.get_absolute_url_online(), sent_pm.last_message.text) # public url is in message + + # Check if we send a wrong pk key + result = self.client.post( + reverse('zds.article.views.warn_typo', args=["1111"]), + { + 'explication': typo_text, + }, + follow=False) + self.assertEqual(result.status_code, 404) + + # Check if we send no explanation + result = self.client.post( + reverse('zds.article.views.warn_typo', args=[self.article.pk]), + { + 'explication': '', + }, + follow=True) + self.assertEqual(result.status_code, 200) + + msgs = result.context['messages'] + last = None + for msg in msgs: + last = msg + self.assertEqual(last.level, messages.ERROR) + + # Check if we send an explanation with only space + result = self.client.post( + reverse('zds.article.views.warn_typo', args=[self.article.pk]), + { + 'explication': ' ', + }, + follow=True) + self.assertEqual(result.status_code, 200) + + msgs = result.context['messages'] + last = None + for msg in msgs: + last = msg + self.assertEqual(last.level, messages.ERROR) + + # Check if a guest can not warn the author + self.client.logout() + + result = self.client.post( + reverse('zds.article.views.warn_typo', args=[self.article.pk]), + { + 'explication': typo_text, + }, + follow=False) + self.assertEqual(result.status_code, 302) + def tearDown(self): if os.path.isdir(settings.ZDS_APP['article']['repo_path']): shutil.rmtree(settings.ZDS_APP['article']['repo_path']) diff --git a/zds/article/urls.py b/zds/article/urls.py index 0797113cef..c93150237d 100644 --- a/zds/article/urls.py +++ b/zds/article/urls.py @@ -56,4 +56,6 @@ 'zds.article.views.like_reaction'), url(r'^message/dislike/$', 'zds.article.views.dislike_reaction'), + url(r'^message/typo/article/(?P\d+)/$', + 'zds.article.views.warn_typo'), ) diff --git a/zds/article/views.py b/zds/article/views.py index f1b5a0604d..d01bb572c2 100644 --- a/zds/article/views.py +++ b/zds/article/views.py @@ -2,6 +2,8 @@ from datetime import datetime from operator import attrgetter +from zds.member.models import Profile + try: import ujson as json_reader except ImportError: @@ -130,6 +132,7 @@ def view(request, article_pk, article_slug): 'validation': validation, 'is_js': is_js, 'formJs': form_js, + 'on_line': False }) @@ -165,6 +168,12 @@ def view_online(request, article_pk, article_slug): .order_by('position')\ .all() + # Check if the author is reachable + authors_reachable_request = Profile.objects.contactable_members().filter(user__in=article.authors.all()) + authors_reachable = [] + for author in authors_reachable_request: + authors_reachable.append(author.user) + # Retrieve pk of the last reaction. If there aren't reactions # for the article, we initialize this last reaction at 0. last_reaction_pk = 0 @@ -211,10 +220,79 @@ def view_online(request, article_pk, article_slug): 'pages': paginator_range(page_nbr, paginator.num_pages), 'nb': page_nbr, 'last_reaction_pk': last_reaction_pk, - 'form': form + 'form': form, + 'on_line': True, + 'authors_reachable': authors_reachable }) +@login_required +@require_POST +def warn_typo(request, article_pk): + """Warn author(s) about a mistake in its (their) article by sending him/her (them) a private message.""" + + # Need profile + profile = get_object_or_404(Profile, user=request.user) + + # Get article + try: + article_pk = int(article_pk) + except (KeyError, ValueError): + raise Http404 + + article = get_object_or_404(Article, pk=article_pk) + + # Check if the article is published + if article.sha_public is None: + raise Http404 + + # Check if authors are reachable + authors_reachable = Profile.objects.contactable_members().filter(user__in=article.authors.all()) + authors = [] + for author in authors_reachable: + authors.append(author.user) + + if len(authors) == 0: + if article.authors.count() > 1: + messages.error(request, _(u"Les auteurs de l'article sont malheureusement injoignables")) + else: + messages.error(request, _(u"L'auteur de l'article est malheureusement injoignable")) + else: + # Fetch explanation + if 'explication' not in request.POST or not request.POST['explication'].strip(): + messages.error(request, _(u'Votre proposition de correction est vide')) + else: + explanation = request.POST['explication'] + explanation = '\n'.join(['> ' + line for line in explanation.split('\n')]) + + # Is the user trying to send PM to himself ? + if request.user in article.authors.all(): + messages.error(request, _(u'Impossible d\'envoyer la correction car vous êtes l\'auteur ' + u'de cet article !')) + else: + # Create message : + msg = _(u'[{}]({}) souhaite vous proposer une correction pour votre article [{}]({}).\n\n').format( + request.user.username, + settings.ZDS_APP['site']['url'] + profile.get_absolute_url(), + article.title, + settings.ZDS_APP['site']['url'] + article.get_absolute_url_online() + ) + + msg += _(u'Voici son message :\n\n{}').format(explanation) + + # Send it + send_mp(request.user, + article.authors.all(), + _(u"Proposition de correction"), + article.title, + msg, + leave=False) + messages.success(request, _(u'Votre correction a bien été proposée !')) + + # return to page : + return redirect(article.get_absolute_url_online()) + + @can_write_and_read_now @login_required def new(request): diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 883f752e51..7beb8eea36 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -55,6 +55,9 @@ def setUp(self): position_in_category=1) self.staff = StaffProfileFactory().user + bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + bot.save() + def test_list_members(self): """ To test the listing of the members with and without page parameter.