From e786f7369d8a632dd3295b96d4c52ef2ba3a37c2 Mon Sep 17 00:00:00 2001
From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com>
Date: Sun, 20 Feb 2022 08:47:16 +0100
Subject: [PATCH 1/3] Scinde des gros fichiers du module member
---
zds/member/tests/tests_views.py | 1820 ----------------
zds/member/tests/views/__init__.py | 0
zds/member/tests/views/tests_admin.py | 117 +
.../tests/views/tests_emailproviders.py | 153 ++
zds/member/tests/views/tests_hats.py | 312 +++
zds/member/tests/views/tests_login.py | 80 +
zds/member/tests/views/tests_moderation.py | 518 +++++
.../tests/views/tests_password_recovery.py | 52 +
zds/member/tests/views/tests_profile.py | 322 +++
zds/member/tests/views/tests_register.py | 392 ++++
zds/member/tests/views/tests_reports.py | 53 +
zds/member/urls.py | 53 +-
zds/member/views.py | 1542 -------------
zds/member/views/__init__.py | 29 +
zds/member/views/admin.py | 89 +
zds/member/views/emailproviders.py | 110 +
zds/member/views/hats.py | 212 ++
zds/member/views/login.py | 117 +
zds/member/views/moderation.py | 181 ++
zds/member/views/password_recovery.py | 91 +
zds/member/views/profile.py | 441 ++++
zds/member/views/register.py | 331 +++
zds/member/views/reports.py | 50 +
.../tests/tests_views/tests_content.py | 1923 -----------------
.../tests/tests_views/tests_published.py | 42 +-
zds/urls.py | 2 +-
26 files changed, 3701 insertions(+), 5331 deletions(-)
delete mode 100644 zds/member/tests/tests_views.py
create mode 100644 zds/member/tests/views/__init__.py
create mode 100644 zds/member/tests/views/tests_admin.py
create mode 100644 zds/member/tests/views/tests_emailproviders.py
create mode 100644 zds/member/tests/views/tests_hats.py
create mode 100644 zds/member/tests/views/tests_login.py
create mode 100644 zds/member/tests/views/tests_moderation.py
create mode 100644 zds/member/tests/views/tests_password_recovery.py
create mode 100644 zds/member/tests/views/tests_profile.py
create mode 100644 zds/member/tests/views/tests_register.py
create mode 100644 zds/member/tests/views/tests_reports.py
delete mode 100644 zds/member/views.py
create mode 100644 zds/member/views/__init__.py
create mode 100644 zds/member/views/admin.py
create mode 100644 zds/member/views/emailproviders.py
create mode 100644 zds/member/views/hats.py
create mode 100644 zds/member/views/login.py
create mode 100644 zds/member/views/moderation.py
create mode 100644 zds/member/views/password_recovery.py
create mode 100644 zds/member/views/profile.py
create mode 100644 zds/member/views/register.py
create mode 100644 zds/member/views/reports.py
diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py
deleted file mode 100644
index 07f8d2a227..0000000000
--- a/zds/member/tests/tests_views.py
+++ /dev/null
@@ -1,1820 +0,0 @@
-import os
-from datetime import datetime
-from smtplib import SMTPException
-
-from django.core.mail.backends.base import BaseEmailBackend
-from unittest.mock import Mock
-from oauth2_provider.models import AccessToken, Application
-
-from django.conf import settings
-from django.contrib.auth.models import User, Group
-from django.core import mail
-from django.urls import reverse
-from django.test import TestCase, override_settings
-from django.utils.html import escape
-from django.utils.translation import gettext_lazy as _
-
-from zds.member.views import member_from_ip
-from zds.notification.models import TopicAnswerSubscription
-from zds.member.tests.factories import (
- ProfileFactory,
- StaffProfileFactory,
- NonAsciiProfileFactory,
- UserFactory,
- DevProfileFactory,
-)
-from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory
-from zds.member.models import Profile, KarmaNote, TokenForgotPassword
-from zds.mp.models import PrivatePost, PrivateTopic
-from zds.member.models import TokenRegister, Ban, NewEmailProvider, BannedEmailProvider
-from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory
-from zds.tutorialv2.models.database import PublishableContent, PublishedContent
-from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
-from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory
-from zds.forum.models import Topic, Post
-from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory
-from zds.gallery.models import Gallery, UserGallery
-from zds.pages.models import GroupContact
-from zds.utils.models import CommentVote, Hat, HatRequest, Alert
-
-
-@override_for_contents()
-class MemberTests(TutorialTestMixin, TestCase):
- def setUp(self):
- settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
- self.mas = ProfileFactory()
- settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
- self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
- self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
- self.category1 = ForumCategoryFactory(position=1)
- self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
- self.staff = StaffProfileFactory().user
-
- self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
- self.bot.save()
-
- def test_karma(self):
- user = ProfileFactory()
- other_user = ProfileFactory()
- self.client.force_login(other_user.user)
- r = self.client.post(reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"})
- self.assertEqual(403, r.status_code)
- self.client.logout()
- self.client.force_login(self.staff)
- # bad id
- r = self.client.post(
- reverse("member-modify-karma"), {"profile_pk": "blah", "karma": 42, "note": "warn"}, follow=True
- )
- self.assertEqual(404, r.status_code)
- # good karma
- r = self.client.post(
- reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}, follow=True
- )
- self.assertEqual(200, r.status_code)
- self.assertIn("{} : 42".format(_("Modification du karma")), r.content.decode("utf-8"))
- # more than 100 karma must unvalidate the karma
- r = self.client.post(
- reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 420, "note": "warn"}, follow=True
- )
- self.assertEqual(200, r.status_code)
- self.assertNotIn("{} : 420".format(_("Modification du karma")), r.content.decode("utf-8"))
- # empty warning must unvalidate the karma
- r = self.client.post(
- reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 41, "note": ""}, follow=True
- )
- self.assertEqual(200, r.status_code)
- self.assertNotIn("{} : 41".format(_("Modification du karma")), r.content.decode("utf-8"))
-
- def test_list_members(self):
- """
- To test the listing of the members with and without page parameter.
- """
-
- # create strange member
- weird = ProfileFactory()
- weird.user.username = "ïtrema718"
- weird.user.email = "foo@\xfbgmail.com"
- weird.user.save()
-
- # list of members.
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
-
- nb_users = len(result.context["members"])
-
- # Test that inactive user don't show up
- unactive_user = ProfileFactory()
- unactive_user.user.is_active = False
- unactive_user.user.save()
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users, len(result.context["members"]))
-
- # Add a Bot and check that list didn't change
- bot_profile = ProfileFactory()
- bot_profile.user.groups.add(self.bot)
- bot_profile.user.save()
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users, len(result.context["members"]))
-
- # list of members with page parameter.
- result = self.client.get(reverse("member-list") + "?page=1", follow=False)
- self.assertEqual(result.status_code, 200)
-
- # page which doesn't exist.
- result = self.client.get(reverse("member-list") + "?page=1534", follow=False)
- self.assertEqual(result.status_code, 404)
-
- # page parameter isn't an integer.
- result = self.client.get(reverse("member-list") + "?page=abcd", follow=False)
- self.assertEqual(result.status_code, 404)
-
- def test_details_member(self):
- """
- To test details of a member given.
- """
-
- # details of a staff user.
- result = self.client.get(reverse("member-detail", args=[self.staff.username]), follow=False)
- self.assertEqual(result.status_code, 200)
-
- # details of an unknown user.
- result = self.client.get(reverse("member-detail", args=["unknown_user"]), follow=False)
- self.assertEqual(result.status_code, 404)
-
- def test_redirection_when_using_old_detail_member_url(self):
- """
- To test the redirection when accessing the member profile through the old url
- """
- user = ProfileFactory().user
- result = self.client.get(reverse("member-detail-redirect", args=[user.username]), follow=False)
-
- self.assertEqual(result.status_code, 301)
-
- def test_old_detail_member_url_with_unexistant_member(self):
- """
- To test wether a 404 error is raised when the user in the old url does not exist
- """
- response = self.client.get(reverse("member-detail-redirect", args=["tartempion"]), follow=False)
-
- self.assertEqual(response.status_code, 404)
-
- def test_moderation_history(self):
- user = ProfileFactory().user
-
- ban = Ban(
- user=user,
- moderator=self.staff,
- type="Lecture Seule Temporaire",
- note="Test de LS",
- pubdate=datetime.now(),
- )
- ban.save()
-
- note = KarmaNote(
- user=user,
- moderator=self.staff,
- karma=5,
- note="Test de karma",
- pubdate=datetime.now(),
- )
- note.save()
-
- # staff rights are required to view the history, check that
- self.client.logout()
- self.client.force_login(user)
- result = self.client.get(user.profile.get_absolute_url(), follow=False)
- self.assertNotContains(result, "Historique de modération")
-
- self.client.logout()
- self.client.force_login(self.staff)
- result = self.client.get(user.profile.get_absolute_url(), follow=False)
- self.assertContains(result, "Historique de modération")
-
- # check that the note and the sanction are in the context
- self.assertIn(ban, result.context["actions"])
- self.assertIn(note, result.context["actions"])
-
- # and are displayed
- self.assertContains(result, "Test de LS")
- self.assertContains(result, "Test de karma")
-
- def test_profile_page_of_weird_member_username(self):
-
- # create some user with weird username
- user_1 = ProfileFactory()
- user_2 = ProfileFactory()
- user_3 = ProfileFactory()
- user_1.user.username = "ïtrema"
- user_1.user.save()
- user_2.user.username = ""a"
- user_2.user.save()
- user_3.user.username = "_`_`_`_"
- user_3.user.save()
-
- # profile pages of weird users.
- result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=True)
- self.assertEqual(result.status_code, 200)
- result = self.client.get(reverse("member-detail", args=[user_2.user.username]), follow=True)
- self.assertEqual(result.status_code, 200)
- result = self.client.get(reverse("member-detail", args=[user_3.user.username]), follow=True)
- self.assertEqual(result.status_code, 200)
-
- def test_modify_member(self):
- user = ProfileFactory().user
-
- # we need staff right for update other profile, so a member who is not staff can't access to the page
- self.client.logout()
- self.client.force_login(user)
-
- result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False)
- self.assertEqual(result.status_code, 403)
-
- self.client.logout()
- self.client.force_login(self.staff)
-
- # an inexistant member return 404
- result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False)
- self.assertEqual(result.status_code, 404)
-
- # an existant member return 200
- result = self.client.get(reverse("member-settings-mini-profile", args=[self.mas.user.username]), follow=False)
- self.assertEqual(result.status_code, 200)
-
- def test_success_preview_biography(self):
-
- member = ProfileFactory()
- self.client.force_login(member.user)
-
- response = self.client.post(
- reverse("update-member"),
- {
- "text": "It is **my** life",
- "preview": "",
- },
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
-
- result_string = "".join(a.decode() for a in response.streaming_content)
- self.assertIn("my", result_string, "We need the biography to be properly formatted")
-
- def test_login(self):
- """
- To test user login.
- """
- user = ProfileFactory()
-
- # login a user. Good password then redirection to the homepage.
- result = self.client.post(
- reverse("member-login"),
- {"username": user.user.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- self.assertRedirects(result, reverse("homepage"))
-
- # login failed with bad password then no redirection
- # (status_code equals 200 and not 302).
- result = self.client.post(
- reverse("member-login"),
- {"username": user.user.username, "password": "hostel", "remember": "remember"},
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
- self.assertContains(
- result,
- _(
- "Le mot de passe saisi est incorrect. "
- "Cliquez sur le lien « Mot de passe oublié ? » "
- "si vous ne vous en souvenez plus."
- ),
- )
-
- # login failed with bad username then no redirection
- # (status_code equals 200 and not 302).
- result = self.client.post(
- reverse("member-login"), {"username": "clem", "password": "hostel77", "remember": "remember"}, follow=False
- )
- self.assertEqual(result.status_code, 200)
- self.assertContains(
- result,
- _("Ce nom d’utilisateur est inconnu. " "Si vous ne possédez pas de compte, " "vous pouvez vous inscrire."),
- )
-
- # login a user. Good password and next parameter then
- # redirection to the "next" page.
- result = self.client.post(
- reverse("member-login") + "?next=" + reverse("gallery-list"),
- {"username": user.user.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- self.assertRedirects(result, reverse("gallery-list"))
-
- # check the user is redirected to the home page if
- # the "next" parameter points to a non-existing page.
- result = self.client.post(
- reverse("member-login") + "?next=/foobar",
- {"username": user.user.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- self.assertRedirects(result, reverse("homepage"))
-
- # check if the login form will redirect if there is
- # a next parameter.
- self.client.logout()
- result = self.client.get(reverse("member-login") + "?next=" + reverse("gallery-list"))
- self.assertContains(result, reverse("member-login") + "?next=" + reverse("gallery-list"), count=1)
-
- def test_register_with_not_allowed_chars(self):
- """
- Test register account with not allowed chars
- :return:
- """
- users = [
- # empty username
- {"username": "", "password": "flavour", "password_confirm": "flavour", "email": "firm1@zestedesavoir.com"},
- # space after username
- {
- "username": "firm1 ",
- "password": "flavour",
- "password_confirm": "flavour",
- "email": "firm1@zestedesavoir.com",
- },
- # space before username
- {
- "username": " firm1",
- "password": "flavour",
- "password_confirm": "flavour",
- "email": "firm1@zestedesavoir.com",
- },
- # username with utf8mb4 chars
- {
- "username": " firm1",
- "password": "flavour",
- "password_confirm": "flavour",
- "email": "firm1@zestedesavoir.com",
- },
- ]
-
- for user in users:
- result = self.client.post(reverse("register-member"), user, follow=False)
- self.assertEqual(result.status_code, 200)
- # check any email has been sent.
- self.assertEqual(len(mail.outbox), 0)
- # user doesn't exist
- self.assertEqual(User.objects.filter(username=user["username"]).count(), 0)
-
- def test_register(self):
- """
- To test user registration.
- """
-
- # register a new user.
- result = self.client.post(
- reverse("register-member"),
- {
- "username": "firm1",
- "password": "flavour",
- "password_confirm": "flavour",
- "email": "firm1@zestedesavoir.com",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
-
- # check email has been sent.
- self.assertEqual(len(mail.outbox), 1)
-
- # check if the new user is well inactive.
- user = User.objects.get(username="firm1")
- self.assertFalse(user.is_active)
-
- # make a request on the link which has been sent in mail to
- # confirm the registration.
- token = TokenRegister.objects.get(user=user)
- result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False)
- self.assertEqual(result.status_code, 200)
-
- # check a new email hasn't been sent at the new user.
- self.assertEqual(len(mail.outbox), 1)
-
- # check if the new user is active.
- self.assertTrue(User.objects.get(username="firm1").is_active)
-
- def test_unregister(self):
- """
- To test that unregistering user is working.
- """
-
- # test not logged user can't unregister.
- self.client.logout()
- result = self.client.post(reverse("member-unregister"), follow=False)
- self.assertEqual(result.status_code, 302)
-
- # test logged user can register.
- user = ProfileFactory()
- self.client.force_login(user.user)
- result = self.client.post(reverse("member-unregister"), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertEqual(User.objects.filter(username=user.user.username).count(), 0)
-
- # Attach a user at tutorials, articles, topics and private topics. After that,
- # unregister this user and check that he is well removed in all contents.
- user = ProfileFactory()
- user2 = ProfileFactory()
- alone_gallery = GalleryFactory()
- UserGalleryFactory(gallery=alone_gallery, user=user.user)
- shared_gallery = GalleryFactory()
- UserGalleryFactory(gallery=shared_gallery, user=user.user)
- UserGalleryFactory(gallery=shared_gallery, user=user2.user)
- # first case : a published tutorial with only one author
- published_tutorial_alone = PublishedContentFactory(type="TUTORIAL")
- published_tutorial_alone.authors.add(user.user)
- published_tutorial_alone.save()
- # second case : a published tutorial with two authors
- published_tutorial_2 = PublishedContentFactory(type="TUTORIAL")
- published_tutorial_2.authors.add(user.user)
- published_tutorial_2.authors.add(user2.user)
- published_tutorial_2.save()
- # third case : a private tutorial with only one author
- writing_tutorial_alone = PublishableContentFactory(type="TUTORIAL")
- writing_tutorial_alone.authors.add(user.user)
- writing_tutorial_alone.save()
- writing_tutorial_alone_galler_path = writing_tutorial_alone.gallery.get_gallery_path()
- # fourth case : a private tutorial with at least two authors
- writing_tutorial_2 = PublishableContentFactory(type="TUTORIAL")
- writing_tutorial_2.authors.add(user.user)
- writing_tutorial_2.authors.add(user2.user)
- writing_tutorial_2.save()
- self.client.force_login(self.staff)
- # same thing for articles
- published_article_alone = PublishedContentFactory(type="ARTICLE")
- published_article_alone.authors.add(user.user)
- published_article_alone.save()
- published_article_2 = PublishedContentFactory(type="ARTICLE")
- published_article_2.authors.add(user.user)
- published_article_2.authors.add(user2.user)
- published_article_2.save()
- writing_article_alone = PublishableContentFactory(type="ARTICLE")
- writing_article_alone.authors.add(user.user)
- writing_article_alone.save()
- writing_article_2 = PublishableContentFactory(type="ARTICLE")
- writing_article_2.authors.add(user.user)
- writing_article_2.authors.add(user2.user)
- writing_article_2.save()
- # beta content
- beta_forum = ForumFactory(category=ForumCategoryFactory())
- beta_content = BetaContentFactory(author_list=[user.user], forum=beta_forum)
- beta_content_2 = BetaContentFactory(author_list=[user.user, user2.user], forum=beta_forum)
- # about posts and topics
- authored_topic = TopicFactory(author=user.user, forum=self.forum11, solved_by=user.user)
- answered_topic = TopicFactory(author=user2.user, forum=self.forum11)
- PostFactory(topic=answered_topic, author=user.user, position=2)
- edited_answer = PostFactory(topic=answered_topic, author=user.user, position=3)
- edited_answer.editor = user.user
- edited_answer.save()
-
- upvoted_answer = PostFactory(topic=answered_topic, author=user2.user, position=4)
- upvoted_answer.like += 1
- upvoted_answer.save()
- CommentVote.objects.create(user=user.user, comment=upvoted_answer, positive=True)
-
- private_topic = PrivateTopicFactory(author=user.user)
- private_topic.participants.add(user2.user)
- private_topic.save()
- PrivatePostFactory(author=user.user, privatetopic=private_topic, position_in_topic=1)
-
- # add API key
- self.assertEqual(Application.objects.count(), 0)
- self.assertEqual(AccessToken.objects.count(), 0)
- api_application = Application()
- api_application.client_id = "foobar"
- api_application.user = user.user
- api_application.client_type = "confidential"
- api_application.authorization_grant_type = "password"
- api_application.client_secret = "42"
- api_application.save()
- token = AccessToken()
- token.user = user.user
- token.token = "r@d0m"
- token.application = api_application
- token.expires = datetime.now()
- token.save()
- self.assertEqual(Application.objects.count(), 1)
- self.assertEqual(AccessToken.objects.count(), 1)
-
- # add a karma note and a sanction with this user
- note = KarmaNote(moderator=user.user, user=user2.user, note="Good!", karma=5)
- note.save()
- ban = Ban(moderator=user.user, user=user2.user, type="Ban définitif", note="Test")
- ban.save()
-
- # login and unregister:
- self.client.force_login(user.user)
- result = self.client.post(reverse("member-unregister"), follow=False)
- self.assertEqual(result.status_code, 302)
-
- # check that the bot have taken authorship of tutorial:
- self.assertEqual(published_tutorial_alone.authors.count(), 1)
- self.assertEqual(
- published_tutorial_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"]
- )
- self.assertFalse(os.path.exists(writing_tutorial_alone_galler_path))
- self.assertEqual(published_tutorial_2.authors.count(), 1)
- self.assertEqual(
- published_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
- )
-
- # check that published tutorials remain published and accessible
- self.assertIsNotNone(published_tutorial_2.public_version.get_prod_path())
- self.assertTrue(os.path.exists(published_tutorial_2.public_version.get_prod_path()))
- self.assertIsNotNone(published_tutorial_alone.public_version.get_prod_path())
- self.assertTrue(os.path.exists(published_tutorial_alone.public_version.get_prod_path()))
- self.assertEqual(
- self.client.get(
- reverse("tutorial:view", args=[published_tutorial_alone.pk, published_tutorial_alone.slug]),
- follow=False,
- ).status_code,
- 200,
- )
- self.assertEqual(
- self.client.get(
- reverse("tutorial:view", args=[published_tutorial_2.pk, published_tutorial_2.slug]), follow=False
- ).status_code,
- 200,
- )
-
- # test that published articles remain accessible
- self.assertTrue(os.path.exists(published_article_alone.public_version.get_prod_path()))
- self.assertEqual(
- self.client.get(
- reverse("article:view", args=[published_article_alone.pk, published_article_alone.slug]), follow=True
- ).status_code,
- 200,
- )
- self.assertEqual(
- self.client.get(
- reverse("article:view", args=[published_article_2.pk, published_article_2.slug]), follow=True
- ).status_code,
- 200,
- )
-
- # check that the tutorial for which the author was alone does not exists anymore
- self.assertEqual(PublishableContent.objects.filter(pk=writing_tutorial_alone.pk).count(), 0)
- self.assertFalse(os.path.exists(writing_tutorial_alone.get_repo_path()))
-
- # check that bot haven't take the authorship of the tuto with more than one author
- self.assertEqual(writing_tutorial_2.authors.count(), 1)
- self.assertEqual(
- writing_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
- )
-
- # authorship for the article for which user was the only author
- self.assertEqual(published_article_alone.authors.count(), 1)
- self.assertEqual(
- published_article_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"]
- )
- self.assertEqual(published_article_2.authors.count(), 1)
-
- self.assertEqual(PublishableContent.objects.filter(pk=writing_article_alone.pk).count(), 0)
- self.assertFalse(os.path.exists(writing_article_alone.get_repo_path()))
-
- # not bot if another author:
- self.assertEqual(
- published_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
- )
- self.assertEqual(writing_article_2.authors.count(), 1)
- self.assertEqual(
- writing_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
- )
-
- # topics, gallery and PMs:
- self.assertEqual(Topic.objects.filter(author__username=user.user.username).count(), 0)
- self.assertEqual(Topic.objects.filter(solved_by=user.user).count(), 0)
- self.assertEqual(Topic.objects.filter(solved_by=self.anonymous).count(), 1)
- self.assertEqual(Post.objects.filter(author__username=user.user.username).count(), 0)
- self.assertEqual(Post.objects.filter(editor__username=user.user.username).count(), 0)
- self.assertEqual(PrivatePost.objects.filter(author__username=user.user.username).count(), 0)
- self.assertEqual(PrivateTopic.objects.filter(author__username=user.user.username).count(), 0)
-
- self.assertIsNotNone(Topic.objects.get(pk=authored_topic.pk))
- self.assertIsNotNone(PrivateTopic.objects.get(pk=private_topic.pk))
- self.assertIsNotNone(Gallery.objects.get(pk=alone_gallery.pk))
- self.assertEqual(alone_gallery.get_linked_users().count(), 1)
- self.assertEqual(shared_gallery.get_linked_users().count(), 1)
- self.assertEqual(UserGallery.objects.filter(user=user.user).count(), 0)
- self.assertEqual(CommentVote.objects.filter(user=user.user, positive=True).count(), 0)
- self.assertEqual(Post.objects.filter(pk=upvoted_answer.id).first().like, 0)
-
- # zep 12, published contents and beta
- self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_alone.pk).first())
- self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_2.pk).first())
- self.assertTrue(Topic.objects.get(pk=beta_content.beta_topic.pk).is_locked)
- self.assertFalse(Topic.objects.get(pk=beta_content_2.beta_topic.pk).is_locked)
-
- # check API
- self.assertEqual(Application.objects.count(), 0)
- self.assertEqual(AccessToken.objects.count(), 0)
-
- # check that the karma note and the sanction were kept
- self.assertTrue(KarmaNote.objects.filter(pk=note.pk).exists())
- self.assertTrue(Ban.objects.filter(pk=ban.pk).exists())
-
- def test_forgot_password(self):
- """To test nominal scenario of a lost password."""
-
- # Empty the test outbox
- mail.outbox = []
-
- result = self.client.post(
- reverse("member-forgot-password"),
- {
- "username": self.mas.user.username,
- "email": "",
- },
- follow=False,
- )
-
- self.assertEqual(result.status_code, 200)
-
- # check email has been sent
- self.assertEqual(len(mail.outbox), 1)
-
- # clic on the link which has been sent in mail
- user = User.objects.get(username=self.mas.user.username)
-
- token = TokenForgotPassword.objects.get(user=user)
- result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False)
-
- self.assertEqual(result.status_code, 200)
-
- def test_sanctions(self):
- """
- Test various sanctions.
- """
-
- staff = StaffProfileFactory()
- self.client.force_login(staff.user)
-
- # list of members.
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- nb_users = len(result.context["members"])
-
- # Test: LS
- user_ls = ProfileFactory()
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}),
- {"ls": "", "ls-text": "Texte de test pour LS"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertFalse(user.can_write)
- self.assertTrue(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-pubdate")[0]
- self.assertEqual(ban.type, "Lecture seule illimitée")
- self.assertEqual(ban.note, "Texte de test pour LS")
- self.assertEqual(len(mail.outbox), 1)
-
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good
-
- # Test: Un-LS
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}),
- {"un-ls": "", "unls-text": "Texte de test pour un-LS"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertTrue(user.can_write)
- self.assertTrue(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
- self.assertEqual(ban.type, "Levée de la lecture seule")
- self.assertEqual(ban.note, "Texte de test pour un-LS")
- self.assertEqual(len(mail.outbox), 2)
-
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good
-
- # Test: LS temp
- user_ls_temp = ProfileFactory()
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ls_temp.user.id}),
- {"ls-temp": "", "ls-jrs": 10, "ls-text": "Texte de test pour LS TEMP"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ls_temp.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertFalse(user.can_write)
- self.assertTrue(user.can_read)
- self.assertIsNotNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
- self.assertIn("Lecture seule temporaire", ban.type)
- self.assertEqual(ban.note, "Texte de test pour LS TEMP")
- self.assertEqual(len(mail.outbox), 3)
-
- # reset nb_users
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- nb_users = len(result.context["members"])
-
- # Test: BAN
- user_ban = ProfileFactory()
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}),
- {"ban": "", "ban-text": "Texte de test pour BAN"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertTrue(user.can_write)
- self.assertFalse(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
- self.assertEqual(ban.type, "Bannissement illimité")
- self.assertEqual(ban.note, "Texte de test pour BAN")
- self.assertEqual(len(mail.outbox), 4)
-
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users, len(result.context["members"])) # Banned guy doesn't show up, good
-
- # Test: un-BAN
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}),
- {"un-ban": "", "unban-text": "Texte de test pour BAN"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertTrue(user.can_write)
- self.assertTrue(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
- self.assertEqual(ban.type, "Levée du bannissement")
- self.assertEqual(ban.note, "Texte de test pour BAN")
- self.assertEqual(len(mail.outbox), 5)
-
- result = self.client.get(reverse("member-list"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(nb_users + 1, len(result.context["members"])) # UnBanned guy shows up, good
-
- # Test: BAN temp
- user_ban_temp = ProfileFactory()
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user_ban_temp.user.id}),
- {"ban-temp": "", "ban-jrs": 10, "ban-text": "Texte de test pour BAN TEMP"},
- follow=False,
- )
- user = Profile.objects.get(id=user_ban_temp.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 302)
- self.assertTrue(user.can_write)
- self.assertFalse(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNotNone(user.end_ban_read)
- ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
- self.assertIn("Bannissement temporaire", ban.type)
- self.assertEqual(ban.note, "Texte de test pour BAN TEMP")
- self.assertEqual(len(mail.outbox), 6)
-
- def test_sanctions_with_not_staff_user(self):
- user = ProfileFactory().user
-
- # we need staff right for update the sanction of a user, so a member who is not staff can't access to the page
- self.client.logout()
- self.client.force_login(user)
-
- # Test: LS
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": self.staff.id}),
- {"ls": "", "ls-text": "Texte de test pour LS"},
- follow=False,
- )
-
- self.assertEqual(result.status_code, 403)
-
- # if the user is staff, he can update the sanction of a user
- self.client.logout()
- self.client.force_login(self.staff)
-
- # Test: LS
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": user.id}),
- {"ls": "", "ls-text": "Texte de test pour LS"},
- follow=False,
- )
-
- self.assertEqual(result.status_code, 302)
-
- def test_failed_bot_sanctions(self):
-
- staff = StaffProfileFactory()
- self.client.force_login(staff.user)
-
- bot_profile = ProfileFactory()
- bot_profile.user.groups.add(self.bot)
- bot_profile.user.save()
-
- # Test: LS
- result = self.client.post(
- reverse("member-modify-profile", kwargs={"user_pk": bot_profile.user.id}),
- {"ls": "", "ls-text": "Texte de test pour LS"},
- follow=False,
- )
- user = Profile.objects.get(id=bot_profile.id) # Refresh profile from DB
- self.assertEqual(result.status_code, 403)
- self.assertTrue(user.can_write)
- self.assertTrue(user.can_read)
- self.assertIsNone(user.end_ban_write)
- self.assertIsNone(user.end_ban_read)
-
- def test_nonascii(self):
- user = NonAsciiProfileFactory()
- result = self.client.get(
- reverse("member-login") + "?next=" + reverse("member-detail", args=[user.user.username]), follow=False
- )
- self.assertEqual(result.status_code, 200)
-
- def test_promote_interface(self):
- """
- Test promotion interface.
- """
-
- # create users (one regular, one staff and one superuser)
- tester = ProfileFactory()
- staff = StaffProfileFactory()
- tester.user.is_active = False
- tester.user.save()
- staff.user.is_superuser = True
- staff.user.save()
-
- # create groups
- group = Group.objects.create(name="DummyGroup_1")
- groupbis = Group.objects.create(name="DummyGroup_2")
-
- # create Forums, Posts and subscribe member to them.
- category1 = ForumCategoryFactory(position=1)
- forum1 = ForumFactory(category=category1, position_in_category=1)
- forum1.groups.add(group)
- forum1.save()
- forum2 = ForumFactory(category=category1, position_in_category=2)
- forum2.groups.add(groupbis)
- forum2.save()
- forum3 = ForumFactory(category=category1, position_in_category=3)
- topic1 = TopicFactory(forum=forum1, author=staff.user)
- topic2 = TopicFactory(forum=forum2, author=staff.user)
- topic3 = TopicFactory(forum=forum3, author=staff.user)
-
- # LET THE TEST BEGIN !
-
- # tester shouldn't be able to connect
- login_check = self.client.login(username=tester.user.username, password="hostel77")
- self.assertEqual(login_check, False)
-
- # connect as staff (superuser)
- self.client.force_login(staff.user)
-
- # check that we can go through the page
- result = self.client.get(reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), follow=False)
- self.assertEqual(result.status_code, 200)
-
- # give groups thanks to staff (but account still not activated)
- result = self.client.post(
- reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}),
- {
- "groups": [group.id, groupbis.id],
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(id=tester.id) # refresh
-
- self.assertEqual(len(tester.user.groups.all()), 2)
- self.assertFalse(tester.user.is_active)
-
- # Now our tester is going to follow one post in every forum (3)
- TopicAnswerSubscription.objects.toggle_follow(topic1, tester.user)
- TopicAnswerSubscription.objects.toggle_follow(topic2, tester.user)
- TopicAnswerSubscription.objects.toggle_follow(topic3, tester.user)
-
- self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 3)
-
- # retract all right, keep one group only and activate account
- result = self.client.post(
- reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}),
- {"groups": [group.id], "activation": "on"},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(id=tester.id) # refresh
-
- self.assertEqual(len(tester.user.groups.all()), 1)
- self.assertTrue(tester.user.is_active)
- self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 2)
-
- # no groups specified
- result = self.client.post(
- reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), {"activation": "on"}, follow=False
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(id=tester.id) # refresh
- self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 1)
-
- # Finally, check that user can connect and can not access the interface
- login_check = self.client.login(username=tester.user.username, password="hostel77")
- self.assertEqual(login_check, True)
- result = self.client.post(
- reverse("member-settings-promote", kwargs={"user_pk": staff.user.id}), {"activation": "on"}, follow=False
- )
- self.assertEqual(result.status_code, 403) # forbidden !
-
- def test_filter_member_ip(self):
- """
- Test filter member by ip.
- """
-
- # create users (one regular and one staff and superuser)
- tester = ProfileFactory()
- staff = StaffProfileFactory()
-
- # test login normal user
- result = self.client.post(
- reverse("member-login"),
- {"username": tester.user.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- # good password then redirection
- self.assertEqual(result.status_code, 302)
-
- # Check that the filter can't be access from normal user
- result = self.client.post(
- reverse("member-from-ip", kwargs={"ip_address": tester.last_ip_address}), {}, follow=False
- )
- self.assertEqual(result.status_code, 403)
-
- # log the staff user
- result = self.client.post(
- reverse("member-login"),
- {"username": staff.user.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- # good password then redirection
- self.assertEqual(result.status_code, 302)
-
- # test that we retrieve correctly the 2 members (staff + user) from this ip
- result = self.client.post(
- reverse("member-from-ip", kwargs={"ip_address": staff.last_ip_address}), {}, follow=False
- )
- self.assertEqual(result.status_code, 200)
- self.assertEqual(len(result.context["members"]), 2)
-
- def test_modify_user_karma(self):
- """
- To test karma of a user modified by a staff user.
- """
- tester = ProfileFactory()
- staff = StaffProfileFactory()
-
- # login as user
- result = self.client.post(
- reverse("member-login"), {"username": tester.user.username, "password": "hostel77"}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # check that user can't use this feature
- result = self.client.post(reverse("member-modify-karma"), follow=False)
- self.assertEqual(result.status_code, 403)
-
- # login as staff
- result = self.client.post(
- reverse("member-login"), {"username": staff.user.username, "password": "hostel77"}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # try to give a few bad points to the tester
- result = self.client.post(
- reverse("member-modify-karma"),
- {"profile_pk": tester.pk, "note": "Bad tester is bad !", "karma": "-50"},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(pk=tester.pk)
- self.assertEqual(tester.karma, -50)
- self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 1)
-
- # Now give a few good points
- result = self.client.post(
- reverse("member-modify-karma"),
- {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": "10"},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(pk=tester.pk)
- self.assertEqual(tester.karma, -40)
- self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 2)
-
- # Now access some unknow user
- result = self.client.post(
- reverse("member-modify-karma"),
- {"profile_pk": 9999, "note": "Good tester is good !", "karma": "10"},
- follow=False,
- )
- self.assertEqual(result.status_code, 404)
-
- # Now give unknow point
- result = self.client.post(
- reverse("member-modify-karma"),
- {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": ""},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(pk=tester.pk)
- self.assertEqual(tester.karma, -40)
- self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 3)
-
- # Now give no point at all
- result = self.client.post(
- reverse("member-modify-karma"), {"profile_pk": tester.pk, "note": "Good tester is good !"}, follow=False
- )
- self.assertEqual(result.status_code, 302)
- tester = Profile.objects.get(pk=tester.pk)
- self.assertEqual(tester.karma, -40)
- self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 4)
-
- # Now access without post
- result = self.client.get(reverse("member-modify-karma"), follow=False)
- self.assertEqual(result.status_code, 405)
-
- def test_karma_and_pseudo_change(self):
- """
- To test that a karma note is added when a member change its pseudo
- """
- tester = ProfileFactory()
- old_pseudo = tester.user.username
- self.client.force_login(tester.user)
- data = {"username": "dummy", "email": tester.user.email}
- result = self.client.post(reverse("update-username-email-member"), data, follow=False)
-
- self.assertEqual(result.status_code, 302)
- notes = KarmaNote.objects.filter(user=tester.user).all()
- self.assertEqual(len(notes), 1)
- self.assertTrue(old_pseudo in notes[0].note and "dummy" in notes[0].note)
-
- def test_members_are_contactable(self):
- """
- The PM button is displayed to logged in users, except if it's the profile
- of a banned user.
- """
- user_ban = ProfileFactory()
- user_ban.can_read = False
- user_ban.can_write = False
- user_ban.save()
- user_1 = ProfileFactory()
- user_2 = ProfileFactory()
-
- phrase = "Envoyer un message"
-
- # The PM button is hidden for anonymous users
- result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
- self.assertNotContains(result, phrase)
-
- # Also for anonymous users viewing banned members profiles
- result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False)
- self.assertNotContains(result, phrase)
-
- self.client.force_login(user_2.user)
-
- # If an user is logged in, the PM button is shown for other normal users
- result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
- self.assertContains(result, phrase)
-
- # But not for banned users
- result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False)
- self.assertNotContains(result, phrase)
-
- self.client.logout()
- self.client.force_login(user_1.user)
-
- # Neither for his own profile
- result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
- self.assertNotContains(result, phrase)
-
- self.client.logout()
-
- def test_github_token(self):
- user = ProfileFactory()
- dev = DevProfileFactory()
-
- # test that github settings are only availables for dev
- self.client.force_login(user.user)
- result = self.client.get(reverse("update-github"), follow=False)
- self.assertEqual(result.status_code, 403)
- result = self.client.post(reverse("remove-github"), follow=False)
- self.assertEqual(result.status_code, 403)
- self.client.logout()
-
- # now, test the form
- self.client.force_login(dev.user)
- result = self.client.get(reverse("update-github"), follow=False)
- self.assertEqual(result.status_code, 200)
- result = self.client.post(
- reverse("update-github"),
- {
- "github_token": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # refresh
- dev = Profile.objects.get(pk=dev.pk)
- self.assertEqual(dev.github_token, "test")
-
- # test the option to remove the token
- result = self.client.post(reverse("remove-github"), follow=False)
- self.assertEqual(result.status_code, 302)
-
- # refresh
- dev = Profile.objects.get(pk=dev.pk)
- self.assertEqual(dev.github_token, "")
-
- def test_markdown_help_settings(self):
- user = ProfileFactory().user
-
- # login and check that the Markdown help is displayed
- self.client.force_login(user)
- result = self.client.get(reverse("pages-index"), follow=False)
- self.assertContains(result, 'data-show-markdown-help="true"')
-
- # disable Markdown help
- user.profile.show_markdown_help = False
- user.profile.save()
- result = self.client.get(reverse("pages-index"), follow=False)
- self.assertContains(result, 'data-show-markdown-help="false"')
-
- def test_new_provider_with_new_account(self):
- new_providers_count = NewEmailProvider.objects.count()
-
- # register a new user
- self.client.post(
- reverse("register-member"),
- {
- "username": "new",
- "password": "hostel77",
- "password_confirm": "hostel77",
- "email": "test@unknown-provider-register.com",
- },
- follow=False,
- )
-
- user = User.objects.get(username="new")
- token = TokenRegister.objects.get(user=user)
- self.client.get(token.get_absolute_url(), follow=False)
-
- # A new provider object should have been created
- self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count())
-
- def test_new_provider_with_email_edit(self):
- new_providers_count = NewEmailProvider.objects.count()
- user = ProfileFactory().user
- self.client.force_login(user)
- # Edit the email with an unknown provider
- self.client.post(
- reverse("update-username-email-member"),
- {"username": user.username, "email": "test@unknown-provider-edit.com"},
- follow=False,
- )
- # A new provider object should have been created
- self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count())
-
- def test_new_providers_list(self):
- # create a new provider
- user = ProfileFactory().user
- provider = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test.com")
- # check that the list is not available for a non-staff member
- self.client.logout()
- result = self.client.get(reverse("new-email-providers"), follow=False)
- self.assertEqual(result.status_code, 302)
- self.client.force_login(user)
- result = self.client.get(reverse("new-email-providers"), follow=False)
- self.assertEqual(result.status_code, 403)
- # and that it contains the provider we created
- self.client.force_login(self.staff)
- result = self.client.get(reverse("new-email-providers"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertIn(provider, result.context["providers"])
-
- def test_check_new_provider(self):
- # create two new providers
- user = ProfileFactory().user
- provider1 = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test1.com")
- provider2 = NewEmailProvider.objects.create(use="EMAIl_EDIT", user=user, provider="test2.com")
- # check that this option is only available for a staff member
- self.client.force_login(user)
- result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False)
- self.assertEqual(result.status_code, 403)
- # test approval
- self.client.force_login(self.staff)
- result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertFalse(NewEmailProvider.objects.filter(pk=provider1.pk).exists())
- self.assertFalse(BannedEmailProvider.objects.filter(provider=provider1.provider).exists())
- # test ban
- self.client.force_login(self.staff)
- result = self.client.post(reverse("check-new-email-provider", args=[provider2.pk]), {"ban": "on"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertFalse(NewEmailProvider.objects.filter(pk=provider2.pk).exists())
- self.assertTrue(BannedEmailProvider.objects.filter(provider=provider2.provider).exists())
-
- def test_banned_providers_list(self):
- user = ProfileFactory().user
- # create a banned provider
- provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test.com")
- # check that the list is not available for a non-staff member
- self.client.logout()
- result = self.client.get(reverse("banned-email-providers"), follow=False)
- self.assertEqual(result.status_code, 302)
- self.client.force_login(user)
- result = self.client.get(reverse("banned-email-providers"), follow=False)
- self.assertEqual(result.status_code, 403)
- # and that it contains the provider we created
- self.client.force_login(self.staff)
- result = self.client.get(reverse("banned-email-providers"), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertIn(provider, result.context["providers"])
-
- def test_add_banned_provider(self):
- # test that this page is only available for staff
- user = ProfileFactory().user
- self.client.logout()
- result = self.client.get(reverse("add-banned-email-provider"), follow=False)
- self.assertEqual(result.status_code, 302)
- self.client.force_login(user)
- result = self.client.get(reverse("add-banned-email-provider"), follow=False)
- self.assertEqual(result.status_code, 403)
- self.client.force_login(self.staff)
- result = self.client.get(reverse("add-banned-email-provider"), follow=False)
- self.assertEqual(result.status_code, 200)
-
- # add a provider
- result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertTrue(BannedEmailProvider.objects.filter(provider="new-provider.com").exists())
-
- # check that it cannot be added again
- result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertEqual(1, BannedEmailProvider.objects.filter(provider="new-provider.com").count())
-
- def test_members_with_provider(self):
- # create two members with the same provider
- member1 = ProfileFactory().user
- member2 = ProfileFactory().user
- member1.email = "test1@test-members.com"
- member1.save()
- member2.email = "test2@test-members.com"
- member2.save()
- # ban this provider
- provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-members.com")
- # check that this page is only available for staff
- self.client.logout()
- result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.client.force_login(member1)
- result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
- self.assertEqual(result.status_code, 403)
- self.client.force_login(self.staff)
- result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
- self.assertEqual(result.status_code, 200)
- # check that it contains the two members
- self.assertIn(member1.profile, result.context["members"])
- self.assertIn(member2.profile, result.context["members"])
-
- def test_remove_banned_provider(self):
- user = ProfileFactory().user
- # add a banned provider
- provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-remove.com")
- # check that this option is only available for a staff member
- self.client.force_login(user)
- result = self.client.post(reverse("check-new-email-provider", args=[provider.pk]), follow=False)
- self.assertEqual(result.status_code, 403)
- # test that it removes the provider
- self.client.force_login(self.staff)
- result = self.client.post(reverse("remove-banned-email-provider", args=[provider.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertFalse(BannedEmailProvider.objects.filter(pk=provider.pk).exists())
-
- def test_hats_on_profile(self):
- hat_name = "A hat"
-
- profile = ProfileFactory()
- user = profile.user
- # Test that hats don't appear if there are no hats
- self.client.force_login(user)
- result = self.client.get(profile.get_absolute_url())
- self.assertNotContains(result, _("Casquettes"))
- # Test that they don't appear with a staff member but that the link to add one does appear
- self.client.force_login(self.staff)
- result = self.client.get(profile.get_absolute_url())
- self.assertNotContains(result, _("Casquettes"))
- self.assertContains(result, _("Ajouter une casquette"))
- # Add a hat and check that it appears
- self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False)
- self.assertIn(hat_name, profile.hats.values_list("name", flat=True))
- result = self.client.get(profile.get_absolute_url())
- self.assertContains(result, _("Casquettes"))
- self.assertContains(result, hat_name)
- # And also for a member that is not staff
- self.client.force_login(user)
- result = self.client.get(profile.get_absolute_url())
- self.assertContains(result, _("Casquettes"))
- self.assertContains(result, hat_name)
- # Test that a hat linked to a group appears
- result = self.client.get(self.staff.profile.get_absolute_url())
- self.assertContains(result, _("Casquettes"))
- self.assertContains(result, "Staff")
-
- def test_add_hat(self):
- short_hat = "A new hat"
- long_hat = "A very long hat" * 3
- utf8mb4_hat = "🍊"
-
- profile = ProfileFactory()
- user = profile.user
- # check that this option is only available for a staff member
- self.client.force_login(user)
- result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False)
- self.assertEqual(result.status_code, 403)
- # login as staff
- self.client.force_login(self.staff)
- # test that it doesn't work with a too long hat (> 40 characters)
- result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": long_hat}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True))
- # test that it doesn't work with a hat using utf8mb4 characters
- result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": utf8mb4_hat}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(utf8mb4_hat, profile.hats.values_list("name", flat=True))
- # test that it doesn't work with a hat linked to a group
- result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": "Staff"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True))
- # test that it works with a short hat (<= 40 characters)
- result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertIn(short_hat, profile.hats.values_list("name", flat=True))
- # test that if the hat already exists, it is used
- result = self.client.post(reverse("add-hat", args=[self.staff.pk]), {"hat": short_hat}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertIn(short_hat, self.staff.profile.hats.values_list("name", flat=True))
- self.assertEqual(Hat.objects.filter(name=short_hat).count(), 1)
-
- def test_remove_hat(self):
- hat_name = "A hat"
-
- profile = ProfileFactory()
- user = profile.user
- # add a hat with a staff member
- self.client.force_login(self.staff)
- self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False)
- self.assertIn(hat_name, profile.hats.values_list("name", flat=True))
- hat = Hat.objects.get(name=hat_name)
- # test that this option is not available for an other user
- self.client.force_login(ProfileFactory().user)
- result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
- self.assertEqual(result.status_code, 403)
- self.assertIn(hat, profile.hats.all())
- # but check that it works for the user having the hat
- self.client.force_login(user)
- result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(hat, profile.hats.all())
- # test that it works for a staff member
- profile.hats.add(hat) # we have to add the hat again for this test
- self.client.force_login(self.staff)
- result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(hat, profile.hats.all())
- # but check that the hat still exists in database
- self.assertTrue(Hat.objects.filter(name=hat_name).exists())
-
- def test_old_smileys(self):
- """Test the cookie"""
-
- # NOTE: we have to use the "real" login and logout pages here
- cookie_key = settings.ZDS_APP["member"]["old_smileys_cookie_key"]
-
- profile_without_clem = ProfileFactory()
- profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
- self.assertFalse(profile_without_clem.use_old_smileys)
-
- user_without_clem = profile_without_clem.user
- profile_with_clem = ProfileFactory()
- profile_with_clem.use_old_smileys = True
- profile_with_clem.save()
- user_with_clem = profile_with_clem.user
-
- settings.ZDS_APP["member"]["old_smileys_allowed"] = True
-
- # test that the cookie is set when connection
- result = self.client.post(
- reverse("member-login"),
- {"username": user_with_clem.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.client.get(reverse("homepage"))
-
- self.assertIn(cookie_key, self.client.cookies)
- self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0)
-
- # test that logout set the cookies expiration to 0 (= no more cookie)
- self.client.post(reverse("member-logout"), follow=True)
- self.client.get(reverse("homepage"))
- self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
-
- # test that user without the setting have the cookie with expiration 0 (= no cookie)
- result = self.client.post(
- reverse("member-login"),
- {"username": user_without_clem.username, "password": "hostel77", "remember": "remember"},
- follow=False,
- )
-
- self.assertEqual(result.status_code, 302)
- self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
-
- # setting use_smileys sets the cookie
- self.client.post(
- reverse("update-member"),
- {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": ["use_old_smileys"]},
- )
- self.client.get(reverse("homepage"))
-
- profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
- self.assertTrue(profile_without_clem.use_old_smileys)
- self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0)
-
- # ... and that not setting it removes the cookie
- self.client.post(
- reverse("update-member"), {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": []}
- )
- self.client.get(reverse("homepage"))
-
- profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
- self.assertFalse(profile_without_clem.use_old_smileys)
- self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
-
- def test_hats_settings(self):
- hat_name = "A hat"
- other_hat_name = "Another hat"
- hat, _ = Hat.objects.get_or_create(name__iexact=hat_name, defaults={"name": hat_name})
- requests_count = HatRequest.objects.count()
- profile = ProfileFactory()
- profile.hats.add(hat)
- # login and check that the hat appears
- self.client.force_login(profile.user)
- result = self.client.get(reverse("hats-settings"))
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, hat_name)
- # check that it's impossible to ask for a hat the user already has
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
- self.assertEqual(HatRequest.objects.count(), requests_count) # request wasn't sent
- # ask for another hat
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": other_hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request was sent!
- # check the request appears
- result = self.client.get(reverse("hats-settings"))
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, other_hat_name)
- # and check it's impossible to ask for it again
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": other_hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
- self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent
- # check that it's impossible to ask for a hat linked to a group
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": "Staff",
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
- self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent
-
- def test_requested_hats(self):
- hat_name = "A hat"
- # ask for a hat
- profile = ProfileFactory()
- self.client.force_login(profile.user)
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- # test this page is only available for staff
- result = self.client.get(reverse("requested-hats"))
- self.assertEqual(result.status_code, 403)
- # login as staff
- self.client.force_login(self.staff)
- # test the count displayed on the user menu is right
- requests_count = HatRequest.objects.count()
- result = self.client.get(reverse("pages-index"))
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, f"({requests_count})")
- # test that the hat asked appears on the requested hats page
- result = self.client.get(reverse("requested-hats"))
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, hat_name)
-
- def test_hat_request_detail(self):
- hat_name = "A hat"
- # ask for a hat
- profile = ProfileFactory()
- self.client.force_login(profile.user)
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- request = HatRequest.objects.latest("date")
- # test this page is available for the request author
- result = self.client.get(request.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- # test it's not available for another user
- other_user = ProfileFactory().user
- self.client.force_login(other_user)
- result = self.client.get(request.get_absolute_url())
- self.assertEqual(result.status_code, 403)
- # login as staff
- self.client.force_login(self.staff)
- # test the page works
- result = self.client.get(request.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, hat_name)
- self.assertContains(result, profile.user.username)
- self.assertContains(result, request.reason)
-
- def test_solve_hat_request(self):
- hat_name = "A hat"
- # ask for a hat
- profile = ProfileFactory()
- self.client.force_login(profile.user)
- result = self.client.post(
- reverse("hats-settings"),
- {
- "hat": hat_name,
- "reason": "test",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- request = HatRequest.objects.latest("date")
- # test this page is only available for staff
- result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False)
- self.assertEqual(result.status_code, 403)
- # test denying as staff
- self.client.force_login(self.staff)
- result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertNotIn(hat_name, [h.name for h in profile.hats.all()])
- request = HatRequest.objects.get(pk=request.pk) # reload
- self.assertEqual(request.is_granted, False)
- # add a new request and test granting
- HatRequest.objects.create(user=profile.user, hat=hat_name, reason="test")
- request = HatRequest.objects.latest("date")
- result = self.client.post(reverse("solve-hat-request", args=[request.pk]), {"grant": "on"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertIn(hat_name, [h.name for h in profile.hats.all()])
- request = HatRequest.objects.get(pk=request.pk) # reload
- self.assertEqual(request.is_granted, True)
-
- def test_hats_list(self):
- # test the page is accessible without being authenticated
- self.client.logout()
- result = self.client.get(reverse("hats-list"))
- self.assertEqual(result.status_code, 200)
- # and while being authenticated
- self.client.force_login(self.staff)
- result = self.client.get(reverse("hats-list"))
- self.assertEqual(result.status_code, 200)
- # test that it does contain the name of a hat
- self.assertContains(result, "Staff") # this hat hat was created with the staff user
-
- def test_hat_detail(self):
- # we will use the staff hat, created with the staff user
- hat = Hat.objects.get(name="Staff")
- # test the page is accessible without being authenticated
- self.client.logout()
- result = self.client.get(hat.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- # and while being authenticated
- self.client.force_login(self.staff)
- result = self.client.get(hat.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- # test that it does contain the name of a hat
- self.assertContains(result, hat.name)
- # and the name of a user having it
- self.client.logout() # to prevent the username from being shown in topbar
- result = self.client.get(hat.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, self.staff.username)
- # if we display this group on the contact page...
- GroupContact.objects.create(group=Group.objects.get(name="staff"), description="group description", position=1)
- # the description should be shown on this page too
- result = self.client.get(hat.get_absolute_url())
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, "group description")
-
- def test_profile_report(self):
- profile = ProfileFactory()
- self.client.logout()
- alerts_count = Alert.objects.count()
- # test that authentication is required to report a profile
- result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertEqual(alerts_count, Alert.objects.count())
- # login and check it doesn't work without reason
- self.client.force_login(self.staff)
- result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": ""}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertEqual(alerts_count, Alert.objects.count())
- # add a reason and check it works
- result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False)
- self.assertEqual(result.status_code, 302)
- self.assertEqual(alerts_count + 1, Alert.objects.count())
- # test alert solving
- alert = Alert.objects.latest("pubdate")
- pm_count = PrivateTopic.objects.count()
- result = self.client.post(reverse("solve-profile-alert", args=[alert.pk]), {"text": "ok"}, follow=False)
- self.assertEqual(result.status_code, 302)
- alert = Alert.objects.get(pk=alert.pk) # refresh
- self.assertTrue(alert.solved)
- self.assertEqual(pm_count + 1, PrivateTopic.objects.count())
-
-
-mail_backend = Mock()
-
-
-class FakeBackend(BaseEmailBackend):
- def send_messages(self, email_messages):
- return mail_backend.send_messages(email_messages)
-
-
-@override_settings(EMAIL_BACKEND="zds.member.tests.tests_views.FakeBackend")
-class RegisterTest(TestCase):
- def test_exception_on_mail(self):
- def send_messages(messages):
- print("message sent")
- raise SMTPException(messages)
-
- mail_backend.send_messages = send_messages
-
- result = self.client.post(
- reverse("register-member"),
- {
- "username": "firm1",
- "password": "flavour",
- "password_confirm": "flavour",
- "email": "firm1@zestedesavoir.com",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
- self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8"))
-
-
-class IpListingsTests(TestCase):
- """Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network."""
-
- def setUp(self) -> None:
- self.staff = StaffProfileFactory().user
- self.regular_user = ProfileFactory()
-
- self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54")
- self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1"
- self.user_ipv4_same_ip_1.user.save()
-
- self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54")
- self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2"
- self.user_ipv4_same_ip_2.user.save()
-
- self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55")
- self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip"
- self.user_ipv4_different_ip.user.save()
-
- self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
- self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1"
- self.user_ipv6_same_ip_1.user.save()
-
- self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
- self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2"
- self.user_ipv6_same_ip_2.user.save()
-
- self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493")
- self.user_ipv6_same_network.user.username = "user_ipv6_same_network"
- self.user_ipv6_same_network.user.save()
-
- self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852")
- self.user_ipv6_different_network.user.username = "user_ipv6_different_network"
- self.user_ipv6_different_network.user.save()
-
- def test_same_ipv4(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address]))
- self.assertContains(response, self.user_ipv4_same_ip_1.user.username)
- self.assertContains(response, self.user_ipv4_same_ip_2.user.username)
- self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address)
- self.assertNotContains(response, self.user_ipv4_different_ip.user.username)
-
- def test_different_ipv4(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address]))
- self.assertContains(response, self.user_ipv4_different_ip.user.username)
- self.assertContains(response, self.user_ipv4_different_ip.last_ip_address)
- self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)
-
- def test_same_ipv6_and_same_ipv6_network(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address]))
- self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
- self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
- self.assertContains(response, self.user_ipv6_same_network.user.username)
- self.assertNotContains(response, self.user_ipv6_different_network.user.username)
-
- def test_same_ipv6_network_but_different_ip(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address]))
- self.assertContains(response, self.user_ipv6_same_network.user.username)
- self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
- self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
- self.assertNotContains(response, self.user_ipv6_different_network.user.username)
-
- def test_different_ipv6_network(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address]))
- self.assertContains(response, self.user_ipv6_different_network.user.username)
- self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)
- self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username)
- self.assertNotContains(response, self.user_ipv6_same_network.user.username)
-
- def test_access_rights_to_ip_page_as_regular_user(self) -> None:
- self.client.force_login(self.regular_user.user)
- response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
- self.assertEqual(response.status_code, 403)
-
- def test_access_rights_to_ip_page_as_anonymous(self) -> None:
- response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
- self.assertEqual(response.status_code, 302)
-
- def test_access_rights_to_ip_page_as_staff(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
- self.assertEqual(response.status_code, 200)
-
- def test_template_used_by_ip_page(self) -> None:
- self.client.force_login(self.staff)
- response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
- self.assertTemplateUsed(response, "member/admin/memberip.html")
diff --git a/zds/member/tests/views/__init__.py b/zds/member/tests/views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/zds/member/tests/views/tests_admin.py b/zds/member/tests/views/tests_admin.py
new file mode 100644
index 0000000000..e85ca3c3bb
--- /dev/null
+++ b/zds/member/tests/views/tests_admin.py
@@ -0,0 +1,117 @@
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.urls import reverse
+from django.test import TestCase
+
+from zds.notification.models import TopicAnswerSubscription
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.models import Profile
+from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory
+
+
+class MemberTests(TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_promote_interface(self):
+ """
+ Test promotion interface.
+ """
+
+ # create users (one regular, one staff and one superuser)
+ tester = ProfileFactory()
+ staff = StaffProfileFactory()
+ tester.user.is_active = False
+ tester.user.save()
+ staff.user.is_superuser = True
+ staff.user.save()
+
+ # create groups
+ group = Group.objects.create(name="DummyGroup_1")
+ groupbis = Group.objects.create(name="DummyGroup_2")
+
+ # create Forums, Posts and subscribe member to them.
+ category1 = ForumCategoryFactory(position=1)
+ forum1 = ForumFactory(category=category1, position_in_category=1)
+ forum1.groups.add(group)
+ forum1.save()
+ forum2 = ForumFactory(category=category1, position_in_category=2)
+ forum2.groups.add(groupbis)
+ forum2.save()
+ forum3 = ForumFactory(category=category1, position_in_category=3)
+ topic1 = TopicFactory(forum=forum1, author=staff.user)
+ topic2 = TopicFactory(forum=forum2, author=staff.user)
+ topic3 = TopicFactory(forum=forum3, author=staff.user)
+
+ # LET THE TEST BEGIN !
+
+ # tester shouldn't be able to connect
+ login_check = self.client.login(username=tester.user.username, password="hostel77")
+ self.assertEqual(login_check, False)
+
+ # connect as staff (superuser)
+ self.client.force_login(staff.user)
+
+ # check that we can go through the page
+ result = self.client.get(reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ # give groups thanks to staff (but account still not activated)
+ result = self.client.post(
+ reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}),
+ {
+ "groups": [group.id, groupbis.id],
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(id=tester.id) # refresh
+
+ self.assertEqual(len(tester.user.groups.all()), 2)
+ self.assertFalse(tester.user.is_active)
+
+ # Now our tester is going to follow one post in every forum (3)
+ TopicAnswerSubscription.objects.toggle_follow(topic1, tester.user)
+ TopicAnswerSubscription.objects.toggle_follow(topic2, tester.user)
+ TopicAnswerSubscription.objects.toggle_follow(topic3, tester.user)
+
+ self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 3)
+
+ # retract all right, keep one group only and activate account
+ result = self.client.post(
+ reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}),
+ {"groups": [group.id], "activation": "on"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(id=tester.id) # refresh
+
+ self.assertEqual(len(tester.user.groups.all()), 1)
+ self.assertTrue(tester.user.is_active)
+ self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 2)
+
+ # no groups specified
+ result = self.client.post(
+ reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), {"activation": "on"}, follow=False
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(id=tester.id) # refresh
+ self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 1)
+
+ # Finally, check that user can connect and can not access the interface
+ login_check = self.client.login(username=tester.user.username, password="hostel77")
+ self.assertEqual(login_check, True)
+ result = self.client.post(
+ reverse("member-settings-promote", kwargs={"user_pk": staff.user.id}), {"activation": "on"}, follow=False
+ )
+ self.assertEqual(result.status_code, 403) # forbidden !
diff --git a/zds/member/tests/views/tests_emailproviders.py b/zds/member/tests/views/tests_emailproviders.py
new file mode 100644
index 0000000000..348cfbd80d
--- /dev/null
+++ b/zds/member/tests/views/tests_emailproviders.py
@@ -0,0 +1,153 @@
+from django.conf import settings
+from django.contrib.auth.models import Group, User
+from django.urls import reverse
+from django.test import TestCase
+
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.models import NewEmailProvider, BannedEmailProvider, TokenRegister
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+
+
+class EmailProvidersTests(TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_new_provider_with_email_edit(self):
+ new_providers_count = NewEmailProvider.objects.count()
+ user = ProfileFactory().user
+ self.client.force_login(user)
+ # Edit the email with an unknown provider
+ self.client.post(
+ reverse("update-username-email-member"),
+ {"username": user.username, "email": "test@unknown-provider-edit.com"},
+ follow=False,
+ )
+ # A new provider object should have been created
+ self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count())
+
+ def test_new_providers_list(self):
+ # create a new provider
+ user = ProfileFactory().user
+ provider = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test.com")
+ # check that the list is not available for a non-staff member
+ self.client.logout()
+ result = self.client.get(reverse("new-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.client.force_login(user)
+ result = self.client.get(reverse("new-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 403)
+ # and that it contains the provider we created
+ self.client.force_login(self.staff)
+ result = self.client.get(reverse("new-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertIn(provider, result.context["providers"])
+
+ def test_check_new_provider(self):
+ # create two new providers
+ user = ProfileFactory().user
+ provider1 = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test1.com")
+ provider2 = NewEmailProvider.objects.create(use="EMAIl_EDIT", user=user, provider="test2.com")
+ # check that this option is only available for a staff member
+ self.client.force_login(user)
+ result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False)
+ self.assertEqual(result.status_code, 403)
+ # test approval
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertFalse(NewEmailProvider.objects.filter(pk=provider1.pk).exists())
+ self.assertFalse(BannedEmailProvider.objects.filter(provider=provider1.provider).exists())
+ # test ban
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("check-new-email-provider", args=[provider2.pk]), {"ban": "on"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertFalse(NewEmailProvider.objects.filter(pk=provider2.pk).exists())
+ self.assertTrue(BannedEmailProvider.objects.filter(provider=provider2.provider).exists())
+
+ def test_banned_providers_list(self):
+ user = ProfileFactory().user
+ # create a banned provider
+ provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test.com")
+ # check that the list is not available for a non-staff member
+ self.client.logout()
+ result = self.client.get(reverse("banned-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.client.force_login(user)
+ result = self.client.get(reverse("banned-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 403)
+ # and that it contains the provider we created
+ self.client.force_login(self.staff)
+ result = self.client.get(reverse("banned-email-providers"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertIn(provider, result.context["providers"])
+
+ def test_add_banned_provider(self):
+ # test that this page is only available for staff
+ user = ProfileFactory().user
+ self.client.logout()
+ result = self.client.get(reverse("add-banned-email-provider"), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.client.force_login(user)
+ result = self.client.get(reverse("add-banned-email-provider"), follow=False)
+ self.assertEqual(result.status_code, 403)
+ self.client.force_login(self.staff)
+ result = self.client.get(reverse("add-banned-email-provider"), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ # add a provider
+ result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(BannedEmailProvider.objects.filter(provider="new-provider.com").exists())
+
+ # check that it cannot be added again
+ result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(1, BannedEmailProvider.objects.filter(provider="new-provider.com").count())
+
+ def test_members_with_provider(self):
+ # create two members with the same provider
+ member1 = ProfileFactory().user
+ member2 = ProfileFactory().user
+ member1.email = "test1@test-members.com"
+ member1.save()
+ member2.email = "test2@test-members.com"
+ member2.save()
+ # ban this provider
+ provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-members.com")
+ # check that this page is only available for staff
+ self.client.logout()
+ result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.client.force_login(member1)
+ result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
+ self.assertEqual(result.status_code, 403)
+ self.client.force_login(self.staff)
+ result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False)
+ self.assertEqual(result.status_code, 200)
+ # check that it contains the two members
+ self.assertIn(member1.profile, result.context["members"])
+ self.assertIn(member2.profile, result.context["members"])
+
+ def test_remove_banned_provider(self):
+ user = ProfileFactory().user
+ # add a banned provider
+ provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-remove.com")
+ # check that this option is only available for a staff member
+ self.client.force_login(user)
+ result = self.client.post(reverse("check-new-email-provider", args=[provider.pk]), follow=False)
+ self.assertEqual(result.status_code, 403)
+ # test that it removes the provider
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("remove-banned-email-provider", args=[provider.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertFalse(BannedEmailProvider.objects.filter(pk=provider.pk).exists())
diff --git a/zds/member/tests/views/tests_hats.py b/zds/member/tests/views/tests_hats.py
new file mode 100644
index 0000000000..2dae508d62
--- /dev/null
+++ b/zds/member/tests/views/tests_hats.py
@@ -0,0 +1,312 @@
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.urls import reverse
+from django.test import TestCase
+from django.utils.translation import gettext_lazy as _
+
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.pages.models import GroupContact
+from zds.utils.models import Hat, HatRequest
+
+
+class HatTests(TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_hats_on_profile(self):
+ hat_name = "A hat"
+
+ profile = ProfileFactory()
+ user = profile.user
+ # Test that hats don't appear if there are no hats
+ self.client.force_login(user)
+ result = self.client.get(profile.get_absolute_url())
+ self.assertNotContains(result, _("Casquettes"))
+ # Test that they don't appear with a staff member but that the link to add one does appear
+ self.client.force_login(self.staff)
+ result = self.client.get(profile.get_absolute_url())
+ self.assertNotContains(result, _("Casquettes"))
+ self.assertContains(result, _("Ajouter une casquette"))
+ # Add a hat and check that it appears
+ self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False)
+ self.assertIn(hat_name, profile.hats.values_list("name", flat=True))
+ result = self.client.get(profile.get_absolute_url())
+ self.assertContains(result, _("Casquettes"))
+ self.assertContains(result, hat_name)
+ # And also for a member that is not staff
+ self.client.force_login(user)
+ result = self.client.get(profile.get_absolute_url())
+ self.assertContains(result, _("Casquettes"))
+ self.assertContains(result, hat_name)
+ # Test that a hat linked to a group appears
+ result = self.client.get(self.staff.profile.get_absolute_url())
+ self.assertContains(result, _("Casquettes"))
+ self.assertContains(result, "Staff")
+
+ def test_add_hat(self):
+ short_hat = "A new hat"
+ long_hat = "A very long hat" * 3
+ utf8mb4_hat = "🍊"
+
+ profile = ProfileFactory()
+ user = profile.user
+ # check that this option is only available for a staff member
+ self.client.force_login(user)
+ result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False)
+ self.assertEqual(result.status_code, 403)
+ # login as staff
+ self.client.force_login(self.staff)
+ # test that it doesn't work with a too long hat (> 40 characters)
+ result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": long_hat}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True))
+ # test that it doesn't work with a hat using utf8mb4 characters
+ result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": utf8mb4_hat}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(utf8mb4_hat, profile.hats.values_list("name", flat=True))
+ # test that it doesn't work with a hat linked to a group
+ result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": "Staff"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True))
+ # test that it works with a short hat (<= 40 characters)
+ result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertIn(short_hat, profile.hats.values_list("name", flat=True))
+ # test that if the hat already exists, it is used
+ result = self.client.post(reverse("add-hat", args=[self.staff.pk]), {"hat": short_hat}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertIn(short_hat, self.staff.profile.hats.values_list("name", flat=True))
+ self.assertEqual(Hat.objects.filter(name=short_hat).count(), 1)
+
+ def test_remove_hat(self):
+ hat_name = "A hat"
+
+ profile = ProfileFactory()
+ user = profile.user
+ # add a hat with a staff member
+ self.client.force_login(self.staff)
+ self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False)
+ self.assertIn(hat_name, profile.hats.values_list("name", flat=True))
+ hat = Hat.objects.get(name=hat_name)
+ # test that this option is not available for an other user
+ self.client.force_login(ProfileFactory().user)
+ result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
+ self.assertEqual(result.status_code, 403)
+ self.assertIn(hat, profile.hats.all())
+ # but check that it works for the user having the hat
+ self.client.force_login(user)
+ result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(hat, profile.hats.all())
+ # test that it works for a staff member
+ profile.hats.add(hat) # we have to add the hat again for this test
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(hat, profile.hats.all())
+ # but check that the hat still exists in database
+ self.assertTrue(Hat.objects.filter(name=hat_name).exists())
+
+ def test_hats_settings(self):
+ hat_name = "A hat"
+ other_hat_name = "Another hat"
+ hat, _ = Hat.objects.get_or_create(name__iexact=hat_name, defaults={"name": hat_name})
+ requests_count = HatRequest.objects.count()
+ profile = ProfileFactory()
+ profile.hats.add(hat)
+ # login and check that the hat appears
+ self.client.force_login(profile.user)
+ result = self.client.get(reverse("hats-settings"))
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, hat_name)
+ # check that it's impossible to ask for a hat the user already has
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(HatRequest.objects.count(), requests_count) # request wasn't sent
+ # ask for another hat
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": other_hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request was sent!
+ # check the request appears
+ result = self.client.get(reverse("hats-settings"))
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, other_hat_name)
+ # and check it's impossible to ask for it again
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": other_hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent
+ # check that it's impossible to ask for a hat linked to a group
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": "Staff",
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent
+
+ def test_requested_hats(self):
+ hat_name = "A hat"
+ # ask for a hat
+ profile = ProfileFactory()
+ self.client.force_login(profile.user)
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ # test this page is only available for staff
+ result = self.client.get(reverse("requested-hats"))
+ self.assertEqual(result.status_code, 403)
+ # login as staff
+ self.client.force_login(self.staff)
+ # test the count displayed on the user menu is right
+ requests_count = HatRequest.objects.count()
+ result = self.client.get(reverse("pages-index"))
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, f"({requests_count})")
+ # test that the hat asked appears on the requested hats page
+ result = self.client.get(reverse("requested-hats"))
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, hat_name)
+
+ def test_hat_request_detail(self):
+ hat_name = "A hat"
+ # ask for a hat
+ profile = ProfileFactory()
+ self.client.force_login(profile.user)
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ request = HatRequest.objects.latest("date")
+ # test this page is available for the request author
+ result = self.client.get(request.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ # test it's not available for another user
+ other_user = ProfileFactory().user
+ self.client.force_login(other_user)
+ result = self.client.get(request.get_absolute_url())
+ self.assertEqual(result.status_code, 403)
+ # login as staff
+ self.client.force_login(self.staff)
+ # test the page works
+ result = self.client.get(request.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, hat_name)
+ self.assertContains(result, profile.user.username)
+ self.assertContains(result, request.reason)
+
+ def test_solve_hat_request(self):
+ hat_name = "A hat"
+ # ask for a hat
+ profile = ProfileFactory()
+ self.client.force_login(profile.user)
+ result = self.client.post(
+ reverse("hats-settings"),
+ {
+ "hat": hat_name,
+ "reason": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ request = HatRequest.objects.latest("date")
+ # test this page is only available for staff
+ result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False)
+ self.assertEqual(result.status_code, 403)
+ # test denying as staff
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertNotIn(hat_name, [h.name for h in profile.hats.all()])
+ request = HatRequest.objects.get(pk=request.pk) # reload
+ self.assertEqual(request.is_granted, False)
+ # add a new request and test granting
+ HatRequest.objects.create(user=profile.user, hat=hat_name, reason="test")
+ request = HatRequest.objects.latest("date")
+ result = self.client.post(reverse("solve-hat-request", args=[request.pk]), {"grant": "on"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertIn(hat_name, [h.name for h in profile.hats.all()])
+ request = HatRequest.objects.get(pk=request.pk) # reload
+ self.assertEqual(request.is_granted, True)
+
+ def test_hats_list(self):
+ # test the page is accessible without being authenticated
+ self.client.logout()
+ result = self.client.get(reverse("hats-list"))
+ self.assertEqual(result.status_code, 200)
+ # and while being authenticated
+ self.client.force_login(self.staff)
+ result = self.client.get(reverse("hats-list"))
+ self.assertEqual(result.status_code, 200)
+ # test that it does contain the name of a hat
+ self.assertContains(result, "Staff") # this hat hat was created with the staff user
+
+ def test_hat_detail(self):
+ # we will use the staff hat, created with the staff user
+ hat = Hat.objects.get(name="Staff")
+ # test the page is accessible without being authenticated
+ self.client.logout()
+ result = self.client.get(hat.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ # and while being authenticated
+ self.client.force_login(self.staff)
+ result = self.client.get(hat.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ # test that it does contain the name of a hat
+ self.assertContains(result, hat.name)
+ # and the name of a user having it
+ self.client.logout() # to prevent the username from being shown in topbar
+ result = self.client.get(hat.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, self.staff.username)
+ # if we display this group on the contact page...
+ GroupContact.objects.create(group=Group.objects.get(name="staff"), description="group description", position=1)
+ # the description should be shown on this page too
+ result = self.client.get(hat.get_absolute_url())
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(result, "group description")
diff --git a/zds/member/tests/views/tests_login.py b/zds/member/tests/views/tests_login.py
new file mode 100644
index 0000000000..f0c6cfa530
--- /dev/null
+++ b/zds/member/tests/views/tests_login.py
@@ -0,0 +1,80 @@
+from django.urls import reverse
+from django.test import TestCase
+from django.utils.translation import gettext_lazy as _
+
+from zds.member.factories import ProfileFactory, NonAsciiProfileFactory
+
+
+class MemberTests(TestCase):
+ def test_login(self):
+ """
+ To test user login.
+ """
+ user = ProfileFactory()
+
+ # login a user. Good password then redirection to the homepage.
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": user.user.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ self.assertRedirects(result, reverse("homepage"))
+
+ # login failed with bad password then no redirection
+ # (status_code equals 200 and not 302).
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": user.user.username, "password": "hostel", "remember": "remember"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(
+ result,
+ _(
+ "Le mot de passe saisi est incorrect. "
+ "Cliquez sur le lien « Mot de passe oublié ? » "
+ "si vous ne vous en souvenez plus."
+ ),
+ )
+
+ # login failed with bad username then no redirection
+ # (status_code equals 200 and not 302).
+ result = self.client.post(
+ reverse("member-login"), {"username": "clem", "password": "hostel77", "remember": "remember"}, follow=False
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertContains(
+ result,
+ _("Ce nom d’utilisateur est inconnu. " "Si vous ne possédez pas de compte, " "vous pouvez vous inscrire."),
+ )
+
+ # login a user. Good password and next parameter then
+ # redirection to the "next" page.
+ result = self.client.post(
+ reverse("member-login") + "?next=" + reverse("gallery-list"),
+ {"username": user.user.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ self.assertRedirects(result, reverse("gallery-list"))
+
+ # check the user is redirected to the home page if
+ # the "next" parameter points to a non-existing page.
+ result = self.client.post(
+ reverse("member-login") + "?next=/foobar",
+ {"username": user.user.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ self.assertRedirects(result, reverse("homepage"))
+
+ # check if the login form will redirect if there is
+ # a next parameter.
+ self.client.logout()
+ result = self.client.get(reverse("member-login") + "?next=" + reverse("gallery-list"))
+ self.assertContains(result, reverse("member-login") + "?next=" + reverse("gallery-list"), count=1)
+
+ def test_nonascii(self):
+ user = NonAsciiProfileFactory()
+ result = self.client.get(
+ reverse("member-login") + "?next=" + reverse("member-detail", args=[user.user.username]), follow=False
+ )
+ self.assertEqual(result.status_code, 200)
diff --git a/zds/member/tests/views/tests_moderation.py b/zds/member/tests/views/tests_moderation.py
new file mode 100644
index 0000000000..2a3f9bead6
--- /dev/null
+++ b/zds/member/tests/views/tests_moderation.py
@@ -0,0 +1,518 @@
+from datetime import datetime
+
+from django.conf import settings
+from django.core import mail
+from django.contrib.auth.models import Group
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+from django.test import TestCase
+
+
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.member.views.moderation import member_from_ip
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.models import Profile, Ban, KarmaNote
+
+
+class TestsModeration(TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_sanctions(self):
+ """
+ Test various sanctions.
+ """
+
+ staff = StaffProfileFactory()
+ self.client.force_login(staff.user)
+
+ # list of members.
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ nb_users = len(result.context["members"])
+
+ # Test: LS
+ user_ls = ProfileFactory()
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}),
+ {"ls": "", "ls-text": "Texte de test pour LS"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertFalse(user.can_write)
+ self.assertTrue(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-pubdate")[0]
+ self.assertEqual(ban.type, "Lecture seule illimitée")
+ self.assertEqual(ban.note, "Texte de test pour LS")
+ self.assertEqual(len(mail.outbox), 1)
+
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good
+
+ # Test: Un-LS
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}),
+ {"un-ls": "", "unls-text": "Texte de test pour un-LS"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(user.can_write)
+ self.assertTrue(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
+ self.assertEqual(ban.type, "Levée de la lecture seule")
+ self.assertEqual(ban.note, "Texte de test pour un-LS")
+ self.assertEqual(len(mail.outbox), 2)
+
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good
+
+ # Test: LS temp
+ user_ls_temp = ProfileFactory()
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ls_temp.user.id}),
+ {"ls-temp": "", "ls-jrs": 10, "ls-text": "Texte de test pour LS TEMP"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ls_temp.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertFalse(user.can_write)
+ self.assertTrue(user.can_read)
+ self.assertIsNotNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
+ self.assertIn("Lecture seule temporaire", ban.type)
+ self.assertEqual(ban.note, "Texte de test pour LS TEMP")
+ self.assertEqual(len(mail.outbox), 3)
+
+ # reset nb_users
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ nb_users = len(result.context["members"])
+
+ # Test: BAN
+ user_ban = ProfileFactory()
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}),
+ {"ban": "", "ban-text": "Texte de test pour BAN"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(user.can_write)
+ self.assertFalse(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
+ self.assertEqual(ban.type, "Bannissement illimité")
+ self.assertEqual(ban.note, "Texte de test pour BAN")
+ self.assertEqual(len(mail.outbox), 4)
+
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users, len(result.context["members"])) # Banned guy doesn't show up, good
+
+ # Test: un-BAN
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}),
+ {"un-ban": "", "unban-text": "Texte de test pour BAN"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(user.can_write)
+ self.assertTrue(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
+ self.assertEqual(ban.type, "Levée du bannissement")
+ self.assertEqual(ban.note, "Texte de test pour BAN")
+ self.assertEqual(len(mail.outbox), 5)
+
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users + 1, len(result.context["members"])) # UnBanned guy shows up, good
+
+ # Test: BAN temp
+ user_ban_temp = ProfileFactory()
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user_ban_temp.user.id}),
+ {"ban-temp": "", "ban-jrs": 10, "ban-text": "Texte de test pour BAN TEMP"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=user_ban_temp.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(user.can_write)
+ self.assertFalse(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNotNone(user.end_ban_read)
+ ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0]
+ self.assertIn("Bannissement temporaire", ban.type)
+ self.assertEqual(ban.note, "Texte de test pour BAN TEMP")
+ self.assertEqual(len(mail.outbox), 6)
+
+ def test_sanctions_with_not_staff_user(self):
+ user = ProfileFactory().user
+
+ # we need staff right for update the sanction of a user, so a member who is not staff can't access to the page
+ self.client.logout()
+ self.client.force_login(user)
+
+ # Test: LS
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": self.staff.id}),
+ {"ls": "", "ls-text": "Texte de test pour LS"},
+ follow=False,
+ )
+
+ self.assertEqual(result.status_code, 403)
+
+ # if the user is staff, he can update the sanction of a user
+ self.client.logout()
+ self.client.force_login(self.staff)
+
+ # Test: LS
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": user.id}),
+ {"ls": "", "ls-text": "Texte de test pour LS"},
+ follow=False,
+ )
+
+ self.assertEqual(result.status_code, 302)
+
+ def test_failed_bot_sanctions(self):
+
+ staff = StaffProfileFactory()
+ self.client.force_login(staff.user)
+
+ bot_profile = ProfileFactory()
+ bot_profile.user.groups.add(self.bot)
+ bot_profile.user.save()
+
+ # Test: LS
+ result = self.client.post(
+ reverse("member-modify-profile", kwargs={"user_pk": bot_profile.user.id}),
+ {"ls": "", "ls-text": "Texte de test pour LS"},
+ follow=False,
+ )
+ user = Profile.objects.get(id=bot_profile.id) # Refresh profile from DB
+ self.assertEqual(result.status_code, 403)
+ self.assertTrue(user.can_write)
+ self.assertTrue(user.can_read)
+ self.assertIsNone(user.end_ban_write)
+ self.assertIsNone(user.end_ban_read)
+
+ def test_karma(self):
+ user = ProfileFactory()
+ other_user = ProfileFactory()
+ self.client.force_login(other_user.user)
+ r = self.client.post(reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"})
+ self.assertEqual(403, r.status_code)
+ self.client.logout()
+ self.client.force_login(self.staff)
+ # bad id
+ r = self.client.post(
+ reverse("member-modify-karma"), {"profile_pk": "blah", "karma": 42, "note": "warn"}, follow=True
+ )
+ self.assertEqual(404, r.status_code)
+ # good karma
+ r = self.client.post(
+ reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}, follow=True
+ )
+ self.assertEqual(200, r.status_code)
+ self.assertIn("{} : 42".format(_("Modification du karma")), r.content.decode("utf-8"))
+ # more than 100 karma must unvalidate the karma
+ r = self.client.post(
+ reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 420, "note": "warn"}, follow=True
+ )
+ self.assertEqual(200, r.status_code)
+ self.assertNotIn("{} : 420".format(_("Modification du karma")), r.content.decode("utf-8"))
+ # empty warning must unvalidate the karma
+ r = self.client.post(
+ reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 41, "note": ""}, follow=True
+ )
+ self.assertEqual(200, r.status_code)
+ self.assertNotIn("{} : 41".format(_("Modification du karma")), r.content.decode("utf-8"))
+
+ def test_modify_user_karma(self):
+ """
+ To test karma of a user modified by a staff user.
+ """
+ tester = ProfileFactory()
+ staff = StaffProfileFactory()
+
+ # login as user
+ result = self.client.post(
+ reverse("member-login"), {"username": tester.user.username, "password": "hostel77"}, follow=False
+ )
+ self.assertEqual(result.status_code, 302)
+
+ # check that user can't use this feature
+ result = self.client.post(reverse("member-modify-karma"), follow=False)
+ self.assertEqual(result.status_code, 403)
+
+ # login as staff
+ result = self.client.post(
+ reverse("member-login"), {"username": staff.user.username, "password": "hostel77"}, follow=False
+ )
+ self.assertEqual(result.status_code, 302)
+
+ # try to give a few bad points to the tester
+ result = self.client.post(
+ reverse("member-modify-karma"),
+ {"profile_pk": tester.pk, "note": "Bad tester is bad !", "karma": "-50"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(pk=tester.pk)
+ self.assertEqual(tester.karma, -50)
+ self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 1)
+
+ # Now give a few good points
+ result = self.client.post(
+ reverse("member-modify-karma"),
+ {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": "10"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(pk=tester.pk)
+ self.assertEqual(tester.karma, -40)
+ self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 2)
+
+ # Now access some unknow user
+ result = self.client.post(
+ reverse("member-modify-karma"),
+ {"profile_pk": 9999, "note": "Good tester is good !", "karma": "10"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 404)
+
+ # Now give unknow point
+ result = self.client.post(
+ reverse("member-modify-karma"),
+ {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": ""},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(pk=tester.pk)
+ self.assertEqual(tester.karma, -40)
+ self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 3)
+
+ # Now give no point at all
+ result = self.client.post(
+ reverse("member-modify-karma"), {"profile_pk": tester.pk, "note": "Good tester is good !"}, follow=False
+ )
+ self.assertEqual(result.status_code, 302)
+ tester = Profile.objects.get(pk=tester.pk)
+ self.assertEqual(tester.karma, -40)
+ self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 4)
+
+ # Now access without post
+ result = self.client.get(reverse("member-modify-karma"), follow=False)
+ self.assertEqual(result.status_code, 405)
+
+ def test_karma_and_pseudo_change(self):
+ """
+ To test that a karma note is added when a member change its pseudo
+ """
+ tester = ProfileFactory()
+ old_pseudo = tester.user.username
+ self.client.force_login(tester.user)
+ data = {"username": "dummy", "email": tester.user.email}
+ result = self.client.post(reverse("update-username-email-member"), data, follow=False)
+
+ self.assertEqual(result.status_code, 302)
+ notes = KarmaNote.objects.filter(user=tester.user).all()
+ self.assertEqual(len(notes), 1)
+ self.assertTrue(old_pseudo in notes[0].note and "dummy" in notes[0].note)
+
+ def test_moderation_history(self):
+ user = ProfileFactory().user
+
+ ban = Ban(
+ user=user,
+ moderator=self.staff,
+ type="Lecture Seule Temporaire",
+ note="Test de LS",
+ pubdate=datetime.now(),
+ )
+ ban.save()
+
+ note = KarmaNote(
+ user=user,
+ moderator=self.staff,
+ karma=5,
+ note="Test de karma",
+ pubdate=datetime.now(),
+ )
+ note.save()
+
+ # staff rights are required to view the history, check that
+ self.client.logout()
+ self.client.force_login(user)
+ result = self.client.get(user.profile.get_absolute_url(), follow=False)
+ self.assertNotContains(result, "Historique de modération")
+
+ self.client.logout()
+ self.client.force_login(self.staff)
+ result = self.client.get(user.profile.get_absolute_url(), follow=False)
+ self.assertContains(result, "Historique de modération")
+
+ # check that the note and the sanction are in the context
+ self.assertIn(ban, result.context["actions"])
+ self.assertIn(note, result.context["actions"])
+
+ # and are displayed
+ self.assertContains(result, "Test de LS")
+ self.assertContains(result, "Test de karma")
+
+ def test_filter_member_ip(self):
+ """
+ Test filter member by ip.
+ """
+
+ # create users (one regular and one staff and superuser)
+ tester = ProfileFactory()
+ staff = StaffProfileFactory()
+
+ # test login normal user
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": tester.user.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ # good password then redirection
+ self.assertEqual(result.status_code, 302)
+
+ # Check that the filter can't be access from normal user
+ result = self.client.post(
+ reverse("member-from-ip", kwargs={"ip_address": tester.last_ip_address}), {}, follow=False
+ )
+ self.assertEqual(result.status_code, 403)
+
+ # log the staff user
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": staff.user.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ # good password then redirection
+ self.assertEqual(result.status_code, 302)
+
+ # test that we retrieve correctly the 2 members (staff + user) from this ip
+ result = self.client.post(
+ reverse("member-from-ip", kwargs={"ip_address": staff.last_ip_address}), {}, follow=False
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(len(result.context["members"]), 2)
+
+
+class IpListingsTests(TestCase):
+ """Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network."""
+
+ def setUp(self) -> None:
+ self.staff = StaffProfileFactory().user
+ self.regular_user = ProfileFactory()
+
+ self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54")
+ self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1"
+ self.user_ipv4_same_ip_1.user.save()
+
+ self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54")
+ self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2"
+ self.user_ipv4_same_ip_2.user.save()
+
+ self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55")
+ self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip"
+ self.user_ipv4_different_ip.user.save()
+
+ self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
+ self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1"
+ self.user_ipv6_same_ip_1.user.save()
+
+ self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
+ self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2"
+ self.user_ipv6_same_ip_2.user.save()
+
+ self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493")
+ self.user_ipv6_same_network.user.username = "user_ipv6_same_network"
+ self.user_ipv6_same_network.user.save()
+
+ self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852")
+ self.user_ipv6_different_network.user.username = "user_ipv6_different_network"
+ self.user_ipv6_different_network.user.save()
+
+ def test_same_ipv4(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address]))
+ self.assertContains(response, self.user_ipv4_same_ip_1.user.username)
+ self.assertContains(response, self.user_ipv4_same_ip_2.user.username)
+ self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address)
+ self.assertNotContains(response, self.user_ipv4_different_ip.user.username)
+
+ def test_different_ipv4(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address]))
+ self.assertContains(response, self.user_ipv4_different_ip.user.username)
+ self.assertContains(response, self.user_ipv4_different_ip.last_ip_address)
+ self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)
+
+ def test_same_ipv6_and_same_ipv6_network(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address]))
+ self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
+ self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
+ self.assertContains(response, self.user_ipv6_same_network.user.username)
+ self.assertNotContains(response, self.user_ipv6_different_network.user.username)
+
+ def test_same_ipv6_network_but_different_ip(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address]))
+ self.assertContains(response, self.user_ipv6_same_network.user.username)
+ self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
+ self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
+ self.assertNotContains(response, self.user_ipv6_different_network.user.username)
+
+ def test_different_ipv6_network(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address]))
+ self.assertContains(response, self.user_ipv6_different_network.user.username)
+ self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)
+ self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username)
+ self.assertNotContains(response, self.user_ipv6_same_network.user.username)
+
+ def test_access_rights_to_ip_page_as_regular_user(self) -> None:
+ self.client.force_login(self.regular_user.user)
+ response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
+ self.assertEqual(response.status_code, 403)
+
+ def test_access_rights_to_ip_page_as_anonymous(self) -> None:
+ response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
+ self.assertEqual(response.status_code, 302)
+
+ def test_access_rights_to_ip_page_as_staff(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
+ self.assertEqual(response.status_code, 200)
+
+ def test_template_used_by_ip_page(self) -> None:
+ self.client.force_login(self.staff)
+ response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
+ self.assertTemplateUsed(response, "member/admin/memberip.html")
diff --git a/zds/member/tests/views/tests_password_recovery.py b/zds/member/tests/views/tests_password_recovery.py
new file mode 100644
index 0000000000..d08e968454
--- /dev/null
+++ b/zds/member/tests/views/tests_password_recovery.py
@@ -0,0 +1,52 @@
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.core import mail
+from django.urls import reverse
+from django.test import TestCase
+
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.models import TokenForgotPassword
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+
+
+class MemberTests(TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_forgot_password(self):
+ """To test nominal scenario of a lost password."""
+
+ # Empty the test outbox
+ mail.outbox = []
+
+ result = self.client.post(
+ reverse("member-forgot-password"),
+ {
+ "username": self.mas.user.username,
+ "email": "",
+ },
+ follow=False,
+ )
+
+ self.assertEqual(result.status_code, 200)
+
+ # check email has been sent
+ self.assertEqual(len(mail.outbox), 1)
+
+ # clic on the link which has been sent in mail
+ user = User.objects.get(username=self.mas.user.username)
+
+ token = TokenForgotPassword.objects.get(user=user)
+ result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False)
+
+ self.assertEqual(result.status_code, 200)
diff --git a/zds/member/tests/views/tests_profile.py b/zds/member/tests/views/tests_profile.py
new file mode 100644
index 0000000000..9b0aeb5bed
--- /dev/null
+++ b/zds/member/tests/views/tests_profile.py
@@ -0,0 +1,322 @@
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.urls import reverse
+from django.test import TestCase
+
+from zds.member.factories import (
+ ProfileFactory,
+ StaffProfileFactory,
+ UserFactory,
+ DevProfileFactory,
+)
+from zds.member.models import Profile
+from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+
+
+@override_for_contents()
+class MemberTests(TutorialTestMixin, TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_list_members(self):
+ """
+ To test the listing of the members with and without page parameter.
+ """
+
+ # create strange member
+ weird = ProfileFactory()
+ weird.user.username = "ïtrema718"
+ weird.user.email = "foo@\xfbgmail.com"
+ weird.user.save()
+
+ # list of members.
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ nb_users = len(result.context["members"])
+
+ # Test that inactive user don't show up
+ unactive_user = ProfileFactory()
+ unactive_user.user.is_active = False
+ unactive_user.user.save()
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users, len(result.context["members"]))
+
+ # Add a Bot and check that list didn't change
+ bot_profile = ProfileFactory()
+ bot_profile.user.groups.add(self.bot)
+ bot_profile.user.save()
+ result = self.client.get(reverse("member-list"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(nb_users, len(result.context["members"]))
+
+ # list of members with page parameter.
+ result = self.client.get(reverse("member-list") + "?page=1", follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ # page which doesn't exist.
+ result = self.client.get(reverse("member-list") + "?page=1534", follow=False)
+ self.assertEqual(result.status_code, 404)
+
+ # page parameter isn't an integer.
+ result = self.client.get(reverse("member-list") + "?page=abcd", follow=False)
+ self.assertEqual(result.status_code, 404)
+
+ def test_details_member(self):
+ """
+ To test details of a member given.
+ """
+
+ # details of a staff user.
+ result = self.client.get(reverse("member-detail", args=[self.staff.username]), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ # details of an unknown user.
+ result = self.client.get(reverse("member-detail", args=["unknown_user"]), follow=False)
+ self.assertEqual(result.status_code, 404)
+
+ def test_redirection_when_using_old_detail_member_url(self):
+ """
+ To test the redirection when accessing the member profile through the old url
+ """
+ user = ProfileFactory().user
+ result = self.client.get(reverse("member-detail-redirect", args=[user.username]), follow=False)
+
+ self.assertEqual(result.status_code, 301)
+
+ def test_old_detail_member_url_with_unexistant_member(self):
+ """
+ To test wether a 404 error is raised when the user in the old url does not exist
+ """
+ response = self.client.get(reverse("member-detail-redirect", args=["tartempion"]), follow=False)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_profile_page_of_weird_member_username(self):
+
+ # create some user with weird username
+ user_1 = ProfileFactory()
+ user_2 = ProfileFactory()
+ user_3 = ProfileFactory()
+ user_1.user.username = "ïtrema"
+ user_1.user.save()
+ user_2.user.username = ""a"
+ user_2.user.save()
+ user_3.user.username = "_`_`_`_"
+ user_3.user.save()
+
+ # profile pages of weird users.
+ result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=True)
+ self.assertEqual(result.status_code, 200)
+ result = self.client.get(reverse("member-detail", args=[user_2.user.username]), follow=True)
+ self.assertEqual(result.status_code, 200)
+ result = self.client.get(reverse("member-detail", args=[user_3.user.username]), follow=True)
+ self.assertEqual(result.status_code, 200)
+
+ def test_modify_member(self):
+ user = ProfileFactory().user
+
+ # we need staff right for update other profile, so a member who is not staff can't access to the page
+ self.client.logout()
+ self.client.force_login(user)
+
+ result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False)
+ self.assertEqual(result.status_code, 403)
+
+ self.client.logout()
+ self.client.force_login(self.staff)
+
+ # an inexistant member return 404
+ result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False)
+ self.assertEqual(result.status_code, 404)
+
+ # an existant member return 200
+ result = self.client.get(reverse("member-settings-mini-profile", args=[self.mas.user.username]), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ def test_success_preview_biography(self):
+
+ member = ProfileFactory()
+ self.client.force_login(member.user)
+
+ response = self.client.post(
+ reverse("update-member"),
+ {
+ "text": "It is **my** life",
+ "preview": "",
+ },
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+
+ result_string = "".join(a.decode() for a in response.streaming_content)
+ self.assertIn("my", result_string, "We need the biography to be properly formatted")
+
+ def test_members_are_contactable(self):
+ """
+ The PM button is displayed to logged in users, except if it's the profile
+ of a banned user.
+ """
+ user_ban = ProfileFactory()
+ user_ban.can_read = False
+ user_ban.can_write = False
+ user_ban.save()
+ user_1 = ProfileFactory()
+ user_2 = ProfileFactory()
+
+ phrase = "Envoyer un message"
+
+ # The PM button is hidden for anonymous users
+ result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
+ self.assertNotContains(result, phrase)
+
+ # Also for anonymous users viewing banned members profiles
+ result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False)
+ self.assertNotContains(result, phrase)
+
+ self.client.force_login(user_2.user)
+
+ # If an user is logged in, the PM button is shown for other normal users
+ result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
+ self.assertContains(result, phrase)
+
+ # But not for banned users
+ result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False)
+ self.assertNotContains(result, phrase)
+
+ self.client.logout()
+ self.client.force_login(user_1.user)
+
+ # Neither for his own profile
+ result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False)
+ self.assertNotContains(result, phrase)
+
+ self.client.logout()
+
+ def test_github_token(self):
+ user = ProfileFactory()
+ dev = DevProfileFactory()
+
+ # test that github settings are only availables for dev
+ self.client.force_login(user.user)
+ result = self.client.get(reverse("update-github"), follow=False)
+ self.assertEqual(result.status_code, 403)
+ result = self.client.post(reverse("remove-github"), follow=False)
+ self.assertEqual(result.status_code, 403)
+ self.client.logout()
+
+ # now, test the form
+ self.client.force_login(dev.user)
+ result = self.client.get(reverse("update-github"), follow=False)
+ self.assertEqual(result.status_code, 200)
+ result = self.client.post(
+ reverse("update-github"),
+ {
+ "github_token": "test",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+
+ # refresh
+ dev = Profile.objects.get(pk=dev.pk)
+ self.assertEqual(dev.github_token, "test")
+
+ # test the option to remove the token
+ result = self.client.post(reverse("remove-github"), follow=False)
+ self.assertEqual(result.status_code, 302)
+
+ # refresh
+ dev = Profile.objects.get(pk=dev.pk)
+ self.assertEqual(dev.github_token, "")
+
+ def test_markdown_help_settings(self):
+ user = ProfileFactory().user
+
+ # login and check that the Markdown help is displayed
+ self.client.force_login(user)
+ result = self.client.get(reverse("pages-index"), follow=False)
+ self.assertContains(result, 'data-show-markdown-help="true"')
+
+ # disable Markdown help
+ user.profile.show_markdown_help = False
+ user.profile.save()
+ result = self.client.get(reverse("pages-index"), follow=False)
+ self.assertContains(result, 'data-show-markdown-help="false"')
+
+ def test_old_smileys(self):
+ """Test the cookie"""
+
+ # NOTE: we have to use the "real" login and logout pages here
+ cookie_key = settings.ZDS_APP["member"]["old_smileys_cookie_key"]
+
+ profile_without_clem = ProfileFactory()
+ profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
+ self.assertFalse(profile_without_clem.use_old_smileys)
+
+ user_without_clem = profile_without_clem.user
+ profile_with_clem = ProfileFactory()
+ profile_with_clem.use_old_smileys = True
+ profile_with_clem.save()
+ user_with_clem = profile_with_clem.user
+
+ settings.ZDS_APP["member"]["old_smileys_allowed"] = True
+
+ # test that the cookie is set when connection
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": user_with_clem.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 302)
+ self.client.get(reverse("homepage"))
+
+ self.assertIn(cookie_key, self.client.cookies)
+ self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0)
+
+ # test that logout set the cookies expiration to 0 (= no more cookie)
+ self.client.post(reverse("member-logout"), follow=True)
+ self.client.get(reverse("homepage"))
+ self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
+
+ # test that user without the setting have the cookie with expiration 0 (= no cookie)
+ result = self.client.post(
+ reverse("member-login"),
+ {"username": user_without_clem.username, "password": "hostel77", "remember": "remember"},
+ follow=False,
+ )
+
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
+
+ # setting use_smileys sets the cookie
+ self.client.post(
+ reverse("update-member"),
+ {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": ["use_old_smileys"]},
+ )
+ self.client.get(reverse("homepage"))
+
+ profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
+ self.assertTrue(profile_without_clem.use_old_smileys)
+ self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0)
+
+ # ... and that not setting it removes the cookie
+ self.client.post(
+ reverse("update-member"), {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": []}
+ )
+ self.client.get(reverse("homepage"))
+
+ profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk)
+ self.assertFalse(profile_without_clem.use_old_smileys)
+ self.assertEqual(self.client.cookies[cookie_key]["expires"], 0)
diff --git a/zds/member/tests/views/tests_register.py b/zds/member/tests/views/tests_register.py
new file mode 100644
index 0000000000..54398d1c52
--- /dev/null
+++ b/zds/member/tests/views/tests_register.py
@@ -0,0 +1,392 @@
+import os
+from datetime import datetime
+from smtplib import SMTPException
+from unittest.mock import Mock
+
+from oauth2_provider.models import AccessToken, Application
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.core import mail
+from django.core.mail.backends.base import BaseEmailBackend
+from django.urls import reverse
+from django.utils.html import escape
+from django.test import TestCase, override_settings
+
+from zds.member.factories import ProfileFactory, UserFactory, StaffProfileFactory
+from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory
+from zds.member.models import KarmaNote, NewEmailProvider
+from zds.mp.models import PrivatePost, PrivateTopic
+from zds.member.models import TokenRegister, Ban
+from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory
+from zds.tutorialv2.models.database import PublishableContent, PublishedContent
+from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
+from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory
+from zds.forum.models import Topic, Post
+from zds.gallery.factories import GalleryFactory, UserGalleryFactory
+from zds.gallery.models import Gallery, UserGallery
+from zds.utils.models import CommentVote
+
+
+@override_for_contents()
+class TestRegister(TutorialTestMixin, TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_register(self):
+ """
+ To test user registration.
+ """
+
+ # register a new user.
+ result = self.client.post(
+ reverse("register-member"),
+ {
+ "username": "firm1",
+ "password": "flavour",
+ "password_confirm": "flavour",
+ "email": "firm1@zestedesavoir.com",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+
+ # check email has been sent.
+ self.assertEqual(len(mail.outbox), 1)
+
+ # check if the new user is well inactive.
+ user = User.objects.get(username="firm1")
+ self.assertFalse(user.is_active)
+
+ # make a request on the link which has been sent in mail to
+ # confirm the registration.
+ token = TokenRegister.objects.get(user=user)
+ result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False)
+ self.assertEqual(result.status_code, 200)
+
+ # check a new email hasn't been sent at the new user.
+ self.assertEqual(len(mail.outbox), 1)
+
+ # check if the new user is active.
+ self.assertTrue(User.objects.get(username="firm1").is_active)
+
+ def test_unregister(self):
+ """
+ To test that unregistering user is working.
+ """
+
+ # test not logged user can't unregister.
+ self.client.logout()
+ result = self.client.post(reverse("member-unregister"), follow=False)
+ self.assertEqual(result.status_code, 302)
+
+ # test logged user can unregister.
+ user = ProfileFactory()
+ self.client.force_login(user.user)
+ result = self.client.post(reverse("member-unregister"), follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(User.objects.filter(username=user.user.username).count(), 0)
+
+ # Attach a user at tutorials, articles, topics and private topics. After that,
+ # unregister this user and check that he is well removed in all contents.
+ user = ProfileFactory()
+ user2 = ProfileFactory()
+ alone_gallery = GalleryFactory()
+ UserGalleryFactory(gallery=alone_gallery, user=user.user)
+ shared_gallery = GalleryFactory()
+ UserGalleryFactory(gallery=shared_gallery, user=user.user)
+ UserGalleryFactory(gallery=shared_gallery, user=user2.user)
+ # first case : a published tutorial with only one author
+ published_tutorial_alone = PublishedContentFactory(type="TUTORIAL")
+ published_tutorial_alone.authors.add(user.user)
+ published_tutorial_alone.save()
+ # second case : a published tutorial with two authors
+ published_tutorial_2 = PublishedContentFactory(type="TUTORIAL")
+ published_tutorial_2.authors.add(user.user)
+ published_tutorial_2.authors.add(user2.user)
+ published_tutorial_2.save()
+ # third case : a private tutorial with only one author
+ writing_tutorial_alone = PublishableContentFactory(type="TUTORIAL")
+ writing_tutorial_alone.authors.add(user.user)
+ writing_tutorial_alone.save()
+ writing_tutorial_alone_galler_path = writing_tutorial_alone.gallery.get_gallery_path()
+ # fourth case : a private tutorial with at least two authors
+ writing_tutorial_2 = PublishableContentFactory(type="TUTORIAL")
+ writing_tutorial_2.authors.add(user.user)
+ writing_tutorial_2.authors.add(user2.user)
+ writing_tutorial_2.save()
+ self.client.force_login(self.staff)
+ # same thing for articles
+ published_article_alone = PublishedContentFactory(type="ARTICLE")
+ published_article_alone.authors.add(user.user)
+ published_article_alone.save()
+ published_article_2 = PublishedContentFactory(type="ARTICLE")
+ published_article_2.authors.add(user.user)
+ published_article_2.authors.add(user2.user)
+ published_article_2.save()
+ writing_article_alone = PublishableContentFactory(type="ARTICLE")
+ writing_article_alone.authors.add(user.user)
+ writing_article_alone.save()
+ writing_article_2 = PublishableContentFactory(type="ARTICLE")
+ writing_article_2.authors.add(user.user)
+ writing_article_2.authors.add(user2.user)
+ writing_article_2.save()
+ # beta content
+ beta_forum = ForumFactory(category=ForumCategoryFactory())
+ beta_content = BetaContentFactory(author_list=[user.user], forum=beta_forum)
+ beta_content_2 = BetaContentFactory(author_list=[user.user, user2.user], forum=beta_forum)
+ # about posts and topics
+ authored_topic = TopicFactory(author=user.user, forum=self.forum11, solved_by=user.user)
+ answered_topic = TopicFactory(author=user2.user, forum=self.forum11)
+ PostFactory(topic=answered_topic, author=user.user, position=2)
+ edited_answer = PostFactory(topic=answered_topic, author=user.user, position=3)
+ edited_answer.editor = user.user
+ edited_answer.save()
+
+ upvoted_answer = PostFactory(topic=answered_topic, author=user2.user, position=4)
+ upvoted_answer.like += 1
+ upvoted_answer.save()
+ CommentVote.objects.create(user=user.user, comment=upvoted_answer, positive=True)
+
+ private_topic = PrivateTopicFactory(author=user.user)
+ private_topic.participants.add(user2.user)
+ private_topic.save()
+ PrivatePostFactory(author=user.user, privatetopic=private_topic, position_in_topic=1)
+
+ # add API key
+ self.assertEqual(Application.objects.count(), 0)
+ self.assertEqual(AccessToken.objects.count(), 0)
+ api_application = Application()
+ api_application.client_id = "foobar"
+ api_application.user = user.user
+ api_application.client_type = "confidential"
+ api_application.authorization_grant_type = "password"
+ api_application.client_secret = "42"
+ api_application.save()
+ token = AccessToken()
+ token.user = user.user
+ token.token = "r@d0m"
+ token.application = api_application
+ token.expires = datetime.now()
+ token.save()
+ self.assertEqual(Application.objects.count(), 1)
+ self.assertEqual(AccessToken.objects.count(), 1)
+
+ # add a karma note and a sanction with this user
+ note = KarmaNote(moderator=user.user, user=user2.user, note="Good!", karma=5)
+ note.save()
+ ban = Ban(moderator=user.user, user=user2.user, type="Ban définitif", note="Test")
+ ban.save()
+
+ # login and unregister:
+ self.client.force_login(user.user)
+ result = self.client.post(reverse("member-unregister"), follow=False)
+ self.assertEqual(result.status_code, 302)
+
+ # check that the bot have taken authorship of tutorial:
+ self.assertEqual(published_tutorial_alone.authors.count(), 1)
+ self.assertEqual(
+ published_tutorial_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"]
+ )
+ self.assertFalse(os.path.exists(writing_tutorial_alone_galler_path))
+ self.assertEqual(published_tutorial_2.authors.count(), 1)
+ self.assertEqual(
+ published_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
+ )
+
+ # check that published tutorials remain published and accessible
+ self.assertIsNotNone(published_tutorial_2.public_version.get_prod_path())
+ self.assertTrue(os.path.exists(published_tutorial_2.public_version.get_prod_path()))
+ self.assertIsNotNone(published_tutorial_alone.public_version.get_prod_path())
+ self.assertTrue(os.path.exists(published_tutorial_alone.public_version.get_prod_path()))
+ self.assertEqual(
+ self.client.get(
+ reverse("tutorial:view", args=[published_tutorial_alone.pk, published_tutorial_alone.slug]),
+ follow=False,
+ ).status_code,
+ 200,
+ )
+ self.assertEqual(
+ self.client.get(
+ reverse("tutorial:view", args=[published_tutorial_2.pk, published_tutorial_2.slug]), follow=False
+ ).status_code,
+ 200,
+ )
+
+ # test that published articles remain accessible
+ self.assertTrue(os.path.exists(published_article_alone.public_version.get_prod_path()))
+ self.assertEqual(
+ self.client.get(
+ reverse("article:view", args=[published_article_alone.pk, published_article_alone.slug]), follow=True
+ ).status_code,
+ 200,
+ )
+ self.assertEqual(
+ self.client.get(
+ reverse("article:view", args=[published_article_2.pk, published_article_2.slug]), follow=True
+ ).status_code,
+ 200,
+ )
+
+ # check that the tutorial for which the author was alone does not exists anymore
+ self.assertEqual(PublishableContent.objects.filter(pk=writing_tutorial_alone.pk).count(), 0)
+ self.assertFalse(os.path.exists(writing_tutorial_alone.get_repo_path()))
+
+ # check that bot haven't take the authorship of the tuto with more than one author
+ self.assertEqual(writing_tutorial_2.authors.count(), 1)
+ self.assertEqual(
+ writing_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
+ )
+
+ # authorship for the article for which user was the only author
+ self.assertEqual(published_article_alone.authors.count(), 1)
+ self.assertEqual(
+ published_article_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"]
+ )
+ self.assertEqual(published_article_2.authors.count(), 1)
+
+ self.assertEqual(PublishableContent.objects.filter(pk=writing_article_alone.pk).count(), 0)
+ self.assertFalse(os.path.exists(writing_article_alone.get_repo_path()))
+
+ # not bot if another author:
+ self.assertEqual(
+ published_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
+ )
+ self.assertEqual(writing_article_2.authors.count(), 1)
+ self.assertEqual(
+ writing_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0
+ )
+
+ # topics, gallery and PMs:
+ self.assertEqual(Topic.objects.filter(author__username=user.user.username).count(), 0)
+ self.assertEqual(Topic.objects.filter(solved_by=user.user).count(), 0)
+ self.assertEqual(Topic.objects.filter(solved_by=self.anonymous).count(), 1)
+ self.assertEqual(Post.objects.filter(author__username=user.user.username).count(), 0)
+ self.assertEqual(Post.objects.filter(editor__username=user.user.username).count(), 0)
+ self.assertEqual(PrivatePost.objects.filter(author__username=user.user.username).count(), 0)
+ self.assertEqual(PrivateTopic.objects.filter(author__username=user.user.username).count(), 0)
+
+ self.assertIsNotNone(Topic.objects.get(pk=authored_topic.pk))
+ self.assertIsNotNone(PrivateTopic.objects.get(pk=private_topic.pk))
+ self.assertIsNotNone(Gallery.objects.get(pk=alone_gallery.pk))
+ self.assertEqual(alone_gallery.get_linked_users().count(), 1)
+ self.assertEqual(shared_gallery.get_linked_users().count(), 1)
+ self.assertEqual(UserGallery.objects.filter(user=user.user).count(), 0)
+ self.assertEqual(CommentVote.objects.filter(user=user.user, positive=True).count(), 0)
+ self.assertEqual(Post.objects.filter(pk=upvoted_answer.id).first().like, 0)
+
+ # zep 12, published contents and beta
+ self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_alone.pk).first())
+ self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_2.pk).first())
+ self.assertTrue(Topic.objects.get(pk=beta_content.beta_topic.pk).is_locked)
+ self.assertFalse(Topic.objects.get(pk=beta_content_2.beta_topic.pk).is_locked)
+
+ # check API
+ self.assertEqual(Application.objects.count(), 0)
+ self.assertEqual(AccessToken.objects.count(), 0)
+
+ # check that the karma note and the sanction were kept
+ self.assertTrue(KarmaNote.objects.filter(pk=note.pk).exists())
+ self.assertTrue(Ban.objects.filter(pk=ban.pk).exists())
+
+ def test_register_with_not_allowed_chars(self):
+ """
+ Test register account with not allowed chars
+ :return:
+ """
+ users = [
+ # empty username
+ {"username": "", "password": "flavour", "password_confirm": "flavour", "email": "firm1@zestedesavoir.com"},
+ # space after username
+ {
+ "username": "firm1 ",
+ "password": "flavour",
+ "password_confirm": "flavour",
+ "email": "firm1@zestedesavoir.com",
+ },
+ # space before username
+ {
+ "username": " firm1",
+ "password": "flavour",
+ "password_confirm": "flavour",
+ "email": "firm1@zestedesavoir.com",
+ },
+ # username with utf8mb4 chars
+ {
+ "username": " firm1",
+ "password": "flavour",
+ "password_confirm": "flavour",
+ "email": "firm1@zestedesavoir.com",
+ },
+ ]
+
+ for user in users:
+ result = self.client.post(reverse("register-member"), user, follow=False)
+ self.assertEqual(result.status_code, 200)
+ # check any email has been sent.
+ self.assertEqual(len(mail.outbox), 0)
+ # user doesn't exist
+ self.assertEqual(User.objects.filter(username=user["username"]).count(), 0)
+
+ def test_new_provider_with_new_account(self):
+ new_providers_count = NewEmailProvider.objects.count()
+
+ # register a new user
+ self.client.post(
+ reverse("register-member"),
+ {
+ "username": "new",
+ "password": "hostel77",
+ "password_confirm": "hostel77",
+ "email": "test@unknown-provider-register.com",
+ },
+ follow=False,
+ )
+
+ user = User.objects.get(username="new")
+ token = TokenRegister.objects.get(user=user)
+ self.client.get(token.get_absolute_url(), follow=False)
+
+ # A new provider object should have been created
+ self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count())
+
+
+mail_backend = Mock()
+
+
+class FakeBackend(BaseEmailBackend):
+ def send_messages(self, email_messages):
+ return mail_backend.send_messages(email_messages)
+
+
+@override_settings(EMAIL_BACKEND="zds.member.tests.views.tests_register.FakeBackend")
+class RegisterTest(TestCase):
+ def test_exception_on_mail(self):
+ def send_messages(messages):
+ print("message sent")
+ raise SMTPException(messages)
+
+ mail_backend.send_messages = send_messages
+
+ result = self.client.post(
+ reverse("register-member"),
+ {
+ "username": "firm1",
+ "password": "flavour",
+ "password_confirm": "flavour",
+ "email": "firm1@zestedesavoir.com",
+ },
+ follow=False,
+ )
+ self.assertEqual(result.status_code, 200)
+ self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8"))
diff --git a/zds/member/tests/views/tests_reports.py b/zds/member/tests/views/tests_reports.py
new file mode 100644
index 0000000000..6fd95bcf0f
--- /dev/null
+++ b/zds/member/tests/views/tests_reports.py
@@ -0,0 +1,53 @@
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.urls import reverse
+from django.test import TestCase
+
+from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+
+from zds.mp.models import PrivateTopic
+from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
+from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.utils.models import Alert
+
+
+@override_for_contents()
+class MemberTests(TutorialTestMixin, TestCase):
+ def setUp(self):
+ settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+ self.mas = ProfileFactory()
+ settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username
+ self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything")
+ self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything")
+ self.category1 = ForumCategoryFactory(position=1)
+ self.forum11 = ForumFactory(category=self.category1, position_in_category=1)
+ self.staff = StaffProfileFactory().user
+
+ self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"])
+ self.bot.save()
+
+ def test_profile_report(self):
+ profile = ProfileFactory()
+ self.client.logout()
+ alerts_count = Alert.objects.count()
+ # test that authentication is required to report a profile
+ result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(alerts_count, Alert.objects.count())
+ # login and check it doesn't work without reason
+ self.client.force_login(self.staff)
+ result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": ""}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(alerts_count, Alert.objects.count())
+ # add a reason and check it works
+ result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(alerts_count + 1, Alert.objects.count())
+ # test alert solving
+ alert = Alert.objects.latest("pubdate")
+ pm_count = PrivateTopic.objects.count()
+ result = self.client.post(reverse("solve-profile-alert", args=[alert.pk]), {"text": "ok"}, follow=False)
+ self.assertEqual(result.status_code, 302)
+ alert = Alert.objects.get(pk=alert.pk) # refresh
+ self.assertTrue(alert.solved)
+ self.assertEqual(pm_count + 1, PrivateTopic.objects.count())
diff --git a/zds/member/urls.py b/zds/member/urls.py
index fb17bfbd93..5a54a8752c 100644
--- a/zds/member/urls.py
+++ b/zds/member/urls.py
@@ -1,35 +1,23 @@
from django.urls import re_path, path
-from zds.member.views import (
- MemberList,
- MemberDetail,
+from zds.member.views import MemberList
+from zds.member.views.profile import (
UpdateMember,
UpdateGitHubToken,
remove_github_token,
UpdateAvatarMember,
UpdatePasswordMember,
UpdateUsernameEmailMember,
- RegisterView,
- SendValidationEmailView,
+ redirect_old_profile_to_new,
+)
+from zds.member.views.moderation import (
modify_karma,
- modify_profile,
settings_mini_profile,
member_from_ip,
- settings_promote,
- login_view,
- logout_view,
- forgot_password,
- new_password,
- activate_account,
- generate_token_account,
- unregister,
- warning_unregister,
- BannedEmailProvidersList,
- NewEmailProvidersList,
- AddBannedEmailProvider,
- remove_banned_email_provider,
- check_new_email_provider,
- MembersWithProviderList,
+ modify_profile,
+)
+from zds.member.views.login import login_view, logout_view
+from zds.member.views.hats import (
HatsSettings,
RequestedHatsList,
HatRequestDetail,
@@ -39,10 +27,27 @@
HatsList,
HatDetail,
SolvedHatRequestsList,
- CreateProfileReportView,
- SolveProfileReportView,
- redirect_old_profile_to_new,
)
+from zds.member.views.emailproviders import (
+ BannedEmailProvidersList,
+ NewEmailProvidersList,
+ AddBannedEmailProvider,
+ remove_banned_email_provider,
+ check_new_email_provider,
+ MembersWithProviderList,
+)
+from zds.member.views.register import (
+ RegisterView,
+ SendValidationEmailView,
+ unregister,
+ warning_unregister,
+ activate_account,
+ generate_token_account,
+)
+from zds.member.views.password_recovery import forgot_password, new_password
+from zds.member.views.admin import settings_promote
+from zds.member.views.reports import CreateProfileReportView, SolveProfileReportView
+
urlpatterns = [
# list
diff --git a/zds/member/views.py b/zds/member/views.py
deleted file mode 100644
index ac0cd24f26..0000000000
--- a/zds/member/views.py
+++ /dev/null
@@ -1,1542 +0,0 @@
-import ipaddress
-import uuid
-from datetime import datetime, timedelta
-from urllib.parse import unquote
-
-from oauth2_provider.models import AccessToken
-
-from django.conf import settings
-from django.contrib import messages
-from django.contrib.auth import authenticate, login, logout
-from django.contrib.auth.decorators import login_required, permission_required
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.contrib.auth.models import User, Group
-from django.template.context_processors import csrf
-from django.core.exceptions import PermissionDenied
-from django.core.mail import EmailMultiAlternatives
-from django.urls import reverse, reverse_lazy, resolve, Resolver404, NoReverseMatch
-from django.db import transaction
-from django.db.models import Q
-from django.http import Http404, HttpResponseBadRequest, StreamingHttpResponse
-from django.shortcuts import redirect, render, get_object_or_404
-from django.template.loader import render_to_string
-from django.utils.decorators import method_decorator
-from django.utils.text import format_lazy
-from django.utils.translation import gettext_lazy as _
-from django.utils.translation import gettext as __
-from django.views.decorators.http import require_POST
-from django.views.generic import DetailView, UpdateView, CreateView, FormView, View
-
-from zds.forum.models import Topic, TopicRead
-from zds.gallery.forms import ImageAsAvatarForm
-from zds.gallery.models import UserGallery
-from zds.member import NEW_ACCOUNT, EMAIL_EDIT
-from zds.member.commons import (
- ProfileCreate,
- TemporaryReadingOnlySanction,
- ReadingOnlySanction,
- DeleteReadingOnlySanction,
- TemporaryBanSanction,
- BanSanction,
- DeleteBanSanction,
- TokenGenerator,
-)
-from zds.member.decorator import can_write_and_read_now, PermissionRequiredMixin
-from zds.member.forms import (
- LoginForm,
- MiniProfileForm,
- ProfileForm,
- RegisterForm,
- ChangePasswordForm,
- ChangeUserForm,
- NewPasswordForm,
- PromoteMemberForm,
- KarmaForm,
- UsernameAndEmailForm,
- GitHubTokenForm,
- BannedEmailProviderForm,
- HatRequestForm,
-)
-from zds.member.models import (
- Profile,
- TokenForgotPassword,
- TokenRegister,
- KarmaNote,
- Ban,
- BannedEmailProvider,
- NewEmailProvider,
- set_old_smileys_cookie,
- remove_old_smileys_cookie,
-)
-from zds.mp.models import PrivatePost, PrivateTopic
-from zds.notification.models import TopicAnswerSubscription, NewPublicationSubscription
-from zds.pages.models import GroupContact
-from zds.tutorialv2.models import CONTENT_TYPES
-from zds.tutorialv2.models.database import PublishedContent, PickListOperation, ContentContribution, ContentReaction
-from zds.utils.models import (
- Comment,
- CommentVote,
- Alert,
- CommentEdit,
- Hat,
- HatRequest,
- get_hat_from_settings,
- get_hat_to_add,
-)
-from zds.utils.mps import send_mp
-from zds.utils.paginator import ZdSPagingListView
-from zds.utils.templatetags.pluralize_fr import pluralize_fr
-from zds.utils.tokens import generate_token
-import logging
-
-
-class MemberList(ZdSPagingListView):
- """Display the list of registered users."""
-
- context_object_name = "members"
- paginate_by = settings.ZDS_APP["member"]["members_per_page"]
- template_name = "member/index.html"
-
- def get_queryset(self):
- self.queryset = Profile.objects.contactable_members()
- return super().get_queryset()
-
-
-class MemberDetail(DetailView):
- """Display details about a profile."""
-
- context_object_name = "usr"
- model = User
- template_name = "member/profile.html"
-
- def get_object(self, queryset=None):
- # Use unquote to accept twicely quoted URLs (for instance in MPs
- # sent through emarkdown parser).
- return get_object_or_404(User, username=unquote(self.kwargs["user_name"]))
-
- def get_summaries(self, profile):
- """
- Returns a summary of this profile's activity, as a list of list of tuples.
- Each first-level list item is an activity category (e.g. contents, forums, etc.)
- Each second-level list item is a stat in this activity category.
- Each tuple is (link url, count, displayed name of the item), where the link url can be None if it's not a link.
-
- :param profile: The profile.
- :return: The summary data.
- """
- summaries = []
-
- if self.request.user.has_perm("member.change_post"):
- count_post = profile.get_post_count_as_staff()
- else:
- count_post = profile.get_post_count()
-
- count_topic = profile.get_topic_count()
- count_followed_topic = profile.get_followed_topic_count()
- count_tutorials = profile.get_public_tutos().count()
- count_articles = profile.get_public_articles().count()
- count_opinions = profile.get_public_opinions().count()
-
- summary = []
- if count_tutorials + count_articles + count_opinions == 0:
- summary.append((None, 0, __("Aucun contenu publié")))
-
- if count_tutorials > 0:
- summary.append(
- (
- reverse_lazy("tutorial:find-tutorial", args=(profile.user.username,)),
- count_tutorials,
- __("tutoriel{}").format(pluralize_fr(count_tutorials)),
- )
- )
- if count_articles > 0:
- summary.append(
- (
- reverse_lazy("article:find-article", args=(profile.user.username,)),
- count_articles,
- __("article{}").format(pluralize_fr(count_articles)),
- )
- )
- if count_opinions > 0:
- summary.append(
- (
- reverse_lazy("opinion:find-opinion", args=(profile.user.username,)),
- count_opinions,
- __("billet{}").format(pluralize_fr(count_opinions)),
- )
- )
- summaries.append(summary)
-
- summary = []
- if count_post > 0:
- summary.append(
- (
- reverse_lazy("post-find", args=(profile.user.pk,)),
- count_post,
- __("message{}").format(pluralize_fr(count_post)),
- )
- )
- else:
- summary.append((None, 0, __("Aucun message")))
- if count_topic > 0:
- summary.append(
- (
- reverse_lazy("topic-find", args=(profile.user.pk,)),
- count_topic,
- __("sujet{} créé{}").format(pluralize_fr(count_topic), pluralize_fr(count_topic)),
- )
- )
- user = self.request.user
- is_user_profile = user.is_authenticated and User.objects.get(pk=user.pk).profile == profile
- if count_followed_topic > 0 and is_user_profile:
- summary.append(
- (
- reverse_lazy("followed-topic-find"),
- count_followed_topic,
- __("sujet{} suivi{}").format(
- pluralize_fr(count_followed_topic), pluralize_fr(count_followed_topic)
- ),
- )
- )
-
- summaries.append(summary)
-
- return summaries
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- usr = context["usr"]
- profile = usr.profile
- context["profile"] = profile
- context["topics"] = list(Topic.objects.last_topics_of_a_member(usr, self.request.user))
- followed_query_set = TopicAnswerSubscription.objects.get_objects_followed_by(self.request.user.id)
- followed_topics = list(set(followed_query_set) & set(context["topics"]))
- for topic in context["topics"]:
- topic.is_followed = topic in followed_topics
- context["articles"] = PublishedContent.objects.last_articles_of_a_member_loaded(usr)
- context["opinions"] = PublishedContent.objects.last_opinions_of_a_member_loaded(usr)
- context["tutorials"] = PublishedContent.objects.last_tutorials_of_a_member_loaded(usr)
- context["articles_and_tutorials"] = PublishedContent.objects.last_tutorials_and_articles_of_a_member_loaded(usr)
- context["topic_read"] = TopicRead.objects.list_read_topic_pk(self.request.user, context["topics"])
- context["subscriber_count"] = NewPublicationSubscription.objects.get_subscriptions(self.object).count()
- context["contribution_articles_count"] = (
- ContentContribution.objects.filter(
- user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[1]["name"]
- )
- .values_list("content", flat=True)
- .distinct()
- .count()
- )
- context["contribution_tutorials_count"] = (
- ContentContribution.objects.filter(
- user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[0]["name"]
- )
- .values_list("content", flat=True)
- .distinct()
- .count()
- )
- context["content_reactions_count"] = ContentReaction.objects.filter(author=usr).count()
-
- if self.request.user.has_perm("member.change_profile"):
- sanctions = list(Ban.objects.filter(user=usr).select_related("moderator"))
- notes = list(KarmaNote.objects.filter(user=usr).select_related("moderator"))
- actions = sanctions + notes
- actions.sort(key=lambda action: action.pubdate)
- actions.reverse()
- context["actions"] = actions
- context["karmaform"] = KarmaForm(profile)
- context["alerts"] = profile.alerts_on_this_profile.all().order_by("-pubdate")
- context["has_unsolved_alerts"] = profile.alerts_on_this_profile.filter(solved=False).exists()
-
- context["summaries"] = self.get_summaries(profile)
- return context
-
-
-def redirect_old_profile_to_new(request, user_name):
- user = get_object_or_404(User, username=user_name)
- return redirect(user.profile, permanent=True)
-
-
-class UpdateMember(UpdateView):
- """Update a profile."""
-
- form_class = ProfileForm
- template_name = "member/settings/profile.html"
-
- @method_decorator(login_required)
- def dispatch(self, *args, **kwargs):
- return super().dispatch(*args, **kwargs)
-
- def get_object(self, queryset=None):
- return get_object_or_404(Profile, user=self.request.user)
-
- def get_form(self, form_class=ProfileForm):
- profile = self.get_object()
- form = form_class(
- initial={
- "biography": profile.biography,
- "site": profile.site,
- "avatar_url": profile.avatar_url,
- "show_sign": profile.show_sign,
- "is_hover_enabled": profile.is_hover_enabled,
- "use_old_smileys": profile.use_old_smileys,
- "allow_temp_visual_changes": profile.allow_temp_visual_changes,
- "show_markdown_help": profile.show_markdown_help,
- "email_for_answer": profile.email_for_answer,
- "email_for_new_mp": profile.email_for_new_mp,
- "sign": profile.sign,
- "licence": profile.licence,
- }
- )
-
- return form
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.POST)
-
- if "preview" in request.POST and request.is_ajax():
- content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")})
- return StreamingHttpResponse(content)
-
- if form.is_valid():
- return self.form_valid(form)
-
- return render(request, self.template_name, {"form": form})
-
- def form_valid(self, form):
- profile = self.get_object()
- self.update_profile(profile, form)
- self.save_profile(profile)
-
- response = redirect(self.get_success_url())
- set_old_smileys_cookie(response, profile)
- return response
-
- def update_profile(self, profile, form):
- cleaned_data_options = form.cleaned_data.get("options")
- profile.biography = form.data["biography"]
- profile.site = form.data["site"]
- profile.show_sign = "show_sign" in cleaned_data_options
- profile.is_hover_enabled = "is_hover_enabled" in cleaned_data_options
- profile.use_old_smileys = "use_old_smileys" in cleaned_data_options
- profile.allow_temp_visual_changes = "allow_temp_visual_changes" in cleaned_data_options
- profile.show_markdown_help = "show_markdown_help" in cleaned_data_options
- profile.email_for_answer = "email_for_answer" in cleaned_data_options
- profile.email_for_new_mp = "email_for_new_mp" in cleaned_data_options
- profile.avatar_url = form.data["avatar_url"]
- profile.sign = form.data["sign"]
- profile.licence = form.cleaned_data["licence"]
-
- def get_success_url(self):
- return reverse("update-member")
-
- def save_profile(self, profile):
- try:
- profile.save()
- profile.user.save()
- except Profile.DoesNotExist:
- messages.error(self.request, self.get_error_message())
- return redirect(reverse("update-member"))
- messages.success(self.request, self.get_success_message())
-
- def get_success_message(self):
- return _("Le profil a correctement été mis à jour.")
-
- def get_error_message(self):
- return _("Une erreur est survenue.")
-
-
-class UpdateGitHubToken(UpdateView):
- """Update the GitHub token."""
-
- form_class = GitHubTokenForm
- template_name = "member/settings/github.html"
-
- @method_decorator(login_required)
- def dispatch(self, request, *args, **kwargs):
- if not request.user.profile.is_dev():
- raise PermissionDenied
- return super().dispatch(request, *args, **kwargs)
-
- def get_object(self, queryset=None):
- return get_object_or_404(Profile, user=self.request.user)
-
- def get_form(self, form_class=GitHubTokenForm):
- return form_class()
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.POST)
-
- if form.is_valid():
- return self.form_valid(form)
-
- return render(request, self.template_name, {"form": form})
-
- def form_valid(self, form):
- profile = self.get_object()
- profile.github_token = form.data["github_token"]
- profile.save()
- messages.success(self.request, self.get_success_message())
-
- return redirect(self.get_success_url())
-
- def get_success_url(self):
- return reverse("update-github")
-
- def get_success_message(self):
- return _("Votre token GitHub a été mis à jour.")
-
- def get_error_message(self):
- return _("Une erreur est survenue.")
-
-
-@require_POST
-@login_required
-def remove_github_token(request):
- """Remove the current user token."""
-
- profile = get_object_or_404(Profile, user=request.user)
- if not profile.is_dev():
- raise PermissionDenied
-
- profile.github_token = ""
- profile.save()
-
- messages.success(request, _("Votre token GitHub a été supprimé."))
- return redirect("update-github")
-
-
-class UpdateAvatarMember(UpdateMember):
- """Update the avatar of a logged in user."""
-
- form_class = ImageAsAvatarForm
-
- def get_success_url(self):
- profile = self.get_object()
-
- return reverse("member-detail", args=[profile.user.username])
-
- def get_form(self, form_class=ImageAsAvatarForm):
- return form_class(self.request.POST)
-
- def update_profile(self, profile, form):
- profile.avatar_url = form.data["avatar_url"]
-
- def get_success_message(self):
- return _("L'avatar a correctement été mis à jour.")
-
-
-class UpdatePasswordMember(UpdateMember):
- """Password-related user settings."""
-
- form_class = ChangePasswordForm
- template_name = "member/settings/account.html"
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.user, request.POST)
-
- if form.is_valid():
- return self.form_valid(form)
-
- return render(request, self.template_name, {"form": form})
-
- def get_form(self, form_class=ChangePasswordForm):
- return form_class(self.request.user)
-
- def update_profile(self, profile, form):
- profile.user.set_password(form.data["password_new"])
-
- def get_success_message(self):
- return _("Le mot de passe a correctement été mis à jour.")
-
- def get_success_url(self):
- return reverse("update-password-member")
-
-
-class UpdateUsernameEmailMember(UpdateMember):
- """Settings related to username and email."""
-
- form_class = ChangeUserForm
- template_name = "member/settings/user.html"
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.user, request.POST)
-
- if form.is_valid():
- return self.form_valid(form)
-
- return render(request, self.template_name, {"form": form})
-
- def get_form(self, form_class=ChangeUserForm):
- return form_class(self.request.user)
-
- def update_profile(self, profile, form):
- profile.show_email = "show_email" in form.cleaned_data.get("options")
- new_username = form.cleaned_data.get("username")
- previous_username = form.cleaned_data.get("previous_username")
- new_email = form.cleaned_data.get("email")
- previous_email = form.cleaned_data.get("previous_email")
- if new_username and new_username != previous_username:
- # Add a karma message for the staff
- bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
- KarmaNote(
- user=profile.user,
- moderator=bot,
- note=_("{} s'est renommé {}").format(profile.user.username, new_username),
- karma=0,
- ).save()
- # Change the username
- profile.user.username = new_username
- # update skeleton
- profile.username_skeleton = Profile.find_username_skeleton(new_username)
- if new_email and new_email != previous_email:
- profile.user.email = new_email
- # Create an alert for the staff if it's a new provider
- provider = provider = new_email.split("@")[-1].lower()
- if (
- not NewEmailProvider.objects.filter(provider=provider).exists()
- and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=profile.user.pk).exists()
- ):
- NewEmailProvider.objects.create(user=profile.user, provider=provider, use=EMAIL_EDIT)
-
- def get_success_url(self):
- profile = self.get_object()
-
- return profile.get_absolute_url()
-
-
-class RegisterView(CreateView, ProfileCreate, TokenGenerator):
- """Create a profile."""
-
- form_class = RegisterForm
- template_name = "member/register/index.html"
-
- def dispatch(self, *args, **kwargs):
- return super().dispatch(*args, **kwargs)
-
- def get_object(self, queryset=None):
- return get_object_or_404(Profile, user=self.request.user)
-
- def get_form(self, form_class=RegisterForm):
- return form_class()
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.POST)
-
- if form.is_valid():
- return self.form_valid(form)
- return render(request, self.template_name, {"form": form})
-
- def form_valid(self, form):
- profile = self.create_profile(form.data)
- profile.last_ip_address = get_client_ip(self.request)
- profile.username_skeleton = Profile.find_username_skeleton(profile.user.username)
- self.save_profile(profile)
- token = self.generate_token(profile.user)
- try:
- self.send_email(token, profile.user)
- except Exception as e:
- logging.getLogger(__name__).warning("Mail not sent", exc_info=e)
- messages.warning(self.request, _("Impossible d'envoyer l'email."))
- self.object = None
- return self.form_invalid(form)
- return render(self.request, self.get_success_template())
-
- def get_success_template(self):
- return "member/register/success.html"
-
-
-class SendValidationEmailView(FormView, TokenGenerator):
- """Send a validation email on demand."""
-
- form_class = UsernameAndEmailForm
- template_name = "member/register/send_validation_email.html"
-
- usr = None
-
- def get_user(self, username, email):
-
- if username:
- self.usr = get_object_or_404(User, username=username)
-
- elif email:
- self.usr = get_object_or_404(User, email=email)
-
- def get_form(self, form_class=UsernameAndEmailForm):
- return form_class()
-
- def post(self, request, *args, **kwargs):
- form = self.form_class(request.POST)
-
- if form.is_valid():
- # Fetch the user
- self.get_user(form.data["username"], form.data["email"])
-
- # User should not already be active
- if not self.usr.is_active:
- return self.form_valid(form)
- else:
- if form.data["username"]:
- form.errors["username"] = form.error_class([self.get_error_message()])
- else:
- form.errors["email"] = form.error_class([self.get_error_message()])
-
- return render(request, self.template_name, {"form": form})
-
- def form_valid(self, form):
- # Delete old token
- token = TokenRegister.objects.filter(user=self.usr)
- if token.count() >= 1:
- token.all().delete()
-
- # Generate new token and send email
- token = self.generate_token(self.usr)
- try:
- self.send_email(token, self.usr)
- except Exception as e:
- logging.getLogger(__name__).warning("Mail not sent", exc_info=e)
- messages.warning(_("Impossible d'envoyer l'email."))
- return self.form_invalid(form)
-
- return render(self.request, self.get_success_template())
-
- def get_success_template(self):
- return "member/register/send_validation_email_success.html"
-
- def get_error_message(self):
- return _("Le compte est déjà activé.")
-
-
-@login_required
-def warning_unregister(request):
- """
- Display a warning page showing what will happen when the user
- unregisters.
- """
- return render(request, "member/settings/unregister.html", {"user": request.user})
-
-
-@login_required
-@require_POST
-@transaction.atomic
-def unregister(request):
- """Allow members to unregister."""
-
- anonymous = get_object_or_404(User, username=settings.ZDS_APP["member"]["anonymous_account"])
- external = get_object_or_404(User, username=settings.ZDS_APP["member"]["external_account"])
- current = request.user
- # Nota : as of v21 all about content paternity is held by a proper receiver in zds.tutorialv2.models.database
- PickListOperation.objects.filter(staff_user=current).update(staff_user=anonymous)
- PickListOperation.objects.filter(canceler_user=current).update(canceler_user=anonymous)
- # Comments likes / dislikes
- votes = CommentVote.objects.filter(user=current)
- for vote in votes:
- if vote.positive:
- vote.comment.like -= 1
- else:
- vote.comment.dislike -= 1
- vote.comment.save()
- votes.delete()
- # All contents anonymization
- Comment.objects.filter(author=current).update(author=anonymous)
- PrivatePost.objects.filter(author=current).update(author=anonymous)
- CommentEdit.objects.filter(editor=current).update(editor=anonymous)
- CommentEdit.objects.filter(deleted_by=current).update(deleted_by=anonymous)
- # Karma notes, alerts and sanctions anonymization (to keep them)
- KarmaNote.objects.filter(moderator=current).update(moderator=anonymous)
- Ban.objects.filter(moderator=current).update(moderator=anonymous)
- Alert.objects.filter(author=current).update(author=anonymous)
- Alert.objects.filter(moderator=current).update(moderator=anonymous)
- BannedEmailProvider.objects.filter(moderator=current).update(moderator=anonymous)
- # Solved hat requests anonymization
- HatRequest.objects.filter(moderator=current).update(moderator=anonymous)
- # In case current user has been moderator in the past
- Comment.objects.filter(editor=current).update(editor=anonymous)
- for topic in PrivateTopic.objects.filter(Q(author=current) | Q(participants__in=[current])):
- if topic.one_participant_remaining():
- topic.delete()
- else:
- topic.remove_participant(current)
- topic.save()
- Topic.objects.filter(solved_by=current).update(solved_by=anonymous)
- Topic.objects.filter(author=current).update(author=anonymous)
-
- # Any content exclusively owned by the unregistering member will
- # be deleted just before the User object (using a pre_delete
- # receiver).
- #
- # Regarding galleries, there are two cases:
- #
- # - "personal galleries" with one owner (the unregistering
- # user). The user's ownership is removed and replaced by an
- # anonymous user in order not to lost the gallery.
- #
- # - "personal galleries" with many other owners. It is safe to
- # remove the user's ownership, the gallery won't be lost.
-
- galleries = UserGallery.objects.filter(user=current)
- for gallery in galleries:
- if gallery.gallery.get_linked_users().count() == 1:
- anonymous_gallery = UserGallery()
- anonymous_gallery.user = external
- anonymous_gallery.mode = "w"
- anonymous_gallery.gallery = gallery.gallery
- anonymous_gallery.save()
- galleries.delete()
-
- # Remove API access (tokens + applications)
- for token in AccessToken.objects.filter(user=current):
- token.revoke()
-
- logout(request)
- User.objects.filter(pk=current.pk).delete()
- return redirect(reverse("homepage"))
-
-
-@require_POST
-@can_write_and_read_now
-@login_required
-@permission_required("member.change_profile", raise_exception=True)
-@transaction.atomic
-def modify_profile(request, user_pk):
- """Modify the sanction of a user if there is a POST request."""
-
- profile = get_object_or_404(Profile, user__pk=user_pk)
- if profile.is_private():
- raise PermissionDenied
- if request.user.profile == profile:
- messages.error(request, _("Vous ne pouvez pas vous sanctionner vous-même !"))
- raise PermissionDenied
-
- if "ls" in request.POST:
- state = ReadingOnlySanction(request.POST)
- elif "ls-temp" in request.POST:
- state = TemporaryReadingOnlySanction(request.POST)
- elif "ban" in request.POST:
- state = BanSanction(request.POST)
- elif "ban-temp" in request.POST:
- state = TemporaryBanSanction(request.POST)
- elif "un-ls" in request.POST:
- state = DeleteReadingOnlySanction(request.POST)
- else:
- # un-ban
- state = DeleteBanSanction(request.POST)
-
- try:
- ban = state.get_sanction(request.user, profile.user)
- except ValueError:
- raise HttpResponseBadRequest
-
- state.apply_sanction(profile, ban)
-
- if "un-ls" in request.POST or "un-ban" in request.POST:
- msg = state.get_message_unsanction()
- else:
- msg = state.get_message_sanction()
-
- msg = msg.format(
- ban.user, ban.moderator, ban.type, state.get_detail(), ban.note, settings.ZDS_APP["site"]["literal_name"]
- )
-
- state.notify_member(ban, msg)
- return redirect(profile.get_absolute_url())
-
-
-# Settings for public profile
-
-
-@can_write_and_read_now
-@login_required
-@permission_required("member.change_profile", raise_exception=True)
-def settings_mini_profile(request, user_name):
- """Minimal settings of users for staff."""
-
- # Extra information about the current user
- profile = get_object_or_404(Profile, user__username=user_name)
- if request.method == "POST":
- form = MiniProfileForm(request.POST)
- data = {"form": form, "profile": profile}
- if form.is_valid():
- profile.biography = form.data["biography"]
- profile.site = form.data["site"]
- profile.avatar_url = form.data["avatar_url"]
- profile.sign = form.data["sign"]
-
- # Save profile and redirect user to the settings page
- # with a message indicating the operation state.
-
- try:
- profile.save()
- except:
- messages.error(request, _("Une erreur est survenue."))
- return redirect(reverse("member-settings-mini-profile"))
-
- messages.success(request, _("Le profil a correctement été mis à jour."))
- return redirect(reverse("member-detail", args=[profile.user.username]))
- else:
- return render(request, "member/settings/profile.html", data)
- else:
- form = MiniProfileForm(
- initial={
- "biography": profile.biography,
- "site": profile.site,
- "avatar_url": profile.avatar_url,
- "sign": profile.sign,
- }
- )
- data = {"form": form, "profile": profile}
- messages.warning(
- request,
- _(
- "Le profil que vous éditez n'est pas le vôtre. "
- "Soyez encore plus prudent lors de l'édition de celui-ci !"
- ),
- )
- return render(request, "member/settings/profile.html", data)
-
-
-class NewEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
- permissions = ["member.change_bannedemailprovider"]
- paginate_by = settings.ZDS_APP["member"]["providers_per_page"]
-
- model = NewEmailProvider
- context_object_name = "providers"
- template_name = "member/admin/new_email_providers.html"
- queryset = NewEmailProvider.objects.select_related("user").select_related("user__profile").order_by("-date")
-
-
-@require_POST
-@login_required
-@permission_required("member.change_bannedemailprovider", raise_exception=True)
-def check_new_email_provider(request, provider_pk):
- """Remove an alert about a new provider."""
-
- provider = get_object_or_404(NewEmailProvider, pk=provider_pk)
- if "ban" in request.POST and not BannedEmailProvider.objects.filter(provider=provider.provider).exists():
- BannedEmailProvider.objects.create(provider=provider.provider, moderator=request.user)
- provider.delete()
-
- messages.success(request, _("Action effectuée."))
- return redirect("new-email-providers")
-
-
-class BannedEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
- """List the banned email providers."""
-
- permissions = ["member.change_bannedemailprovider"]
- paginate_by = settings.ZDS_APP["member"]["providers_per_page"]
-
- model = BannedEmailProvider
- context_object_name = "providers"
- template_name = "member/admin/banned_email_providers.html"
- queryset = (
- BannedEmailProvider.objects.select_related("moderator").select_related("moderator__profile").order_by("-date")
- )
-
-
-class MembersWithProviderList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
- """List users using a banned email provider."""
-
- permissions = ["member.change_bannedemailprovider"]
- paginate_by = settings.ZDS_APP["member"]["members_per_page"]
-
- model = User
- context_object_name = "members"
- template_name = "member/admin/members_with_provider.html"
-
- def get_object(self):
- return get_object_or_404(BannedEmailProvider, pk=self.kwargs["provider_pk"])
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["provider"] = self.get_object()
- return context
-
- def get_queryset(self):
- provider = self.get_object()
- return (
- Profile.objects.select_related("user")
- .order_by("-last_visit")
- .filter(user__email__icontains=f"@{provider.provider}")
- )
-
-
-class AddBannedEmailProvider(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
- """Add an email provider to the banned list."""
-
- permissions = ["member.change_bannedemailprovider"]
-
- model = BannedEmailProvider
- template_name = "member/admin/add_banned_email_provider.html"
- form_class = BannedEmailProviderForm
- success_url = reverse_lazy("banned-email-providers")
-
- def form_valid(self, form):
- form.instance.moderator = self.request.user
- messages.success(self.request, _("Le fournisseur a été banni."))
- return super().form_valid(form)
-
-
-@require_POST
-@login_required
-@permission_required("member.change_bannedemailprovider", raise_exception=True)
-def remove_banned_email_provider(request, provider_pk):
- """Unban an email provider."""
-
- provider = get_object_or_404(BannedEmailProvider, pk=provider_pk)
- provider.delete()
-
- messages.success(request, _("Le fournisseur « {} » a été débanni.").format(provider.provider))
- return redirect("banned-email-providers")
-
-
-class HatsList(ZdSPagingListView):
- """Display the list of hats."""
-
- context_object_name = "hats"
- paginate_by = settings.ZDS_APP["member"]["hats_per_page"]
- template_name = "member/hats.html"
- queryset = (
- Hat.objects.order_by("name")
- .select_related("group")
- .prefetch_related("group__user_set")
- .prefetch_related("group__user_set__profile")
- .prefetch_related("profile_set")
- .prefetch_related("profile_set__user")
- )
-
-
-class HatDetail(DetailView):
- model = Hat
- context_object_name = "hat"
- template_name = "member/hat.html"
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- hat = context["hat"]
- if self.request.user.is_authenticated:
- context["is_required"] = HatRequest.objects.filter(
- user=self.request.user, hat__iexact=hat.name, is_granted__isnull=True
- ).exists()
- if hat.group:
- context["users"] = hat.group.user_set.select_related("profile")
- try:
- context["groupcontact"] = hat.group.groupcontact
- except GroupContact.DoesNotExist:
- context["groupcontact"] = None # group not displayed on contact page
- else:
- context["users"] = [p.user for p in hat.profile_set.select_related("user")]
- return context
-
-
-class HatsSettings(LoginRequiredMixin, CreateView):
- model = HatRequest
- template_name = "member/settings/hats.html"
- form_class = HatRequestForm
-
- def get_initial(self):
- initial = super().get_initial()
- if "ask" in self.request.GET:
- try:
- hat = Hat.objects.get(pk=int(self.request.GET["ask"]))
- initial["hat"] = hat.name
- except (ValueError, Hat.DoesNotExist):
- pass
- return initial
-
- def post(self, request, *args, **kwargs):
- if "preview" in request.POST and request.is_ajax():
- content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")})
- return StreamingHttpResponse(content)
-
- return super().post(request, *args, **kwargs)
-
- def form_valid(self, form):
- form.instance.user = self.request.user
- messages.success(self.request, _("Votre demande a bien été envoyée."))
- return super().form_valid(form)
-
- def get_success_url(self):
- # To remove #send-request HTML-anchor.
- return "{}#".format(reverse("hats-settings"))
-
-
-class RequestedHatsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
- permissions = ["utils.change_hat"]
- paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"]
-
- model = HatRequest
- context_object_name = "requests"
- template_name = "member/admin/requested_hats.html"
- queryset = (
- HatRequest.objects.filter(is_granted__isnull=True)
- .select_related("user")
- .select_related("user__profile")
- .order_by("-date")
- )
-
-
-class SolvedHatRequestsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
- permissions = ["utils.change_hat"]
- paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"]
-
- model = HatRequest
- context_object_name = "requests"
- template_name = "member/admin/solved_hat_requests.html"
- queryset = (
- HatRequest.objects.filter(is_granted__isnull=False)
- .select_related("user")
- .select_related("user__profile")
- .select_related("moderator")
- .select_related("moderator__profile")
- .order_by("-solved_at")
- )
-
-
-class HatRequestDetail(LoginRequiredMixin, DetailView):
- model = HatRequest
- context_object_name = "hat_request"
- template_name = "member/admin/hat_request.html"
-
- def get_object(self, queryset=None):
- request = super().get_object()
- if request.user != self.request.user and not self.request.user.has_perm("utils.change_hat"):
- raise PermissionDenied
- return request
-
-
-@require_POST
-@login_required
-@permission_required("utils.change_hat", raise_exception=True)
-@transaction.atomic
-def solve_hat_request(request, request_pk):
- """
- Solve a hat request by granting or denying the requested hat
- according to moderator's decision.
- """
-
- hat_request = get_object_or_404(HatRequest, pk=request_pk)
-
- if hat_request.is_granted is not None:
- raise PermissionDenied
-
- try:
- hat_request.solve(
- "grant" in request.POST, request.user, request.POST.get("comment", ""), request.POST.get("hat", None)
- )
- messages.success(request, _("La demande a été résolue."))
- return redirect("requested-hats")
- except ValueError as e:
- messages.error(request, str(e))
- return redirect(hat_request.get_absolute_url())
-
-
-@require_POST
-@login_required
-@permission_required("utils.change_hat", raise_exception=True)
-@transaction.atomic
-def add_hat(request, user_pk):
- """
- Add a hat to a user.
- Creates the hat if it doesn't exist.
- """
-
- user = get_object_or_404(User, pk=user_pk)
-
- hat_name = request.POST.get("hat", "")
-
- try:
- hat = get_hat_to_add(hat_name, user)
- user.profile.hats.add(hat)
- try: # if hat was requested, remove the relevant request
- hat_request = HatRequest.objects.get(user=user, hat__iexact=hat.name, is_granted__isnull=True)
- hat_request.solve(
- is_granted=False,
- comment=_(
- "La demande a été automatiquement annulée car " "la casquette vous a été accordée manuellement."
- ),
- )
- except HatRequest.DoesNotExist:
- pass
- messages.success(request, _("La casquette a bien été ajoutée."))
- except ValueError as e:
- messages.error(request, str(e))
-
- return redirect(user.profile.get_absolute_url())
-
-
-@require_POST
-@login_required
-@transaction.atomic
-def remove_hat(request, user_pk, hat_pk):
- """Remove a hat from a user."""
-
- user = get_object_or_404(User, pk=user_pk)
- hat = get_object_or_404(Hat, pk=hat_pk)
- if user != request.user and not request.user.has_perm("utils.change_hat"):
- raise PermissionDenied
- if hat not in user.profile.hats.all():
- raise Http404
-
- user.profile.hats.remove(hat)
-
- messages.success(request, _("La casquette a bien été retirée."))
- return redirect(user.profile.get_absolute_url())
-
-
-def login_view(request):
- """Logs user in."""
- next_page = request.GET.get("next", "/")
- if next_page in [reverse("member-login"), reverse("register-member"), reverse("member-logout")]:
- next_page = "/"
- csrf_tk = {"next_page": next_page}
- csrf_tk.update(csrf(request))
- error = False
-
- if request.method != "POST":
- form = LoginForm()
- else:
- form = LoginForm(request.POST)
- if form.is_valid():
- username = form.cleaned_data["username"]
- password = form.cleaned_data["password"]
- user = authenticate(username=username, password=password)
- if user is None:
- initial = {"username": username}
- if User.objects.filter(username=username).exists():
- messages.error(
- request,
- _(
- "Le mot de passe saisi est incorrect. "
- "Cliquez sur le lien « Mot de passe oublié ? » "
- "si vous ne vous en souvenez plus."
- ),
- )
- else:
- messages.error(
- request,
- _(
- "Ce nom d’utilisateur est inconnu. "
- "Si vous ne possédez pas de compte, "
- "vous pouvez vous inscrire."
- ),
- )
- form = LoginForm(initial=initial)
- if next_page is not None:
- form.helper.form_action += "?next=" + next_page
- csrf_tk["error"] = error
- csrf_tk["form"] = form
- return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk})
- profile = get_object_or_404(Profile, user=user)
- if not user.is_active:
- messages.error(
- request,
- _(
- "Vous n'avez pas encore activé votre compte, "
- "vous devez le faire pour pouvoir vous "
- "connecter sur le site. Regardez dans vos "
- "mails : {}."
- ).format(user.email),
- )
- elif not profile.can_read_now():
- messages.error(
- request,
- _(
- "Vous n'êtes pas autorisé à vous connecter "
- "sur le site, vous avez été banni par un "
- "modérateur."
- ),
- )
- else:
- login(request, user)
- request.session["get_token"] = generate_token()
- if "remember" not in request.POST:
- request.session.set_expiry(0)
- profile.last_ip_address = get_client_ip(request)
- profile.save()
- # Redirect the user if needed.
- # Set the cookie for Clem smileys.
- # (For people switching account or clearing cookies
- # after a browser session.)
- try:
- response = redirect(resolve(next_page).url_name)
- except NoReverseMatch:
- response = redirect(next_page)
- except Resolver404:
- response = redirect(reverse("homepage"))
- set_old_smileys_cookie(response, profile)
- return response
-
- if next_page is not None:
- form.helper.form_action += "?next=" + next_page
- csrf_tk["error"] = error
- csrf_tk["form"] = form
- return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk})
-
-
-@login_required
-@require_POST
-def logout_view(request):
- """Log user out."""
-
- logout(request)
- request.session.clear()
- response = redirect(reverse("homepage"))
- # disable Clem smileys:
- remove_old_smileys_cookie(response)
- return response
-
-
-def forgot_password(request):
- """If the user has forgotten his password, they can get a new one."""
-
- if request.method == "POST":
- form = UsernameAndEmailForm(request.POST)
- if form.is_valid():
-
- # Get data from form
- data = form.data
- username = data["username"]
- email = data["email"]
-
- # Fetch the user, we need his email address
- usr = None
- if username:
- usr = get_object_or_404(User, Q(username=username))
-
- if email:
- usr = get_object_or_404(User, Q(email=email))
-
- # Generate a valid token during one hour
- uuid_token = str(uuid.uuid4())
- date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0)
- token = TokenForgotPassword(user=usr, token=uuid_token, date_end=date_end)
- token.save()
-
- # Send email
- subject = _("{} - Mot de passe oublié").format(settings.ZDS_APP["site"]["literal_name"])
- from_email = "{} <{}>".format(
- settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"]
- )
- context = {
- "username": usr.username,
- "site_name": settings.ZDS_APP["site"]["literal_name"],
- "site_url": settings.ZDS_APP["site"]["url"],
- "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(),
- }
- message_html = render_to_string("email/member/confirm_forgot_password.html", context)
- message_txt = render_to_string("email/member/confirm_forgot_password.txt", context)
-
- msg = EmailMultiAlternatives(subject, message_txt, from_email, [usr.email])
- msg.attach_alternative(message_html, "text/html")
- msg.send()
- return render(request, "member/forgot_password/success.html")
- else:
- return render(request, "member/forgot_password/index.html", {"form": form})
- form = UsernameAndEmailForm()
- return render(request, "member/forgot_password/index.html", {"form": form})
-
-
-def new_password(request):
- """Create a new password for a user."""
-
- try:
- token = request.GET["token"]
- except KeyError:
- return redirect(reverse("homepage"))
- token = get_object_or_404(TokenForgotPassword, token=token)
- if request.method == "POST":
- form = NewPasswordForm(token.user.username, request.POST)
- if form.is_valid():
- data = form.data
- password = data["password"]
- # User can't confirm his request if it is too late
-
- if datetime.now() > token.date_end:
- return render(request, "member/new_password/failed.html")
- token.user.set_password(password)
- token.user.save()
- token.delete()
- return render(request, "member/new_password/success.html")
- else:
- return render(request, "member/new_password/index.html", {"form": form})
- form = NewPasswordForm(identifier=token.user.username)
- return render(request, "member/new_password/index.html", {"form": form})
-
-
-def activate_account(request):
- """Activate an account with a token."""
- try:
- token = request.GET["token"]
- except KeyError:
- return redirect(reverse("homepage"))
- token = get_object_or_404(TokenRegister, token=token)
- usr = token.user
-
- # User can't confirm their request if their account is already active
- if usr.is_active:
- return render(request, "member/register/token_already_used.html")
-
- # User can't confirm their request if it is too late
- if datetime.now() > token.date_end:
- return render(request, "member/register/token_failed.html", {"token": token})
- usr.is_active = True
- usr.save()
-
- # Send welcome message
- bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
- msg = render_to_string(
- "member/messages/account_activated.md",
- {
- "username": usr.username,
- "site_name": settings.ZDS_APP["site"]["literal_name"],
- "library_url": settings.ZDS_APP["site"]["url"] + reverse("publication:list"),
- "opinions_url": settings.ZDS_APP["site"]["url"] + reverse("opinion:list"),
- "forums_url": settings.ZDS_APP["site"]["url"] + reverse("cats-forums-list"),
- },
- )
-
- send_mp(
- bot,
- [usr],
- _("Bienvenue sur {}").format(settings.ZDS_APP["site"]["literal_name"]),
- _("Le manuel du nouveau membre"),
- msg,
- send_by_mail=False,
- leave=True,
- direct=False,
- hat=get_hat_from_settings("moderation"),
- )
- token.delete()
-
- # Create an alert for the staff if it's a new provider
- if usr.email:
- provider = usr.email.split("@")[-1].lower()
- if (
- not NewEmailProvider.objects.filter(provider=provider).exists()
- and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=usr.pk).exists()
- ):
- NewEmailProvider.objects.create(user=usr, provider=provider, use=NEW_ACCOUNT)
-
- form = LoginForm(initial={"username": usr.username})
- return render(request, "member/register/token_success.html", {"usr": usr, "form": form})
-
-
-def generate_token_account(request):
- """Generate a token for an account."""
-
- try:
- token = request.GET["token"]
- except KeyError:
- return redirect(reverse("homepage"))
- token = get_object_or_404(TokenRegister, token=token)
-
- # Push date
-
- date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0)
- token.date_end = date_end
- token.save()
-
- # Send email
- subject = _("{} - Confirmation d'inscription").format(settings.ZDS_APP["site"]["literal_name"])
- from_email = "{} <{}>".format(settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"])
- context = {
- "username": token.user.username,
- "site_url": settings.ZDS_APP["site"]["url"],
- "site_name": settings.ZDS_APP["site"]["literal_name"],
- "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(),
- }
- message_html = render_to_string("email/member/confirm_registration.html", context)
- message_txt = render_to_string("email/member/confirm_registration.txt", context)
-
- msg = EmailMultiAlternatives(subject, message_txt, from_email, [token.user.email])
- msg.attach_alternative(message_html, "text/html")
- try:
- msg.send()
- except:
- msg = None
- return render(request, "member/register/success.html", {})
-
-
-def get_client_ip(request):
- """Retrieve the real IP address of the client."""
-
- if "HTTP_X_REAL_IP" in request.META: # nginx
- return request.META.get("HTTP_X_REAL_IP")
- elif "REMOTE_ADDR" in request.META:
- # other
- return request.META.get("REMOTE_ADDR")
- else:
- # Should never happen
- return "0.0.0.0"
-
-
-@login_required
-def settings_promote(request, user_pk):
- """
- Manage groups and activation status of a user.
- Only superusers are allowed to use this.
- """
-
- if not request.user.is_superuser:
- raise PermissionDenied
-
- profile = get_object_or_404(Profile, user__pk=user_pk)
- user = profile.user
-
- if request.method == "POST":
- form = PromoteMemberForm(request.POST)
- data = dict(form.data)
-
- groups = Group.objects.all()
- usergroups = user.groups.all()
-
- if "groups" in data:
- for group in groups:
- if str(group.id) in data["groups"]:
- if group not in usergroups:
- user.groups.add(group)
- messages.success(
- request, _("{0} appartient maintenant au groupe {1}.").format(user.username, group.name)
- )
- else:
- if group in usergroups:
- user.groups.remove(group)
- messages.warning(
- request,
- _("{0} n'appartient maintenant plus au groupe {1}.").format(user.username, group.name),
- )
- else:
- user.groups.clear()
- messages.warning(request, _("{0} n'appartient (plus ?) à aucun groupe.").format(user.username))
-
- if "activation" in data and "on" in data["activation"]:
- user.is_active = True
- messages.success(request, _("{0} est maintenant activé.").format(user.username))
- else:
- user.is_active = False
- messages.warning(request, _("{0} est désactivé.").format(user.username))
-
- user.save()
-
- usergroups = user.groups.all()
- bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
- msg = _(
- "Bonjour {0},\n\n" "Un administrateur vient de modifier les groupes " "auxquels vous appartenez. \n"
- ).format(user.username)
- if len(usergroups) > 0:
- msg = format_lazy("{}{}", msg, _("Voici la liste des groupes dont vous faites dorénavant partie :\n\n"))
- for group in usergroups:
- msg += f"* {group.name}\n"
- else:
- msg = format_lazy("{}{}", msg, _("* Vous ne faites partie d'aucun groupe"))
- send_mp(
- bot,
- [user],
- _("Modification des groupes"),
- "",
- msg,
- send_by_mail=True,
- leave=True,
- hat=get_hat_from_settings("moderation"),
- )
-
- return redirect(profile.get_absolute_url())
-
- form = PromoteMemberForm(initial={"groups": user.groups.all(), "activation": user.is_active})
- return render(request, "member/admin/promote.html", {"usr": user, "profile": profile, "form": form})
-
-
-@login_required
-@permission_required("member.change_profile", raise_exception=True)
-def member_from_ip(request, ip_address):
- """List users connected from a particular IP, and an IPV6 subnetwork."""
-
- members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit")
- members_and_ip = {"members": members, "ip": ip_address}
-
- if ":" in ip_address: # Check if it's an IPV6
- network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block
- # Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network
- network_ip = str(network_ip)[:-1]
- network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit")
- members_and_ip["network_members"] = network_members
- members_and_ip["network_ip"] = network_ip
-
- return render(request, "member/admin/memberip.html", members_and_ip)
-
-
-@login_required
-@permission_required("member.change_profile", raise_exception=True)
-@require_POST
-def modify_karma(request):
- """Add a Karma note to a user profile."""
-
- try:
- profile_pk = int(request.POST["profile_pk"])
- except (KeyError, ValueError):
- raise Http404
-
- profile = get_object_or_404(Profile, pk=profile_pk)
- if profile.is_private():
- raise PermissionDenied
-
- note = KarmaNote(user=profile.user, moderator=request.user, note=request.POST.get("note", "").strip())
-
- try:
- note.karma = int(request.POST["karma"])
- except (KeyError, ValueError):
- note.karma = 0
-
- try:
- if not note.note:
- raise ValueError("note cannot be empty")
- elif note.karma > 100 or note.karma < -100:
- raise ValueError(f"Max karma amount has to be between -100 and 100, you entered {note.karma}")
- else:
- note.save()
- profile.karma += note.karma
- profile.save()
- except ValueError as e:
- logging.getLogger(__name__).warn(f"ValueError: modifying karma failed because {e}")
-
- return redirect(reverse("member-detail", args=[profile.user.username]))
-
-
-class CreateProfileReportView(LoginRequiredMixin, View):
- def post(self, request, *args, **kwargs):
- profile = get_object_or_404(Profile, pk=kwargs["profile_pk"])
- reason = request.POST.get("reason", "")
- if reason == "":
- messages.warning(request, _("Veuillez saisir une raison."))
- else:
- alert = Alert(author=request.user, profile=profile, scope="PROFILE", text=reason, pubdate=datetime.now())
- alert.save()
- messages.success(
- request, _("Votre signalement a été transmis à l'équipe de modération. " "Merci de votre aide !")
- )
- return redirect(profile.get_absolute_url())
-
-
-class SolveProfileReportView(LoginRequiredMixin, PermissionRequiredMixin, View):
- permissions = ["member.change_profile"]
-
- def post(self, request, *args, **kwargs):
- alert = get_object_or_404(Alert, pk=kwargs["alert_pk"], solved=False, scope="PROFILE")
- text = request.POST.get("text", "")
- if text:
- msg_title = _("Signalement traité : profil de {}").format(alert.profile.user.username)
- msg_content = render_to_string(
- "member/messages/alert_solved.md",
- {
- "alert_author": alert.author.username,
- "reported_user": alert.profile.user.username,
- "moderator": request.user.username,
- "staff_message": text,
- },
- )
- alert.solve(request.user, text, msg_title, msg_content)
- else:
- alert.solve(request.user)
- messages.success(request, _("Merci, l'alerte a bien été résolue."))
- return redirect(alert.profile.get_absolute_url())
diff --git a/zds/member/views/__init__.py b/zds/member/views/__init__.py
new file mode 100644
index 0000000000..5162a97826
--- /dev/null
+++ b/zds/member/views/__init__.py
@@ -0,0 +1,29 @@
+from django.conf import settings
+
+from zds.member.models import Profile
+from zds.utils.paginator import ZdSPagingListView
+
+
+def get_client_ip(request):
+ """Retrieve the real IP address of the client."""
+
+ if "HTTP_X_REAL_IP" in request.META: # nginx
+ return request.META.get("HTTP_X_REAL_IP")
+ elif "REMOTE_ADDR" in request.META:
+ # other
+ return request.META.get("REMOTE_ADDR")
+ else:
+ # Should never happen
+ return "0.0.0.0"
+
+
+class MemberList(ZdSPagingListView):
+ """Display the list of registered users."""
+
+ context_object_name = "members"
+ paginate_by = settings.ZDS_APP["member"]["members_per_page"]
+ template_name = "member/index.html"
+
+ def get_queryset(self):
+ self.queryset = Profile.objects.contactable_members()
+ return super().get_queryset()
diff --git a/zds/member/views/admin.py b/zds/member/views/admin.py
new file mode 100644
index 0000000000..e5a8dcd3dd
--- /dev/null
+++ b/zds/member/views/admin.py
@@ -0,0 +1,89 @@
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import Group, User
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.text import format_lazy
+from django.utils.translation import gettext_lazy as _
+
+from zds.member.forms import PromoteMemberForm
+from zds.member.models import Profile
+from zds.utils.models import get_hat_from_settings
+from zds.utils.mps import send_mp
+
+
+@login_required
+def settings_promote(request, user_pk):
+ """
+ Manage groups and activation status of a user.
+ Only superusers are allowed to use this.
+ """
+
+ if not request.user.is_superuser:
+ raise PermissionDenied
+
+ profile = get_object_or_404(Profile, user__pk=user_pk)
+ user = profile.user
+
+ if request.method == "POST":
+ form = PromoteMemberForm(request.POST)
+ data = dict(form.data)
+
+ groups = Group.objects.all()
+ usergroups = user.groups.all()
+
+ if "groups" in data:
+ for group in groups:
+ if str(group.id) in data["groups"]:
+ if group not in usergroups:
+ user.groups.add(group)
+ messages.success(
+ request, _("{0} appartient maintenant au groupe {1}.").format(user.username, group.name)
+ )
+ else:
+ if group in usergroups:
+ user.groups.remove(group)
+ messages.warning(
+ request,
+ _("{0} n'appartient maintenant plus au groupe {1}.").format(user.username, group.name),
+ )
+ else:
+ user.groups.clear()
+ messages.warning(request, _("{0} n'appartient (plus ?) à aucun groupe.").format(user.username))
+
+ if "activation" in data and "on" in data["activation"]:
+ user.is_active = True
+ messages.success(request, _("{0} est maintenant activé.").format(user.username))
+ else:
+ user.is_active = False
+ messages.warning(request, _("{0} est désactivé.").format(user.username))
+
+ user.save()
+
+ usergroups = user.groups.all()
+ bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
+ msg = _(
+ "Bonjour {0},\n\n" "Un administrateur vient de modifier les groupes " "auxquels vous appartenez. \n"
+ ).format(user.username)
+ if len(usergroups) > 0:
+ msg = format_lazy("{}{}", msg, _("Voici la liste des groupes dont vous faites dorénavant partie :\n\n"))
+ for group in usergroups:
+ msg += f"* {group.name}\n"
+ else:
+ msg = format_lazy("{}{}", msg, _("* Vous ne faites partie d'aucun groupe"))
+ send_mp(
+ bot,
+ [user],
+ _("Modification des groupes"),
+ "",
+ msg,
+ send_by_mail=True,
+ leave=True,
+ hat=get_hat_from_settings("moderation"),
+ )
+
+ return redirect(profile.get_absolute_url())
+
+ form = PromoteMemberForm(initial={"groups": user.groups.all(), "activation": user.is_active})
+ return render(request, "member/admin/promote.html", {"usr": user, "profile": profile, "form": form})
diff --git a/zds/member/views/emailproviders.py b/zds/member/views/emailproviders.py
new file mode 100644
index 0000000000..f9776c5ffa
--- /dev/null
+++ b/zds/member/views/emailproviders.py
@@ -0,0 +1,110 @@
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.models import User
+from django.shortcuts import redirect, get_object_or_404
+from django.views.decorators.http import require_POST
+from django.views.generic import CreateView
+from django.urls import reverse_lazy
+from django.utils.translation import gettext_lazy as _
+
+from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin
+from zds.member.forms import BannedEmailProviderForm
+from zds.member.models import NewEmailProvider, BannedEmailProvider, Profile
+
+from zds.utils.paginator import ZdSPagingListView
+
+
+class NewEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
+ permissions = ["member.change_bannedemailprovider"]
+ paginate_by = settings.ZDS_APP["member"]["providers_per_page"]
+
+ model = NewEmailProvider
+ context_object_name = "providers"
+ template_name = "member/admin/new_email_providers.html"
+ queryset = NewEmailProvider.objects.select_related("user").select_related("user__profile").order_by("-date")
+
+
+@require_POST
+@login_required
+@permission_required("member.change_bannedemailprovider", raise_exception=True)
+def check_new_email_provider(request, provider_pk):
+ """Remove an alert about a new provider."""
+
+ provider = get_object_or_404(NewEmailProvider, pk=provider_pk)
+ if "ban" in request.POST and not BannedEmailProvider.objects.filter(provider=provider.provider).exists():
+ BannedEmailProvider.objects.create(provider=provider.provider, moderator=request.user)
+ provider.delete()
+
+ messages.success(request, _("Action effectuée."))
+ return redirect("new-email-providers")
+
+
+class BannedEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
+ """List the banned email providers."""
+
+ permissions = ["member.change_bannedemailprovider"]
+ paginate_by = settings.ZDS_APP["member"]["providers_per_page"]
+
+ model = BannedEmailProvider
+ context_object_name = "providers"
+ template_name = "member/admin/banned_email_providers.html"
+ queryset = (
+ BannedEmailProvider.objects.select_related("moderator").select_related("moderator__profile").order_by("-date")
+ )
+
+
+class MembersWithProviderList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
+ """List users using a banned email provider."""
+
+ permissions = ["member.change_bannedemailprovider"]
+ paginate_by = settings.ZDS_APP["member"]["members_per_page"]
+
+ model = User
+ context_object_name = "members"
+ template_name = "member/admin/members_with_provider.html"
+
+ def get_object(self):
+ return get_object_or_404(BannedEmailProvider, pk=self.kwargs["provider_pk"])
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["provider"] = self.get_object()
+ return context
+
+ def get_queryset(self):
+ provider = self.get_object()
+ return (
+ Profile.objects.select_related("user")
+ .order_by("-last_visit")
+ .filter(user__email__icontains=f"@{provider.provider}")
+ )
+
+
+class AddBannedEmailProvider(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
+ """Add an email provider to the banned list."""
+
+ permissions = ["member.change_bannedemailprovider"]
+
+ model = BannedEmailProvider
+ template_name = "member/admin/add_banned_email_provider.html"
+ form_class = BannedEmailProviderForm
+ success_url = reverse_lazy("banned-email-providers")
+
+ def form_valid(self, form):
+ form.instance.moderator = self.request.user
+ messages.success(self.request, _("Le fournisseur a été banni."))
+ return super().form_valid(form)
+
+
+@require_POST
+@login_required
+@permission_required("member.change_bannedemailprovider", raise_exception=True)
+def remove_banned_email_provider(request, provider_pk):
+ """Unban an email provider."""
+
+ provider = get_object_or_404(BannedEmailProvider, pk=provider_pk)
+ provider.delete()
+
+ messages.success(request, _("Le fournisseur « {} » a été débanni.").format(provider.provider))
+ return redirect("banned-email-providers")
diff --git a/zds/member/views/hats.py b/zds/member/views/hats.py
new file mode 100644
index 0000000000..2766e1d84a
--- /dev/null
+++ b/zds/member/views/hats.py
@@ -0,0 +1,212 @@
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.models import User
+from django.core.exceptions import PermissionDenied
+from django.db import transaction
+from django.http import Http404, StreamingHttpResponse
+from django.shortcuts import redirect, render, get_object_or_404
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.http import require_POST
+from django.views.generic import DetailView, CreateView
+
+from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin
+from zds.member.forms import HatRequestForm
+from zds.pages.models import GroupContact
+from zds.utils.models import HatRequest, Hat, get_hat_to_add
+from zds.utils.paginator import ZdSPagingListView
+
+
+class HatsList(ZdSPagingListView):
+ """Display the list of hats."""
+
+ context_object_name = "hats"
+ paginate_by = settings.ZDS_APP["member"]["hats_per_page"]
+ template_name = "member/hats.html"
+ queryset = (
+ Hat.objects.order_by("name")
+ .select_related("group")
+ .prefetch_related("group__user_set")
+ .prefetch_related("group__user_set__profile")
+ .prefetch_related("profile_set")
+ .prefetch_related("profile_set__user")
+ )
+
+
+class HatDetail(DetailView):
+ model = Hat
+ context_object_name = "hat"
+ template_name = "member/hat.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ hat = context["hat"]
+ if self.request.user.is_authenticated:
+ context["is_required"] = HatRequest.objects.filter(
+ user=self.request.user, hat__iexact=hat.name, is_granted__isnull=True
+ ).exists()
+ if hat.group:
+ context["users"] = hat.group.user_set.select_related("profile")
+ try:
+ context["groupcontact"] = hat.group.groupcontact
+ except GroupContact.DoesNotExist:
+ context["groupcontact"] = None # group not displayed on contact page
+ else:
+ context["users"] = [p.user for p in hat.profile_set.select_related("user")]
+ return context
+
+
+class HatsSettings(LoginRequiredMixin, CreateView):
+ model = HatRequest
+ template_name = "member/settings/hats.html"
+ form_class = HatRequestForm
+
+ def get_initial(self):
+ initial = super().get_initial()
+ if "ask" in self.request.GET:
+ try:
+ hat = Hat.objects.get(pk=int(self.request.GET["ask"]))
+ initial["hat"] = hat.name
+ except (ValueError, Hat.DoesNotExist):
+ pass
+ return initial
+
+ def post(self, request, *args, **kwargs):
+ if "preview" in request.POST and request.is_ajax():
+ content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")})
+ return StreamingHttpResponse(content)
+
+ return super().post(request, *args, **kwargs)
+
+ def form_valid(self, form):
+ form.instance.user = self.request.user
+ messages.success(self.request, _("Votre demande a bien été envoyée."))
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ # To remove #send-request HTML-anchor.
+ return "{}#".format(reverse("hats-settings"))
+
+
+class RequestedHatsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
+ permissions = ["utils.change_hat"]
+ paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"]
+
+ model = HatRequest
+ context_object_name = "requests"
+ template_name = "member/admin/requested_hats.html"
+ queryset = (
+ HatRequest.objects.filter(is_granted__isnull=True)
+ .select_related("user")
+ .select_related("user__profile")
+ .order_by("-date")
+ )
+
+
+class SolvedHatRequestsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView):
+ permissions = ["utils.change_hat"]
+ paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"]
+
+ model = HatRequest
+ context_object_name = "requests"
+ template_name = "member/admin/solved_hat_requests.html"
+ queryset = (
+ HatRequest.objects.filter(is_granted__isnull=False)
+ .select_related("user")
+ .select_related("user__profile")
+ .select_related("moderator")
+ .select_related("moderator__profile")
+ .order_by("-solved_at")
+ )
+
+
+class HatRequestDetail(LoginRequiredMixin, DetailView):
+ model = HatRequest
+ context_object_name = "hat_request"
+ template_name = "member/admin/hat_request.html"
+
+ def get_object(self, queryset=None):
+ request = super().get_object()
+ if request.user != self.request.user and not self.request.user.has_perm("utils.change_hat"):
+ raise PermissionDenied
+ return request
+
+
+@require_POST
+@login_required
+@permission_required("utils.change_hat", raise_exception=True)
+@transaction.atomic
+def solve_hat_request(request, request_pk):
+ """
+ Solve a hat request by granting or denying the requested hat
+ according to moderator's decision.
+ """
+
+ hat_request = get_object_or_404(HatRequest, pk=request_pk)
+
+ if hat_request.is_granted is not None:
+ raise PermissionDenied
+
+ try:
+ hat_request.solve(
+ "grant" in request.POST, request.user, request.POST.get("comment", ""), request.POST.get("hat", None)
+ )
+ messages.success(request, _("La demande a été résolue."))
+ return redirect("requested-hats")
+ except ValueError as e:
+ messages.error(request, str(e))
+ return redirect(hat_request.get_absolute_url())
+
+
+@require_POST
+@login_required
+@permission_required("utils.change_hat", raise_exception=True)
+@transaction.atomic
+def add_hat(request, user_pk):
+ """
+ Add a hat to a user.
+ Creates the hat if it doesn't exist.
+ """
+
+ user = get_object_or_404(User, pk=user_pk)
+
+ hat_name = request.POST.get("hat", "")
+
+ try:
+ hat = get_hat_to_add(hat_name, user)
+ user.profile.hats.add(hat)
+ try: # if hat was requested, remove the relevant request
+ hat_request = HatRequest.objects.get(user=user, hat__iexact=hat.name, is_granted__isnull=True)
+ hat_request.solve(
+ is_granted=False,
+ comment=_(
+ "La demande a été automatiquement annulée car " "la casquette vous a été accordée manuellement."
+ ),
+ )
+ except HatRequest.DoesNotExist:
+ pass
+ messages.success(request, _("La casquette a bien été ajoutée."))
+ except ValueError as e:
+ messages.error(request, str(e))
+
+ return redirect(user.profile.get_absolute_url())
+
+
+@require_POST
+@login_required
+@transaction.atomic
+def remove_hat(request, user_pk, hat_pk):
+ """Remove a hat from a user."""
+
+ user = get_object_or_404(User, pk=user_pk)
+ hat = get_object_or_404(Hat, pk=hat_pk)
+ if user != request.user and not request.user.has_perm("utils.change_hat"):
+ raise PermissionDenied
+ if hat not in user.profile.hats.all():
+ raise Http404
+
+ user.profile.hats.remove(hat)
+
+ messages.success(request, _("La casquette a bien été retirée."))
+ return redirect(user.profile.get_absolute_url())
diff --git a/zds/member/views/login.py b/zds/member/views/login.py
new file mode 100644
index 0000000000..712b10e68b
--- /dev/null
+++ b/zds/member/views/login.py
@@ -0,0 +1,117 @@
+from django.contrib import messages
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.template.context_processors import csrf
+from django.urls import reverse, resolve, Resolver404, NoReverseMatch
+from django.shortcuts import redirect, render, get_object_or_404
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.http import require_POST
+
+from zds.member.forms import LoginForm
+from zds.member.models import Profile, set_old_smileys_cookie, remove_old_smileys_cookie
+from zds.member.views import get_client_ip
+from zds.utils.tokens import generate_token
+
+
+def login_view(request):
+ """Logs user in."""
+ next_page = request.GET.get("next", "/")
+ if next_page in [reverse("member-login"), reverse("register-member"), reverse("member-logout")]:
+ next_page = "/"
+ csrf_tk = {"next_page": next_page}
+ csrf_tk.update(csrf(request))
+ error = False
+
+ if request.method != "POST":
+ form = LoginForm()
+ else:
+ form = LoginForm(request.POST)
+ if form.is_valid():
+ username = form.cleaned_data["username"]
+ password = form.cleaned_data["password"]
+ user = authenticate(username=username, password=password)
+ if user is None:
+ initial = {"username": username}
+ if User.objects.filter(username=username).exists():
+ messages.error(
+ request,
+ _(
+ "Le mot de passe saisi est incorrect. "
+ "Cliquez sur le lien « Mot de passe oublié ? » "
+ "si vous ne vous en souvenez plus."
+ ),
+ )
+ else:
+ messages.error(
+ request,
+ _(
+ "Ce nom d’utilisateur est inconnu. "
+ "Si vous ne possédez pas de compte, "
+ "vous pouvez vous inscrire."
+ ),
+ )
+ form = LoginForm(initial=initial)
+ if next_page is not None:
+ form.helper.form_action += "?next=" + next_page
+ csrf_tk["error"] = error
+ csrf_tk["form"] = form
+ return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk})
+ profile = get_object_or_404(Profile, user=user)
+ if not user.is_active:
+ messages.error(
+ request,
+ _(
+ "Vous n'avez pas encore activé votre compte, "
+ "vous devez le faire pour pouvoir vous "
+ "connecter sur le site. Regardez dans vos "
+ "mails : {}."
+ ).format(user.email),
+ )
+ elif not profile.can_read_now():
+ messages.error(
+ request,
+ _(
+ "Vous n'êtes pas autorisé à vous connecter "
+ "sur le site, vous avez été banni par un "
+ "modérateur."
+ ),
+ )
+ else:
+ login(request, user)
+ request.session["get_token"] = generate_token()
+ if "remember" not in request.POST:
+ request.session.set_expiry(0)
+ profile.last_ip_address = get_client_ip(request)
+ profile.save()
+ # Redirect the user if needed.
+ # Set the cookie for Clem smileys.
+ # (For people switching account or clearing cookies
+ # after a browser session.)
+ try:
+ response = redirect(resolve(next_page).url_name)
+ except NoReverseMatch:
+ response = redirect(next_page)
+ except Resolver404:
+ response = redirect(reverse("homepage"))
+ set_old_smileys_cookie(response, profile)
+ return response
+
+ if next_page is not None:
+ form.helper.form_action += "?next=" + next_page
+ csrf_tk["error"] = error
+ csrf_tk["form"] = form
+ return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk})
+
+
+@login_required
+@require_POST
+def logout_view(request):
+ """Log user out."""
+
+ logout(request)
+ request.session.clear()
+ response = redirect(reverse("homepage"))
+ # disable Clem smileys:
+ remove_old_smileys_cookie(response)
+ return response
diff --git a/zds/member/views/moderation.py b/zds/member/views/moderation.py
new file mode 100644
index 0000000000..a742a52a4a
--- /dev/null
+++ b/zds/member/views/moderation.py
@@ -0,0 +1,181 @@
+import ipaddress
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required, permission_required
+from django.core.exceptions import PermissionDenied
+from django.db import transaction
+from django.http import HttpResponseBadRequest
+from django.urls import reverse
+from django.http import Http404
+from django.shortcuts import redirect, render, get_object_or_404
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.http import require_POST
+
+from zds.member.commons import (
+ TemporaryReadingOnlySanction,
+ ReadingOnlySanction,
+ DeleteReadingOnlySanction,
+ TemporaryBanSanction,
+ BanSanction,
+ DeleteBanSanction,
+)
+from zds.member.decorator import can_write_and_read_now
+from zds.member.forms import MiniProfileForm
+from zds.member.models import Profile, KarmaNote
+import logging
+
+
+@login_required
+@permission_required("member.change_profile", raise_exception=True)
+def member_from_ip(request, ip_address):
+ """List users connected from a particular IP, and an IPV6 subnetwork."""
+
+ members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit")
+ members_and_ip = {"members": members, "ip": ip_address}
+
+ if ":" in ip_address: # Check if it's an IPV6
+ network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block
+ # Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network
+ network_ip = str(network_ip)[:-1]
+ network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit")
+ members_and_ip["network_members"] = network_members
+ members_and_ip["network_ip"] = network_ip
+
+ return render(request, "member/admin/memberip.html", members_and_ip)
+
+
+@login_required
+@permission_required("member.change_profile", raise_exception=True)
+@require_POST
+def modify_karma(request):
+ """Add a Karma note to a user profile."""
+
+ try:
+ profile_pk = int(request.POST["profile_pk"])
+ except (KeyError, ValueError):
+ raise Http404
+
+ profile = get_object_or_404(Profile, pk=profile_pk)
+ if profile.is_private():
+ raise PermissionDenied
+
+ note = KarmaNote(user=profile.user, moderator=request.user, note=request.POST.get("note", "").strip())
+
+ try:
+ note.karma = int(request.POST["karma"])
+ except (KeyError, ValueError):
+ note.karma = 0
+
+ try:
+ if not note.note:
+ raise ValueError("note cannot be empty")
+ elif note.karma > 100 or note.karma < -100:
+ raise ValueError(f"Max karma amount has to be between -100 and 100, you entered {note.karma}")
+ else:
+ note.save()
+ profile.karma += note.karma
+ profile.save()
+ except ValueError as e:
+ logging.getLogger(__name__).warning(f"ValueError: modifying karma failed because {e}")
+
+ return redirect(reverse("member-detail", args=[profile.user.username]))
+
+
+@can_write_and_read_now
+@login_required
+@permission_required("member.change_profile", raise_exception=True)
+def settings_mini_profile(request, user_name):
+ """Minimal settings of users for staff."""
+
+ # Extra information about the current user
+ profile = get_object_or_404(Profile, user__username=user_name)
+ if request.method == "POST":
+ form = MiniProfileForm(request.POST)
+ data = {"form": form, "profile": profile}
+ if form.is_valid():
+ profile.biography = form.data["biography"]
+ profile.site = form.data["site"]
+ profile.avatar_url = form.data["avatar_url"]
+ profile.sign = form.data["sign"]
+
+ # Save profile and redirect user to the settings page
+ # with a message indicating the operation state.
+
+ try:
+ profile.save()
+ except:
+ messages.error(request, _("Une erreur est survenue."))
+ return redirect(reverse("member-settings-mini-profile"))
+
+ messages.success(request, _("Le profil a correctement été mis à jour."))
+ return redirect(reverse("member-detail", args=[profile.user.username]))
+ else:
+ return render(request, "member/settings/profile.html", data)
+ else:
+ form = MiniProfileForm(
+ initial={
+ "biography": profile.biography,
+ "site": profile.site,
+ "avatar_url": profile.avatar_url,
+ "sign": profile.sign,
+ }
+ )
+ data = {"form": form, "profile": profile}
+ messages.warning(
+ request,
+ _(
+ "Le profil que vous éditez n'est pas le vôtre. "
+ "Soyez encore plus prudent lors de l'édition de celui-ci !"
+ ),
+ )
+ return render(request, "member/settings/profile.html", data)
+
+
+@require_POST
+@can_write_and_read_now
+@login_required
+@permission_required("member.change_profile", raise_exception=True)
+@transaction.atomic
+def modify_profile(request, user_pk):
+ """Modify the sanction of a user if there is a POST request."""
+
+ profile = get_object_or_404(Profile, user__pk=user_pk)
+ if profile.is_private():
+ raise PermissionDenied
+ if request.user.profile == profile:
+ messages.error(request, _("Vous ne pouvez pas vous sanctionner vous-même !"))
+ raise PermissionDenied
+
+ if "ls" in request.POST:
+ state = ReadingOnlySanction(request.POST)
+ elif "ls-temp" in request.POST:
+ state = TemporaryReadingOnlySanction(request.POST)
+ elif "ban" in request.POST:
+ state = BanSanction(request.POST)
+ elif "ban-temp" in request.POST:
+ state = TemporaryBanSanction(request.POST)
+ elif "un-ls" in request.POST:
+ state = DeleteReadingOnlySanction(request.POST)
+ else:
+ # un-ban
+ state = DeleteBanSanction(request.POST)
+
+ try:
+ ban = state.get_sanction(request.user, profile.user)
+ except ValueError:
+ raise HttpResponseBadRequest
+
+ state.apply_sanction(profile, ban)
+
+ if "un-ls" in request.POST or "un-ban" in request.POST:
+ msg = state.get_message_unsanction()
+ else:
+ msg = state.get_message_sanction()
+
+ msg = msg.format(
+ ban.user, ban.moderator, ban.type, state.get_detail(), ban.note, settings.ZDS_APP["site"]["literal_name"]
+ )
+
+ state.notify_member(ban, msg)
+ return redirect(profile.get_absolute_url())
diff --git a/zds/member/views/password_recovery.py b/zds/member/views/password_recovery.py
new file mode 100644
index 0000000000..71a65dd483
--- /dev/null
+++ b/zds/member/views/password_recovery.py
@@ -0,0 +1,91 @@
+import uuid
+from datetime import datetime, timedelta
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.mail import EmailMultiAlternatives
+from django.urls import reverse
+from django.db.models import Q
+from django.shortcuts import redirect, render, get_object_or_404
+from django.template.loader import render_to_string
+from django.utils.translation import gettext_lazy as _
+
+from zds.member.forms import NewPasswordForm, UsernameAndEmailForm
+from zds.member.models import TokenForgotPassword
+
+
+def forgot_password(request):
+ """If the user has forgotten his password, they can get a new one."""
+
+ if request.method == "POST":
+ form = UsernameAndEmailForm(request.POST)
+ if form.is_valid():
+
+ # Get data from form
+ data = form.data
+ username = data["username"]
+ email = data["email"]
+
+ # Fetch the user, we need his email address
+ usr = None
+ if username:
+ usr = get_object_or_404(User, Q(username=username))
+
+ if email:
+ usr = get_object_or_404(User, Q(email=email))
+
+ # Generate a valid token during one hour
+ uuid_token = str(uuid.uuid4())
+ date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0)
+ token = TokenForgotPassword(user=usr, token=uuid_token, date_end=date_end)
+ token.save()
+
+ # Send email
+ subject = _("{} - Mot de passe oublié").format(settings.ZDS_APP["site"]["literal_name"])
+ from_email = "{} <{}>".format(
+ settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"]
+ )
+ context = {
+ "username": usr.username,
+ "site_name": settings.ZDS_APP["site"]["literal_name"],
+ "site_url": settings.ZDS_APP["site"]["url"],
+ "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(),
+ }
+ message_html = render_to_string("email/member/confirm_forgot_password.html", context)
+ message_txt = render_to_string("email/member/confirm_forgot_password.txt", context)
+
+ msg = EmailMultiAlternatives(subject, message_txt, from_email, [usr.email])
+ msg.attach_alternative(message_html, "text/html")
+ msg.send()
+ return render(request, "member/forgot_password/success.html")
+ else:
+ return render(request, "member/forgot_password/index.html", {"form": form})
+ form = UsernameAndEmailForm()
+ return render(request, "member/forgot_password/index.html", {"form": form})
+
+
+def new_password(request):
+ """Create a new password for a user."""
+
+ try:
+ token = request.GET["token"]
+ except KeyError:
+ return redirect(reverse("homepage"))
+ token = get_object_or_404(TokenForgotPassword, token=token)
+ if request.method == "POST":
+ form = NewPasswordForm(token.user.username, request.POST)
+ if form.is_valid():
+ data = form.data
+ password = data["password"]
+ # User can't confirm his request if it is too late
+
+ if datetime.now() > token.date_end:
+ return render(request, "member/new_password/failed.html")
+ token.user.set_password(password)
+ token.user.save()
+ token.delete()
+ return render(request, "member/new_password/success.html")
+ else:
+ return render(request, "member/new_password/index.html", {"form": form})
+ form = NewPasswordForm(identifier=token.user.username)
+ return render(request, "member/new_password/index.html", {"form": form})
diff --git a/zds/member/views/profile.py b/zds/member/views/profile.py
new file mode 100644
index 0000000000..4a69027e32
--- /dev/null
+++ b/zds/member/views/profile.py
@@ -0,0 +1,441 @@
+from urllib.parse import unquote
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.core.exceptions import PermissionDenied
+from django.urls import reverse, reverse_lazy
+from django.http import StreamingHttpResponse
+from django.shortcuts import redirect, render, get_object_or_404
+from django.utils.decorators import method_decorator
+from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext as __
+from django.views.decorators.http import require_POST
+from django.views.generic import DetailView, UpdateView
+
+from zds.forum.models import Topic, TopicRead
+from zds.gallery.forms import ImageAsAvatarForm
+from zds.member import EMAIL_EDIT
+
+from zds.member.forms import (
+ ProfileForm,
+ ChangePasswordForm,
+ ChangeUserForm,
+ KarmaForm,
+ GitHubTokenForm,
+)
+from zds.member.models import (
+ Profile,
+ KarmaNote,
+ Ban,
+ NewEmailProvider,
+ set_old_smileys_cookie,
+)
+from zds.notification.models import TopicAnswerSubscription, NewPublicationSubscription
+from zds.tutorialv2.models import CONTENT_TYPES
+from zds.tutorialv2.models.database import PublishedContent, ContentContribution, ContentReaction
+from zds.utils.templatetags.pluralize_fr import pluralize_fr
+
+
+class MemberDetail(DetailView):
+ """Display details about a profile."""
+
+ context_object_name = "usr"
+ model = User
+ template_name = "member/profile.html"
+
+ def get_object(self, queryset=None):
+ # Use unquote to accept twicely quoted URLs (for instance in MPs
+ # sent through emarkdown parser).
+ return get_object_or_404(User, username=unquote(self.kwargs["user_name"]))
+
+ def get_summaries(self, profile):
+ """
+ Returns a summary of this profile's activity, as a list of list of tuples.
+ Each first-level list item is an activity category (e.g. contents, forums, etc.)
+ Each second-level list item is a stat in this activity category.
+ Each tuple is (link url, count, displayed name of the item), where the link url can be None if it's not a link.
+
+ :param profile: The profile.
+ :return: The summary data.
+ """
+ summaries = []
+
+ if self.request.user.has_perm("member.change_post"):
+ count_post = profile.get_post_count_as_staff()
+ else:
+ count_post = profile.get_post_count()
+
+ count_topic = profile.get_topic_count()
+ count_followed_topic = profile.get_followed_topic_count()
+ count_tutorials = profile.get_public_tutos().count()
+ count_articles = profile.get_public_articles().count()
+ count_opinions = profile.get_public_opinions().count()
+
+ summary = []
+ if count_tutorials + count_articles + count_opinions == 0:
+ summary.append((None, 0, __("Aucun contenu publié")))
+
+ if count_tutorials > 0:
+ summary.append(
+ (
+ reverse_lazy("tutorial:find-tutorial", args=(profile.user.username,)),
+ count_tutorials,
+ __("tutoriel{}").format(pluralize_fr(count_tutorials)),
+ )
+ )
+ if count_articles > 0:
+ summary.append(
+ (
+ reverse_lazy("article:find-article", args=(profile.user.username,)),
+ count_articles,
+ __("article{}").format(pluralize_fr(count_articles)),
+ )
+ )
+ if count_opinions > 0:
+ summary.append(
+ (
+ reverse_lazy("opinion:find-opinion", args=(profile.user.username,)),
+ count_opinions,
+ __("billet{}").format(pluralize_fr(count_opinions)),
+ )
+ )
+ summaries.append(summary)
+
+ summary = []
+ if count_post > 0:
+ summary.append(
+ (
+ reverse_lazy("post-find", args=(profile.user.pk,)),
+ count_post,
+ __("message{}").format(pluralize_fr(count_post)),
+ )
+ )
+ else:
+ summary.append((None, 0, __("Aucun message")))
+ if count_topic > 0:
+ summary.append(
+ (
+ reverse_lazy("topic-find", args=(profile.user.pk,)),
+ count_topic,
+ __("sujet{} créé{}").format(pluralize_fr(count_topic), pluralize_fr(count_topic)),
+ )
+ )
+ user = self.request.user
+ is_user_profile = user.is_authenticated and User.objects.get(pk=user.pk).profile == profile
+ if count_followed_topic > 0 and is_user_profile:
+ summary.append(
+ (
+ reverse_lazy("followed-topic-find"),
+ count_followed_topic,
+ __("sujet{} suivi{}").format(
+ pluralize_fr(count_followed_topic), pluralize_fr(count_followed_topic)
+ ),
+ )
+ )
+
+ summaries.append(summary)
+
+ return summaries
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ usr = context["usr"]
+ profile = usr.profile
+ context["profile"] = profile
+ context["topics"] = list(Topic.objects.last_topics_of_a_member(usr, self.request.user))
+ followed_query_set = TopicAnswerSubscription.objects.get_objects_followed_by(self.request.user.id)
+ followed_topics = list(set(followed_query_set) & set(context["topics"]))
+ for topic in context["topics"]:
+ topic.is_followed = topic in followed_topics
+ context["articles"] = PublishedContent.objects.last_articles_of_a_member_loaded(usr)
+ context["opinions"] = PublishedContent.objects.last_opinions_of_a_member_loaded(usr)
+ context["tutorials"] = PublishedContent.objects.last_tutorials_of_a_member_loaded(usr)
+ context["articles_and_tutorials"] = PublishedContent.objects.last_tutorials_and_articles_of_a_member_loaded(usr)
+ context["topic_read"] = TopicRead.objects.list_read_topic_pk(self.request.user, context["topics"])
+ context["subscriber_count"] = NewPublicationSubscription.objects.get_subscriptions(self.object).count()
+ context["contribution_articles_count"] = (
+ ContentContribution.objects.filter(
+ user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[1]["name"]
+ )
+ .values_list("content", flat=True)
+ .distinct()
+ .count()
+ )
+ context["contribution_tutorials_count"] = (
+ ContentContribution.objects.filter(
+ user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[0]["name"]
+ )
+ .values_list("content", flat=True)
+ .distinct()
+ .count()
+ )
+ context["content_reactions_count"] = ContentReaction.objects.filter(author=usr).count()
+
+ if self.request.user.has_perm("member.change_profile"):
+ sanctions = list(Ban.objects.filter(user=usr).select_related("moderator"))
+ notes = list(KarmaNote.objects.filter(user=usr).select_related("moderator"))
+ actions = sanctions + notes
+ actions.sort(key=lambda action: action.pubdate)
+ actions.reverse()
+ context["actions"] = actions
+ context["karmaform"] = KarmaForm(profile)
+ context["alerts"] = profile.alerts_on_this_profile.all().order_by("-pubdate")
+ context["has_unsolved_alerts"] = profile.alerts_on_this_profile.filter(solved=False).exists()
+
+ context["summaries"] = self.get_summaries(profile)
+ return context
+
+
+def redirect_old_profile_to_new(request, user_name):
+ user = get_object_or_404(User, username=user_name)
+ return redirect(user.profile, permanent=True)
+
+
+class UpdateMember(UpdateView):
+ """Update a profile."""
+
+ form_class = ProfileForm
+ template_name = "member/settings/profile.html"
+
+ @method_decorator(login_required)
+ def dispatch(self, *args, **kwargs):
+ return super().dispatch(*args, **kwargs)
+
+ def get_object(self, queryset=None):
+ return get_object_or_404(Profile, user=self.request.user)
+
+ def get_form(self, form_class=ProfileForm):
+ profile = self.get_object()
+ form = form_class(
+ initial={
+ "biography": profile.biography,
+ "site": profile.site,
+ "avatar_url": profile.avatar_url,
+ "show_sign": profile.show_sign,
+ "is_hover_enabled": profile.is_hover_enabled,
+ "use_old_smileys": profile.use_old_smileys,
+ "allow_temp_visual_changes": profile.allow_temp_visual_changes,
+ "show_markdown_help": profile.show_markdown_help,
+ "email_for_answer": profile.email_for_answer,
+ "email_for_new_mp": profile.email_for_new_mp,
+ "sign": profile.sign,
+ "licence": profile.licence,
+ }
+ )
+
+ return form
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST)
+
+ if "preview" in request.POST and request.is_ajax():
+ content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")})
+ return StreamingHttpResponse(content)
+
+ if form.is_valid():
+ return self.form_valid(form)
+
+ return render(request, self.template_name, {"form": form})
+
+ def form_valid(self, form):
+ profile = self.get_object()
+ self.update_profile(profile, form)
+ self.save_profile(profile)
+
+ response = redirect(self.get_success_url())
+ set_old_smileys_cookie(response, profile)
+ return response
+
+ def update_profile(self, profile, form):
+ cleaned_data_options = form.cleaned_data.get("options")
+ profile.biography = form.data["biography"]
+ profile.site = form.data["site"]
+ profile.show_sign = "show_sign" in cleaned_data_options
+ profile.is_hover_enabled = "is_hover_enabled" in cleaned_data_options
+ profile.use_old_smileys = "use_old_smileys" in cleaned_data_options
+ profile.allow_temp_visual_changes = "allow_temp_visual_changes" in cleaned_data_options
+ profile.show_markdown_help = "show_markdown_help" in cleaned_data_options
+ profile.email_for_answer = "email_for_answer" in cleaned_data_options
+ profile.email_for_new_mp = "email_for_new_mp" in cleaned_data_options
+ profile.avatar_url = form.data["avatar_url"]
+ profile.sign = form.data["sign"]
+ profile.licence = form.cleaned_data["licence"]
+
+ def get_success_url(self):
+ return reverse("update-member")
+
+ def save_profile(self, profile):
+ try:
+ profile.save()
+ profile.user.save()
+ except Profile.DoesNotExist:
+ messages.error(self.request, self.get_error_message())
+ return redirect(reverse("update-member"))
+ messages.success(self.request, self.get_success_message())
+
+ def get_success_message(self):
+ return _("Le profil a correctement été mis à jour.")
+
+ def get_error_message(self):
+ return _("Une erreur est survenue.")
+
+
+class UpdateGitHubToken(UpdateView):
+ """Update the GitHub token."""
+
+ form_class = GitHubTokenForm
+ template_name = "member/settings/github.html"
+
+ @method_decorator(login_required)
+ def dispatch(self, request, *args, **kwargs):
+ if not request.user.profile.is_dev():
+ raise PermissionDenied
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_object(self, queryset=None):
+ return get_object_or_404(Profile, user=self.request.user)
+
+ def get_form(self, form_class=GitHubTokenForm):
+ return form_class()
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST)
+
+ if form.is_valid():
+ return self.form_valid(form)
+
+ return render(request, self.template_name, {"form": form})
+
+ def form_valid(self, form):
+ profile = self.get_object()
+ profile.github_token = form.data["github_token"]
+ profile.save()
+ messages.success(self.request, self.get_success_message())
+
+ return redirect(self.get_success_url())
+
+ def get_success_url(self):
+ return reverse("update-github")
+
+ def get_success_message(self):
+ return _("Votre token GitHub a été mis à jour.")
+
+ def get_error_message(self):
+ return _("Une erreur est survenue.")
+
+
+@require_POST
+@login_required
+def remove_github_token(request):
+ """Remove the current user token."""
+
+ profile = get_object_or_404(Profile, user=request.user)
+ if not profile.is_dev():
+ raise PermissionDenied
+
+ profile.github_token = ""
+ profile.save()
+
+ messages.success(request, _("Votre token GitHub a été supprimé."))
+ return redirect("update-github")
+
+
+class UpdateAvatarMember(UpdateMember):
+ """Update the avatar of a logged in user."""
+
+ form_class = ImageAsAvatarForm
+
+ def get_success_url(self):
+ profile = self.get_object()
+
+ return reverse("member-detail", args=[profile.user.username])
+
+ def get_form(self, form_class=ImageAsAvatarForm):
+ return form_class(self.request.POST)
+
+ def update_profile(self, profile, form):
+ profile.avatar_url = form.data["avatar_url"]
+
+ def get_success_message(self):
+ return _("L'avatar a correctement été mis à jour.")
+
+
+class UpdatePasswordMember(UpdateMember):
+ """Password-related user settings."""
+
+ form_class = ChangePasswordForm
+ template_name = "member/settings/account.html"
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.user, request.POST)
+
+ if form.is_valid():
+ return self.form_valid(form)
+
+ return render(request, self.template_name, {"form": form})
+
+ def get_form(self, form_class=ChangePasswordForm):
+ return form_class(self.request.user)
+
+ def update_profile(self, profile, form):
+ profile.user.set_password(form.data["password_new"])
+
+ def get_success_message(self):
+ return _("Le mot de passe a correctement été mis à jour.")
+
+ def get_success_url(self):
+ return reverse("update-password-member")
+
+
+class UpdateUsernameEmailMember(UpdateMember):
+ """Settings related to username and email."""
+
+ form_class = ChangeUserForm
+ template_name = "member/settings/user.html"
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.user, request.POST)
+
+ if form.is_valid():
+ return self.form_valid(form)
+
+ return render(request, self.template_name, {"form": form})
+
+ def get_form(self, form_class=ChangeUserForm):
+ return form_class(self.request.user)
+
+ def update_profile(self, profile, form):
+ profile.show_email = "show_email" in form.cleaned_data.get("options")
+ new_username = form.cleaned_data.get("username")
+ previous_username = form.cleaned_data.get("previous_username")
+ new_email = form.cleaned_data.get("email")
+ previous_email = form.cleaned_data.get("previous_email")
+ if new_username and new_username != previous_username:
+ # Add a karma message for the staff
+ bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
+ KarmaNote(
+ user=profile.user,
+ moderator=bot,
+ note=_("{} s'est renommé {}").format(profile.user.username, new_username),
+ karma=0,
+ ).save()
+ # Change the username
+ profile.user.username = new_username
+ # update skeleton
+ profile.username_skeleton = Profile.find_username_skeleton(new_username)
+ if new_email and new_email != previous_email:
+ profile.user.email = new_email
+ # Create an alert for the staff if it's a new provider
+ provider = provider = new_email.split("@")[-1].lower()
+ if (
+ not NewEmailProvider.objects.filter(provider=provider).exists()
+ and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=profile.user.pk).exists()
+ ):
+ NewEmailProvider.objects.create(user=profile.user, provider=provider, use=EMAIL_EDIT)
+
+ def get_success_url(self):
+ profile = self.get_object()
+
+ return profile.get_absolute_url()
diff --git a/zds/member/views/register.py b/zds/member/views/register.py
new file mode 100644
index 0000000000..d92e6f6121
--- /dev/null
+++ b/zds/member/views/register.py
@@ -0,0 +1,331 @@
+from datetime import datetime, timedelta
+
+from oauth2_provider.models import AccessToken
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth import logout
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.core.mail import EmailMultiAlternatives
+from django.urls import reverse
+from django.db import transaction
+from django.db.models import Q
+from django.shortcuts import redirect, render, get_object_or_404
+from django.template.loader import render_to_string
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.http import require_POST
+from django.views.generic import CreateView, FormView
+
+from zds.forum.models import Topic
+from zds.gallery.models import UserGallery
+from zds.member import NEW_ACCOUNT
+from zds.member.commons import (
+ ProfileCreate,
+ TokenGenerator,
+)
+from zds.member.forms import RegisterForm, UsernameAndEmailForm, LoginForm
+from zds.member.models import (
+ Profile,
+ TokenRegister,
+ KarmaNote,
+ Ban,
+ BannedEmailProvider,
+ NewEmailProvider,
+)
+from zds.member.views import get_client_ip
+from zds.mp.models import PrivatePost, PrivateTopic
+from zds.tutorialv2.models.database import PickListOperation
+from zds.utils.models import (
+ Comment,
+ CommentVote,
+ Alert,
+ CommentEdit,
+ HatRequest,
+ get_hat_from_settings,
+)
+import logging
+
+from zds.utils.mps import send_mp
+
+
+class RegisterView(CreateView, ProfileCreate, TokenGenerator):
+ """Create a profile."""
+
+ form_class = RegisterForm
+ template_name = "member/register/index.html"
+
+ def dispatch(self, *args, **kwargs):
+ return super().dispatch(*args, **kwargs)
+
+ def get_object(self, queryset=None):
+ return get_object_or_404(Profile, user=self.request.user)
+
+ def get_form(self, form_class=RegisterForm):
+ return form_class()
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST)
+
+ if form.is_valid():
+ return self.form_valid(form)
+ return render(request, self.template_name, {"form": form})
+
+ def form_valid(self, form):
+ profile = self.create_profile(form.data)
+ profile.last_ip_address = get_client_ip(self.request)
+ profile.username_skeleton = Profile.find_username_skeleton(profile.user.username)
+ self.save_profile(profile)
+ token = self.generate_token(profile.user)
+ try:
+ self.send_email(token, profile.user)
+ except Exception as e:
+ logging.getLogger(__name__).warning("Mail not sent", exc_info=e)
+ messages.warning(self.request, _("Impossible d'envoyer l'email."))
+ self.object = None
+ return self.form_invalid(form)
+ return render(self.request, self.get_success_template())
+
+ def get_success_template(self):
+ return "member/register/success.html"
+
+
+class SendValidationEmailView(FormView, TokenGenerator):
+ """Send a validation email on demand."""
+
+ form_class = UsernameAndEmailForm
+ template_name = "member/register/send_validation_email.html"
+
+ usr = None
+
+ def get_user(self, username, email):
+
+ if username:
+ self.usr = get_object_or_404(User, username=username)
+
+ elif email:
+ self.usr = get_object_or_404(User, email=email)
+
+ def get_form(self, form_class=UsernameAndEmailForm):
+ return form_class()
+
+ def post(self, request, *args, **kwargs):
+ form = self.form_class(request.POST)
+
+ if form.is_valid():
+ # Fetch the user
+ self.get_user(form.data["username"], form.data["email"])
+
+ # User should not already be active
+ if not self.usr.is_active:
+ return self.form_valid(form)
+ else:
+ if form.data["username"]:
+ form.errors["username"] = form.error_class([self.get_error_message()])
+ else:
+ form.errors["email"] = form.error_class([self.get_error_message()])
+
+ return render(request, self.template_name, {"form": form})
+
+ def form_valid(self, form):
+ # Delete old token
+ token = TokenRegister.objects.filter(user=self.usr)
+ if token.count() >= 1:
+ token.all().delete()
+
+ # Generate new token and send email
+ token = self.generate_token(self.usr)
+ try:
+ self.send_email(token, self.usr)
+ except Exception as e:
+ logging.getLogger(__name__).warning("Mail not sent", exc_info=e)
+ messages.warning(_("Impossible d'envoyer l'email."))
+ return self.form_invalid(form)
+
+ return render(self.request, self.get_success_template())
+
+ def get_success_template(self):
+ return "member/register/send_validation_email_success.html"
+
+ def get_error_message(self):
+ return _("Le compte est déjà activé.")
+
+
+@login_required
+def warning_unregister(request):
+ """
+ Display a warning page showing what will happen when the user
+ unregisters.
+ """
+ return render(request, "member/settings/unregister.html", {"user": request.user})
+
+
+@login_required
+@require_POST
+@transaction.atomic
+def unregister(request):
+ """Allow members to unregister."""
+
+ anonymous = get_object_or_404(User, username=settings.ZDS_APP["member"]["anonymous_account"])
+ external = get_object_or_404(User, username=settings.ZDS_APP["member"]["external_account"])
+ current = request.user
+ # Nota : as of v21 all about content paternity is held by a proper receiver in zds.tutorialv2.models.database
+ PickListOperation.objects.filter(staff_user=current).update(staff_user=anonymous)
+ PickListOperation.objects.filter(canceler_user=current).update(canceler_user=anonymous)
+ # Comments likes / dislikes
+ votes = CommentVote.objects.filter(user=current)
+ for vote in votes:
+ if vote.positive:
+ vote.comment.like -= 1
+ else:
+ vote.comment.dislike -= 1
+ vote.comment.save()
+ votes.delete()
+ # All contents anonymization
+ Comment.objects.filter(author=current).update(author=anonymous)
+ PrivatePost.objects.filter(author=current).update(author=anonymous)
+ CommentEdit.objects.filter(editor=current).update(editor=anonymous)
+ CommentEdit.objects.filter(deleted_by=current).update(deleted_by=anonymous)
+ # Karma notes, alerts and sanctions anonymization (to keep them)
+ KarmaNote.objects.filter(moderator=current).update(moderator=anonymous)
+ Ban.objects.filter(moderator=current).update(moderator=anonymous)
+ Alert.objects.filter(author=current).update(author=anonymous)
+ Alert.objects.filter(moderator=current).update(moderator=anonymous)
+ BannedEmailProvider.objects.filter(moderator=current).update(moderator=anonymous)
+ # Solved hat requests anonymization
+ HatRequest.objects.filter(moderator=current).update(moderator=anonymous)
+ # In case current user has been moderator in the past
+ Comment.objects.filter(editor=current).update(editor=anonymous)
+ for topic in PrivateTopic.objects.filter(Q(author=current) | Q(participants__in=[current])):
+ if topic.one_participant_remaining():
+ topic.delete()
+ else:
+ topic.remove_participant(current)
+ topic.save()
+ Topic.objects.filter(solved_by=current).update(solved_by=anonymous)
+ Topic.objects.filter(author=current).update(author=anonymous)
+
+ # Any content exclusively owned by the unregistering member will
+ # be deleted just before the User object (using a pre_delete
+ # receiver).
+ #
+ # Regarding galleries, there are two cases:
+ #
+ # - "personal galleries" with one owner (the unregistering
+ # user). The user's ownership is removed and replaced by an
+ # anonymous user in order not to lost the gallery.
+ #
+ # - "personal galleries" with many other owners. It is safe to
+ # remove the user's ownership, the gallery won't be lost.
+
+ galleries = UserGallery.objects.filter(user=current)
+ for gallery in galleries:
+ if gallery.gallery.get_linked_users().count() == 1:
+ anonymous_gallery = UserGallery()
+ anonymous_gallery.user = external
+ anonymous_gallery.mode = "w"
+ anonymous_gallery.gallery = gallery.gallery
+ anonymous_gallery.save()
+ galleries.delete()
+
+ # Remove API access (tokens + applications)
+ for token in AccessToken.objects.filter(user=current):
+ token.revoke()
+
+ logout(request)
+ User.objects.filter(pk=current.pk).delete()
+ return redirect(reverse("homepage"))
+
+
+def activate_account(request):
+ """Activate an account with a token."""
+ try:
+ token = request.GET["token"]
+ except KeyError:
+ return redirect(reverse("homepage"))
+ token = get_object_or_404(TokenRegister, token=token)
+ usr = token.user
+
+ # User can't confirm their request if their account is already active
+ if usr.is_active:
+ return render(request, "member/register/token_already_used.html")
+
+ # User can't confirm their request if it is too late
+ if datetime.now() > token.date_end:
+ return render(request, "member/register/token_failed.html", {"token": token})
+ usr.is_active = True
+ usr.save()
+
+ # Send welcome message
+ bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"])
+ msg = render_to_string(
+ "member/messages/account_activated.md",
+ {
+ "username": usr.username,
+ "site_name": settings.ZDS_APP["site"]["literal_name"],
+ "library_url": settings.ZDS_APP["site"]["url"] + reverse("publication:list"),
+ "opinions_url": settings.ZDS_APP["site"]["url"] + reverse("opinion:list"),
+ "forums_url": settings.ZDS_APP["site"]["url"] + reverse("cats-forums-list"),
+ },
+ )
+
+ send_mp(
+ bot,
+ [usr],
+ _("Bienvenue sur {}").format(settings.ZDS_APP["site"]["literal_name"]),
+ _("Le manuel du nouveau membre"),
+ msg,
+ send_by_mail=False,
+ leave=True,
+ direct=False,
+ hat=get_hat_from_settings("moderation"),
+ )
+ token.delete()
+
+ # Create an alert for the staff if it's a new provider
+ if usr.email:
+ provider = usr.email.split("@")[-1].lower()
+ if (
+ not NewEmailProvider.objects.filter(provider=provider).exists()
+ and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=usr.pk).exists()
+ ):
+ NewEmailProvider.objects.create(user=usr, provider=provider, use=NEW_ACCOUNT)
+
+ form = LoginForm(initial={"username": usr.username})
+ return render(request, "member/register/token_success.html", {"usr": usr, "form": form})
+
+
+def generate_token_account(request):
+ """Generate a token for an account."""
+
+ try:
+ token = request.GET["token"]
+ except KeyError:
+ return redirect(reverse("homepage"))
+ token = get_object_or_404(TokenRegister, token=token)
+
+ # Push date
+
+ date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0)
+ token.date_end = date_end
+ token.save()
+
+ # Send email
+ subject = _("{} - Confirmation d'inscription").format(settings.ZDS_APP["site"]["literal_name"])
+ from_email = "{} <{}>".format(settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"])
+ context = {
+ "username": token.user.username,
+ "site_url": settings.ZDS_APP["site"]["url"],
+ "site_name": settings.ZDS_APP["site"]["literal_name"],
+ "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(),
+ }
+ message_html = render_to_string("email/member/confirm_registration.html", context)
+ message_txt = render_to_string("email/member/confirm_registration.txt", context)
+
+ msg = EmailMultiAlternatives(subject, message_txt, from_email, [token.user.email])
+ msg.attach_alternative(message_html, "text/html")
+ try:
+ msg.send()
+ except:
+ msg = None
+ return render(request, "member/register/success.html", {})
diff --git a/zds/member/views/reports.py b/zds/member/views/reports.py
new file mode 100644
index 0000000000..c9a258734b
--- /dev/null
+++ b/zds/member/views/reports.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+
+from django.contrib import messages
+from django.shortcuts import redirect, get_object_or_404
+from django.template.loader import render_to_string
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import View
+
+from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin
+from zds.member.models import Profile
+from zds.utils.models import Alert
+
+
+class CreateProfileReportView(LoginRequiredMixin, View):
+ def post(self, request, *args, **kwargs):
+ profile = get_object_or_404(Profile, pk=kwargs["profile_pk"])
+ reason = request.POST.get("reason", "")
+ if reason == "":
+ messages.warning(request, _("Veuillez saisir une raison."))
+ else:
+ alert = Alert(author=request.user, profile=profile, scope="PROFILE", text=reason, pubdate=datetime.now())
+ alert.save()
+ messages.success(
+ request, _("Votre signalement a été transmis à l'équipe de modération. " "Merci de votre aide !")
+ )
+ return redirect(profile.get_absolute_url())
+
+
+class SolveProfileReportView(LoginRequiredMixin, PermissionRequiredMixin, View):
+ permissions = ["member.change_profile"]
+
+ def post(self, request, *args, **kwargs):
+ alert = get_object_or_404(Alert, pk=kwargs["alert_pk"], solved=False, scope="PROFILE")
+ text = request.POST.get("text", "")
+ if text:
+ msg_title = _("Signalement traité : profil de {}").format(alert.profile.user.username)
+ msg_content = render_to_string(
+ "member/messages/alert_solved.md",
+ {
+ "alert_author": alert.author.username,
+ "reported_user": alert.profile.user.username,
+ "moderator": request.user.username,
+ "staff_message": text,
+ },
+ )
+ alert.solve(request.user, text, msg_title, msg_content)
+ else:
+ alert.solve(request.user)
+ messages.success(request, _("Merci, l'alerte a bien été résolue."))
+ return redirect(alert.profile.get_absolute_url())
diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py
index ebc436bbc3..bb1302acd4 100644
--- a/zds/tutorialv2/tests/tests_views/tests_content.py
+++ b/zds/tutorialv2/tests/tests_views/tests_content.py
@@ -3581,1926 +3581,3 @@ def test_no_invalid_titles(self):
self.assertEqual(result.status_code, 302)
self.assertNotEqual(PublishableContent.objects.all().count(), prev_count)
prev_count += 1
-
-
-@override_for_contents()
-class PublishedContentTests(TutorialTestMixin, TestCase):
- def setUp(self):
- self.overridden_zds_app["content"]["default_licence_pk"] = LicenceFactory().pk
- # don't build PDF to speed up the tests
- self.overridden_zds_app["content"]["build_pdf_when_published"] = False
-
- self.staff = StaffProfileFactory().user
-
- settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
- self.mas = ProfileFactory().user
- self.overridden_zds_app["member"]["bot_account"] = self.mas.username
-
- bot = Group(name=self.overridden_zds_app["member"]["bot_group"])
- bot.save()
- self.external = UserFactory(username=self.overridden_zds_app["member"]["external_account"], password="anything")
-
- self.beta_forum = ForumFactory(
- pk=self.overridden_zds_app["forum"]["beta_forum_id"],
- category=ForumCategoryFactory(position=1),
- position_in_category=1,
- ) # ensure that the forum, for the beta versions, is created
-
- self.licence = LicenceFactory()
- self.subcategory = SubCategoryFactory()
-
- self.user_author = ProfileFactory().user
- self.user_staff = StaffProfileFactory().user
- self.user_guest = ProfileFactory().user
-
- self.hat, _ = Hat.objects.get_or_create(name__iexact="A hat", defaults={"name": "A hat"})
- self.user_guest.profile.hats.add(self.hat)
-
- # create a tutorial
- self.tuto = PublishableContentFactory(type="TUTORIAL")
- self.tuto.authors.add(self.user_author)
- UserGalleryFactory(gallery=self.tuto.gallery, user=self.user_author, mode="W")
- self.tuto.licence = self.licence
- self.tuto.subcategory.add(self.subcategory)
- self.tuto.save()
-
- # fill it with one part, containing one chapter, containing one extract
- self.tuto_draft = self.tuto.load_version()
- self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto)
- self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto)
- self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto)
-
- # then, publish it !
- version = self.tuto_draft.current_version
- self.published = publish_content(self.tuto, self.tuto_draft, is_major_update=True)
-
- self.tuto.sha_public = version
- self.tuto.sha_draft = version
- self.tuto.public_version = self.published
- self.tuto.save()
-
- def test_published(self):
- """Just a small test to ensure that the setUp() function produce a proper published content"""
-
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": self.tuto.pk, "slug": self.tuto.slug}))
- self.assertEqual(result.status_code, 200)
-
- # test access for guest user
- self.client.force_login(self.user_guest)
-
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": self.tuto.pk, "slug": self.tuto.slug}))
- self.assertEqual(result.status_code, 200)
-
- def test_public_access(self):
- """Test that everybody have access to a content after its publication"""
-
- text_validation = "Valide moi ce truc, please !"
- text_publication = "Aussi tôt dit, aussi tôt fait !"
-
- # 1. Article:
- article = PublishableContentFactory(type="ARTICLE")
-
- article.authors.add(self.user_author)
- UserGalleryFactory(gallery=article.gallery, user=self.user_author, mode="W")
- article.licence = self.licence
- article.save()
-
- # populate the article
- article_draft = article.load_version()
- ExtractFactory(container=article_draft, db_object=article)
- ExtractFactory(container=article_draft, db_object=article)
-
- # connect with author:
- self.client.force_login(self.user_author)
-
- # ask validation
- self.assertEqual(Validation.objects.count(), 0)
-
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "version": article_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff and publish
- self.client.force_login(self.user_staff)
-
- validation = Validation.objects.filter(content=article).last()
-
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # accept
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- published = PublishedContent.objects.filter(content=article).first()
- self.assertIsNotNone(published)
-
- # test access to staff
- result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- # test access to public
- self.client.logout()
- result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- # test access for guest user
- self.client.force_login(self.user_guest)
- result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- # 2. middle-size tutorial (just to test the access to chapters)
- midsize_tuto = PublishableContentFactory(type="TUTORIAL")
-
- midsize_tuto.authors.add(self.user_author)
- UserGalleryFactory(gallery=midsize_tuto.gallery, user=self.user_author, mode="W")
- midsize_tuto.licence = self.licence
- midsize_tuto.save()
-
- # populate the midsize_tuto
- midsize_tuto_draft = midsize_tuto.load_version()
- chapter1 = ContainerFactory(parent=midsize_tuto_draft, db_object=midsize_tuto)
- ExtractFactory(container=chapter1, db_object=midsize_tuto)
-
- # connect with author:
- self.client.force_login(self.user_author)
-
- # ask validation
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto.slug}),
- {"text": text_validation, "version": midsize_tuto_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff and publish
- self.client.force_login(self.user_staff)
-
- validation = Validation.objects.filter(content=midsize_tuto).last()
-
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # accept
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- published = PublishedContent.objects.filter(content=midsize_tuto).first()
- self.assertIsNotNone(published)
-
- # test access to staff
- result = self.client.get(
- reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug})
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # test access to public
- self.client.logout()
- result = self.client.get(
- reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug})
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # test access for guest user
- self.client.force_login(self.user_guest)
- result = self.client.get(
- reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug})
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # 3. a big tutorial (just to test the access to parts and chapters)
- bigtuto = PublishableContentFactory(type="TUTORIAL")
-
- bigtuto.authors.add(self.user_author)
- UserGalleryFactory(gallery=bigtuto.gallery, user=self.user_author, mode="W")
- bigtuto.licence = self.licence
- bigtuto.save()
-
- # populate the bigtuto
- bigtuto_draft = bigtuto.load_version()
- part1 = ContainerFactory(parent=bigtuto_draft, db_object=bigtuto)
- chapter1 = ContainerFactory(parent=part1, db_object=bigtuto)
- ExtractFactory(container=chapter1, db_object=bigtuto)
-
- # connect with author:
- self.client.force_login(self.user_author)
-
- # ask validation
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}),
- {"text": text_validation, "version": bigtuto_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff and publish
- self.client.force_login(self.user_staff)
-
- validation = Validation.objects.filter(content=bigtuto).last()
-
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # accept
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {
- "text": text_publication,
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- published = PublishedContent.objects.filter(content=bigtuto).first()
- self.assertIsNotNone(published)
-
- # test access to staff
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # test access to public
- self.client.logout()
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # test access for guest user
- self.client.force_login(self.user_guest)
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 200)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 200)
-
- # just for the fun of it, lets then revoke publication
- self.client.force_login(self.user_staff)
-
- result = self.client.post(
- reverse("validation:revoke", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}),
- {"text": "Pour le fun", "version": bigtuto_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # now, let's get a whole bunch of good old fashioned 404 (and not 403 or 302 !!)
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 404)
-
- # test access to public
- self.client.logout()
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 404)
-
- # test access for guest user
- self.client.force_login(self.user_guest)
-
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug}))
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug},
- )
- )
- self.assertEqual(result.status_code, 404)
-
- result = self.client.get(
- reverse(
- "tutorial:view-container",
- kwargs={
- "pk": bigtuto.pk,
- "slug": bigtuto_draft.slug,
- "parent_container_slug": part1.slug,
- "container_slug": chapter1.slug,
- },
- )
- )
- self.assertEqual(result.status_code, 404)
-
- def test_add_note(self):
-
- message_to_post = "la ZEP-12"
-
- self.client.force_login(self.user_guest)
-
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.published.content.pk}",
- {"text": message_to_post, "last_note": 0, "with_hat": self.hat.pk},
- follow=True,
- )
- self.assertEqual(result.status_code, 200)
- self.assertEqual(ContentReaction.objects.latest("pubdate").hat, self.hat)
-
- reactions = ContentReaction.objects.all()
- self.assertEqual(len(reactions), 1)
- self.assertEqual(reactions[0].text, message_to_post)
-
- reads = ContentRead.objects.filter(user=self.user_guest).all()
- self.assertEqual(len(reads), 1)
- self.assertEqual(reads[0].content.pk, self.tuto.pk)
- self.assertEqual(reads[0].note.pk, reactions[0].pk)
-
- self.assertEqual(
- self.client.get(reverse("tutorial:view", args=[self.tuto.pk, self.tuto.slug])).status_code, 200
- )
- result = self.client.post(
- reverse("content:add-reaction") + f"?clementine={self.published.content.pk}",
- {"text": message_to_post, "last_note": "0"},
- follow=True,
- )
- self.assertEqual(result.status_code, 404)
-
- # visit the tutorial trigger the creation of a ContentRead
- self.client.force_login(self.user_staff)
-
- self.assertEqual(
- 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()
- # simple visit does not trigger follow but remembers reading
- self.assertEqual(len(reads), 1)
- interventions = [
- post["url"] for post in get_header_notifications(self.user_staff)["general_notifications"]["list"]
- ]
- self.assertTrue(reads.first().note.get_absolute_url() not in interventions)
-
- # login with author
- self.client.force_login(self.user_author)
-
- # test preview (without JS)
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.published.content.pk}",
- {"text": message_to_post, "last_note": reactions[0].pk, "preview": True},
- )
- self.assertEqual(result.status_code, 200)
-
- self.assertTrue(message_to_post in result.context["text"])
-
- # test preview (with JS)
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.published.content.pk}",
- {"text": message_to_post, "last_note": reactions[0].pk, "preview": True},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- self.assertEqual(result.status_code, 200)
-
- result_string = "".join(a.decode() for a in result.streaming_content)
- self.assertTrue(message_to_post in result_string)
-
- # test quoting (without JS)
- result = self.client.get(
- reverse("content:add-reaction") + "?pk={}&cite={}".format(self.published.content.pk, reactions[0].pk)
- )
- self.assertEqual(result.status_code, 200)
-
- text_field_value = result.context["form"].initial["text"]
-
- self.assertTrue(message_to_post in text_field_value)
- self.assertTrue(self.user_guest.username in text_field_value)
- self.assertTrue(reactions[0].get_absolute_url() in text_field_value)
-
- # test quoting (with JS)
- result = self.client.get(
- reverse("content:add-reaction") + "?pk={}&cite={}".format(self.published.content.pk, reactions[0].pk),
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
-
- self.assertEqual(result.status_code, 200)
- json = {}
-
- try:
- json = json_handler.loads("".join(a.decode() for a in result.streaming_content))
- except Exception as e: # broad exception on purpose
- self.assertEqual(e, "")
-
- self.assertTrue("text" in json)
- text_field_value = json["text"]
-
- self.assertTrue(message_to_post in text_field_value)
- self.assertTrue(self.user_guest.username in text_field_value)
- self.assertTrue(reactions[0].get_absolute_url() in text_field_value)
-
- # test that if the wrong last_note is given, user get a message
- self.assertEqual(ContentReaction.objects.count(), 1)
-
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.published.content.pk}",
- {"text": message_to_post, "last_note": -1}, # wrong pk
- follow=False,
- )
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(ContentReaction.objects.count(), 1) # no new reaction has been posted
- self.assertTrue(result.context["newnote"]) # message appears !
-
- def test_hide_reaction(self):
- text_hidden = (
- "Ever notice how you come across somebody once in a while you shouldn't have fucked with? That's me."
- )
-
- self.client.force_login(self.user_guest)
-
- self.client.post(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}",
- {"text": "message", "last_note": "0"},
- follow=True,
- )
-
- self.client.force_login(self.user_staff)
-
- reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first()
-
- result = self.client.post(
- reverse("content:hide-reaction", args=[reaction.pk]), {"text_hidden": text_hidden}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- reaction = ContentReaction.objects.get(pk=reaction.pk)
- self.assertFalse(reaction.is_visible)
- 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.client.force_login(self.user_guest)
-
- result = self.client.get(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}&cite={reaction.pk}", follow=False
- )
- self.assertEqual(result.status_code, 403) # unable to quote a reaction if hidden
-
- # then, unhide it !
- self.client.force_login(self.user_guest)
-
- result = self.client.post(reverse("content:show-reaction", args=[reaction.pk]), follow=False)
-
- self.assertEqual(result.status_code, 302)
-
- reaction = ContentReaction.objects.get(pk=reaction.pk)
- self.assertTrue(reaction.is_visible)
-
- def test_alert_reaction(self):
-
- self.client.force_login(self.user_guest)
-
- self.client.post(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}",
- {"text": "message", "last_note": "0"},
- follow=True,
- )
- reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first()
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("content:alert-reaction", args=[reaction.pk]),
- {"signal_text": "No. Try not. Do... or do not. There is no try."},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertIsNotNone(Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first())
- result = self.client.post(
- reverse("content:resolve-reaction"),
- {
- "alert_pk": Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first().pk,
- "text": "No. Try not. Do... or do not. There is no try.",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 403)
- self.client.force_login(self.user_staff)
- result = self.client.post(
- reverse("content:resolve-reaction"),
- {
- "alert_pk": Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first().pk,
- "text": "Much to learn, you still have.",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertIsNone(
- Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first()
- )
- reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first()
-
- # test that edition of a comment with an alert by an admin also solve the alert
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("content:alert-reaction", args=[reaction.pk]),
- {"signal_text": "No. Try not. Do... or do not. There is no try."},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertIsNotNone(
- Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first()
- )
-
- self.client.force_login(self.user_staff)
- result = self.client.post(
- reverse("content:update-reaction") + f"?message={reaction.pk}&pk={self.tuto.pk}",
- {"text": "Much to learn, you still have."},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertIsNone(
- Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first()
- )
-
- def test_warn_typo_without_accessible_author(self):
-
- self.client.force_login(self.user_guest)
- result = self.client.post(
- reverse("content:warn-typo") + f"?pk={self.tuto.pk}",
- {
- "pk": self.tuto.pk,
- "version": self.published.sha_public,
- "text": "This is how they controlled it. "
- "It took us 15 years and three supercomputers to MacGyver a system for the gate on Earth. ",
- "target": "",
- },
- follow=True,
- )
- self.assertEqual(result.status_code, 200)
- self.assertIsNone(PrivateTopic.objects.filter(participants__in=[self.external]).first())
-
- # add a banned user:
- user_banned = ProfileFactory(
- can_write=False,
- end_ban_write=datetime.date(2048, 0o1, 0o1),
- can_read=False,
- end_ban_read=datetime.date(2048, 0o1, 0o1),
- )
- self.tuto.authors.add(user_banned.user)
- self.tuto.save()
-
- result = self.client.post(
- reverse("content:warn-typo") + f"?pk={self.tuto.pk}",
- {
- "pk": self.tuto.pk,
- "version": self.published.sha_public,
- "text": "This is how they controlled it. "
- "It took us 15 years and three supercomputers to MacGyver a system for the gate on Earth. ",
- "target": "",
- },
- follow=True,
- )
- self.assertIsNone(PrivateTopic.objects.filter(participants__in=[self.external]).first())
- self.assertEqual(result.status_code, 200)
-
- def test_find_tutorial_or_article(self):
- """test the behavior of `article:find-article` and `tutorial:find-tutorial` urls"""
-
- self.client.force_login(self.user_author)
-
- tuto_in_beta = PublishableContentFactory(type="TUTORIAL")
- tuto_in_beta.authors.add(self.user_author)
- tuto_in_beta.sha_beta = "whatever"
- tuto_in_beta.save()
-
- tuto_draft = PublishableContentFactory(type="TUTORIAL")
- tuto_draft.authors.add(self.user_author)
- tuto_draft.save()
-
- article_in_validation = PublishableContentFactory(type="ARTICLE")
- article_in_validation.authors.add(self.user_author)
- article_in_validation.sha_validation = "whatever" # the article is in validation
- article_in_validation.save()
-
- # test without filters
- response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False)
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 3) # 3 tutorials
-
- response = self.client.get(reverse("article:find-article", args=[self.user_author.username]), follow=False)
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 1) # 1 article
-
- # test a non-existing filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=whatever", follow=False
- )
- self.assertEqual(404, response.status_code) # this filter does not exists !
-
- # test 'redaction' filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one tutorial in redaction
- self.assertEqual(contents[0].pk, tuto_draft.pk)
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=redaction", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no article in redaction
-
- # test beta filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one tutorial in beta
- self.assertEqual(contents[0].pk, tuto_in_beta.pk)
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=beta", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no article in beta
-
- # test validation filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 0) # no tutorial in validation
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=validation", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 1) # one article in validation
- self.assertEqual(contents[0].pk, article_in_validation.pk)
-
- # test public filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=public", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one published tutorial
- self.assertEqual(contents[0].pk, self.tuto.pk)
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=public", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no published article
-
- self.client.logout()
-
- # test validation filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False
- )
- self.assertEqual(403, response.status_code) # not allowed for public
-
- # test redaction filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False
- )
- self.assertEqual(403, response.status_code) # not allowed for public
-
- # test beta filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one tutorial in beta
- self.assertEqual(contents[0].pk, tuto_in_beta.pk)
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=beta", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no article in beta
-
- # test public filter
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=public", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one published tutorial
- self.assertEqual(contents[0].pk, self.tuto.pk)
-
- response = self.client.get(
- reverse("article:find-article", args=[self.user_author.username]) + "?filter=public", follow=False
- )
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no published article
-
- # test no filter → same answer as 'public'
- response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False)
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # one published tutorial
- self.assertEqual(contents[0].pk, self.tuto.pk)
-
- response = self.client.get(reverse("article:find-article", args=[self.user_author.username]), follow=False)
- self.assertEqual(200, response.status_code)
- contents = response.context["articles"]
- self.assertEqual(len(contents), 0) # no published article
-
- self.client.force_login(self.user_staff)
-
- response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False)
- self.assertEqual(200, response.status_code)
- contents = response.context["tutorials"]
- self.assertEqual(len(contents), 1) # 1 published tutorial by user_author !
-
- # staff can use all filters without a 403 !
-
- # test validation filter:
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False
- )
- self.assertEqual(403, response.status_code)
-
- # test redaction filter:
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False
- )
- self.assertEqual(403, response.status_code)
-
- # test beta filter:
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False
- )
- self.assertEqual(200, response.status_code)
-
- # test redaction filter:
- response = self.client.get(
- reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False
- )
- self.assertEqual(403, response.status_code)
-
- def test_last_reactions(self):
- """Test and ensure the behavior of last_read_note() and first_unread_note().
-
- Note: for a unknown reason, `get_current_user()` does not return the good answer if a page is not
- visited before, therefore this test will visit the index after each login (because :p)"""
-
- # login with guest
- self.client.force_login(self.user_guest)
-
- result = self.client.get(reverse("pages-index")) # go to whatever page
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 0)
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
-
- # no reaction yet:
- self.assertIsNone(tuto.last_read_note())
- self.assertIsNone(tuto.first_unread_note())
- self.assertIsNone(tuto.first_note())
-
- # post a reaction
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}",
- {"text": "message", "last_note": "0"},
- follow=True,
- )
- self.assertEqual(result.status_code, 200)
-
- reactions = ContentReaction.objects.filter(related_content=self.tuto).all()
- self.assertEqual(len(reactions), 1)
-
- self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1) # last reaction read
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
-
- self.assertEqual(tuto.first_note(), reactions[0])
- self.assertEqual(tuto.last_read_note(), reactions[0])
- self.assertEqual(tuto.first_unread_note(), reactions[0]) # if no next reaction, first unread=last read
-
- self.client.logout()
-
- # login with author (could be staff, we don't care in this test)
- self.client.force_login(self.user_author)
-
- result = self.client.get(reverse("pages-index")) # go to whatever page
- self.assertEqual(result.status_code, 200)
-
- self.assertIsNone(
- ContentReactionAnswerSubscription.objects.get_existing(user=self.user_author, content_object=tuto)
- )
-
- self.assertEqual(tuto.last_read_note(), reactions[0]) # if never read, last note=first note
- self.assertEqual(tuto.first_unread_note(), reactions[0])
-
- # post another reaction
- result = self.client.post(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}",
- {"text": "message", "last_note": reactions[0].pk},
- follow=True,
- )
- self.assertEqual(result.status_code, 200)
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
-
- reactions = list(ContentReaction.objects.filter(related_content=self.tuto).all())
- self.assertEqual(len(reactions), 2)
-
- self.assertTrue(
- ContentReactionAnswerSubscription.objects.get_existing(user=self.user_author, content_object=tuto).is_active
- )
-
- self.assertEqual(tuto.first_note(), reactions[0]) # first note is still first note
- self.assertEqual(tuto.last_read_note(), reactions[1])
- self.assertEqual(tuto.first_unread_note(), reactions[1])
-
- # test if not connected
- self.client.logout()
-
- result = self.client.get(reverse("pages-index")) # go to whatever page
- self.assertEqual(result.status_code, 200)
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertEqual(tuto.last_read_note(), reactions[0]) # last read note = first note
- self.assertEqual(tuto.first_unread_note(), reactions[0]) # first unread note = first note
-
- # visit tutorial
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": tuto.pk, "slug": tuto.slug}))
- self.assertEqual(result.status_code, 200)
-
- # but nothing has changed (because not connected = no notifications and no 'tracking')
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertEqual(tuto.last_read_note(), reactions[0]) # last read note = first note
- self.assertEqual(tuto.first_unread_note(), reactions[0]) # first unread note = first note
-
- # re-login with guest
- self.client.force_login(self.user_guest)
-
- result = self.client.get(reverse("pages-index")) # go to whatever page
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1) # already read first reaction
- reads = ContentRead.objects.filter(user=self.user_guest).all()
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertEqual(tuto.last_read_note(), reactions[0])
- self.assertEqual(tuto.first_unread_note(), reactions[1]) # new reaction of author is unread
-
- # visit tutorial to get rid of the notification
- result = self.client.get(reverse("tutorial:view", kwargs={"pk": tuto.pk, "slug": tuto.slug}))
- self.assertEqual(result.status_code, 200)
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertEqual(tuto.last_read_note(), reactions[1]) # now, new reaction is read !
- self.assertEqual(tuto.first_unread_note(), reactions[1])
-
- self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1)
- self.assertNotEqual(reads, ContentRead.objects.filter(user=self.user_guest).all()) # not the same message
-
- self.client.logout()
-
- result = self.client.get(reverse("pages-index")) # go to whatever page
- self.assertEqual(result.status_code, 200)
-
- def test_reaction_follow_email(self):
- settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
- self.assertEqual(0, len(mail.outbox))
-
- profile = ProfileFactory()
- self.client.force_login(profile.user)
- response = self.client.post(reverse("content:follow-reactions", args=[self.tuto.pk]), {"email": "1"})
- self.assertEqual(302, response.status_code)
-
- self.assertIsNotNone(
- ContentReactionAnswerSubscription.objects.get_existing(
- profile.user, self.tuto, is_active=True, by_email=True
- )
- )
-
- self.client.logout()
-
- self.client.force_login(self.user_author)
-
- # post another reaction
- self.client.post(
- reverse("content:add-reaction") + f"?pk={self.tuto.pk}",
- {"text": "message", "last_note": "0"},
- follow=True,
- )
-
- self.assertEqual(1, len(mail.outbox))
-
- def test_note_with_bad_param(self):
- self.client.force_login(self.user_staff)
- 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 test_cant_edit_not_owned_note(self):
- article = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE")
- new_user = ProfileFactory().user
- new_reaction = ContentReaction(related_content=article, position=1)
- new_reaction.author = self.user_guest
- new_reaction.update_content("I will find you.")
-
- new_reaction.save()
- self.client.force_login(new_user)
- resp = self.client.get(reverse("content:update-reaction") + f"?message={new_reaction.pk}&pk={article.pk}")
- self.assertEqual(403, resp.status_code)
- resp = self.client.post(
- reverse("content:update-reaction") + f"?message={new_reaction.pk}&pk={article.pk}",
- {"text": "I edited it"},
- )
- self.assertEqual(403, resp.status_code)
-
- def test_quote_note(self):
- """Ensure the behavior of the `&cite=xxx` parameter on 'content:add-reaction'"""
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- text = (
- "À force de temps, de patience et de crachats, "
- "on met un pépin de callebasse dans le derrière d'un moustique (proverbe créole)"
- )
-
- # add note :
- reaction = ContentReaction(related_content=tuto, position=1)
- reaction.author = self.user_guest
- reaction.update_content(text)
- reaction.save()
-
- self.client.force_login(self.user_author)
-
- # cite note
- result = self.client.get(reverse("content:add-reaction") + f"?pk={tuto.pk}&cite={reaction.pk}", follow=True)
- self.assertEqual(200, result.status_code)
-
- self.assertTrue(text in result.context["form"].initial["text"]) # ok, text quoted !
-
- # cite with a abnormal parameter raises 404
- result = self.client.get(
- reverse("content:add-reaction") + "?pk={}&cite={}".format(tuto.pk, "lililol"), follow=True
- )
- self.assertEqual(404, result.status_code)
-
- # cite not existing note just gives the form empty
- result = self.client.get(
- reverse("content:add-reaction") + "?pk={}&cite={}".format(tuto.pk, 99999999), follow=True
- )
- self.assertEqual(200, result.status_code)
-
- self.assertTrue("text" not in result.context["form"]) # nothing quoted, so no text cited
-
- # it's not possible to cite an hidden note (get 403)
- reaction.is_visible = False
- reaction.save()
-
- result = self.client.get(reverse("content:add-reaction") + f"?pk={tuto.pk}&cite={reaction.pk}", follow=True)
- self.assertEqual(403, result.status_code)
-
- def test_cant_view_private_even_if_draft_is_equal_to_public(self):
- content = PublishedContentFactory(author_list=[self.user_author])
- self.client.force_login(self.user_guest)
- resp = self.client.get(reverse("content:view", args=[content.pk, content.slug]))
- self.assertEqual(403, resp.status_code)
-
- def test_republish_with_different_slug(self):
- """Ensure that a new PublishedContent object is created and well filled"""
-
- self.assertEqual(PublishedContent.objects.count(), 1)
-
- old_published = PublishedContent.objects.filter(content__pk=self.tuto.pk).first()
- self.assertIsNotNone(old_published)
- self.assertFalse(old_published.must_redirect)
-
- # connect with author:
- self.client.force_login(self.user_author)
-
- # change title
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- old_slug = tuto.slug
- random = "Whatever, we don't care about the details"
-
- result = self.client.post(
- reverse("content:edit", args=[tuto.pk, tuto.slug]),
- {
- "title": "{} ({})".format(self.tuto.title, "modified"), # will change slug
- "description": random,
- "introduction": random,
- "conclusion": random,
- "type": "TUTORIAL",
- "licence": self.tuto.licence.pk,
- "subcategory": self.subcategory.pk,
- "last_hash": tuto.load_version().compute_hash(),
- "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"),
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertNotEqual(tuto.slug, old_slug)
-
- # ask validation
- text_validation = "Valide moi ce truc, please !"
- text_publication = "Aussi tôt dit, aussi tôt fait !"
- self.assertEqual(Validation.objects.count(), 0)
-
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": tuto.pk, "slug": tuto.slug}),
- {"text": text_validation, "version": tuto.sha_draft},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff and publish
- self.client.force_login(self.user_staff)
-
- validation = Validation.objects.filter(content__pk=tuto.pk).last()
-
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # accept
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": False}, # minor modification (just the title)
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- self.assertEqual(PublishedContent.objects.count(), 2)
-
- old_published = PublishedContent.objects.get(pk=old_published.pk)
- self.assertIsNotNone(old_published) # still exists
- self.assertTrue(old_published.must_redirect) # do redirection if any
- self.assertIsNone(old_published.update_date)
-
- new_published = PublishedContent.objects.filter(content__pk=self.tuto.pk).last()
- self.assertIsNotNone(new_published) # new version exists
- self.assertNotEqual(old_published.pk, new_published.pk) # not the old one
- self.assertEqual(new_published.publication_date, old_published.publication_date) # keep publication date
- self.assertIsNotNone(new_published.update_date) # ... But is updated !
-
- tuto = PublishableContent.objects.get(pk=self.tuto.pk)
- self.assertEqual(tuto.public_version.pk, new_published.pk)
-
- def test_logical_article_pagination(self):
- """Test that the past is 'left' and the future is 'right', or in other word that the good article
- are given to pagination and not the opposite"""
-
- article1 = PublishedContentFactory(type="ARTICLE")
-
- # force 'article1' to be the first one by setting a publication date equals to one hour before the test
- article1.public_version.publication_date = datetime.datetime.now() - datetime.timedelta(0, 1)
- article1.public_version.save()
-
- article2 = PublishedContentFactory(type="ARTICLE")
-
- self.assertEqual(PublishedContent.objects.count(), 3) # both articles have been published
-
- # visit article 1 (so article2 is next)
- result = self.client.get(reverse("article:view", kwargs={"pk": article1.pk, "slug": article1.slug}))
- self.assertEqual(result.status_code, 200)
- self.assertEqual(result.context["next_content"].pk, article2.public_version.pk)
- self.assertIsNone(result.context["previous_content"])
-
- # visit article 2 (so article1 is previous)
- result = self.client.get(reverse("article:view", kwargs={"pk": article2.pk, "slug": article2.slug}))
- self.assertEqual(result.status_code, 200)
- self.assertEqual(result.context["previous_content"].pk, article1.public_version.pk)
- self.assertIsNone(result.context["next_content"])
-
- def test_validation_list_has_good_title(self):
- # aka fix 3172
- tuto = PublishableContentFactory(author_list=[self.user_author], type="TUTORIAL")
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("validation:ask", args=[tuto.pk, tuto.slug]),
- {"text": "something good", "version": tuto.sha_draft},
- follow=False,
- )
- old_title = tuto.title
- new_title = "a brand new title"
- self.client.post(
- reverse("content:edit", args=[tuto.pk, tuto.slug]),
- {
- "title": new_title,
- "description": tuto.description,
- "introduction": "a",
- "conclusion": "b",
- "type": "TUTORIAL",
- "licence": self.licence.pk,
- "subcategory": self.subcategory.pk,
- "last_hash": tuto.sha_draft,
- },
- follow=False,
- )
- self.client.logout()
- self.client.force_login(self.user_staff)
- result = self.client.get(reverse("validation:list") + "?type=tuto")
- self.assertIn(old_title, str(result.content))
- self.assertNotIn(new_title, str(result.content))
-
- def test_public_authors_versioned(self):
- published = PublishedContentFactory(author_list=[self.user_author])
- other_author = ProfileFactory()
- published.authors.add(other_author.user)
- published.save()
- response = self.client.get(published.get_absolute_url_online())
- self.assertIn(self.user_author.username, str(response.content))
- self.assertNotIn(other_author.user.username, str(response.content))
- self.assertEqual(0, len(other_author.get_public_contents()))
-
- def test_unpublish_with_title_change(self):
- # aka 3329
- article = PublishedContentFactory(type="ARTICLE", author_list=[self.user_author], licence=self.licence)
- registered_validation = Validation(
- content=article,
- version=article.sha_draft,
- status="ACCEPT",
- comment_authors="bla",
- comment_validator="bla",
- date_reserve=datetime.datetime.now(),
- date_proposition=datetime.datetime.now(),
- date_validation=datetime.datetime.now(),
- )
- registered_validation.save()
- self.client.force_login(self.user_staff)
- self.client.post(
- reverse("content:edit", args=[article.pk, article.slug]),
- {
- "title": "new title so that everything explode",
- "description": article.description,
- "introduction": article.load_version().get_introduction(),
- "conclusion": article.load_version().get_conclusion(),
- "type": "ARTICLE",
- "licence": article.licence.pk,
- "subcategory": self.subcategory.pk,
- "last_hash": article.load_version(article.sha_draft).compute_hash(),
- "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"),
- },
- follow=False,
- )
- public_count = PublishedContent.objects.count()
- result = self.client.post(
- reverse("validation:revoke", kwargs={"pk": article.pk, "slug": article.public_version.content_public_slug}),
- {"text": "This content was bad", "version": article.public_version.sha_public},
- follow=False,
- )
- self.assertEqual(302, result.status_code)
- self.assertEqual(public_count - 1, PublishedContent.objects.count())
- self.assertEqual("PENDING", Validation.objects.get(pk=registered_validation.pk).status)
-
- def test_unpublish_with_empty_subscription(self):
- article = PublishedContentFactory(type="ARTICLE", author_list=[self.user_author], licence=self.licence)
- registered_validation = Validation(
- content=article,
- version=article.sha_draft,
- status="ACCEPT",
- comment_authors="bla",
- comment_validator="bla",
- date_reserve=datetime.datetime.now(),
- date_proposition=datetime.datetime.now(),
- date_validation=datetime.datetime.now(),
- )
- registered_validation.save()
- subscriber = ProfileFactory().user
- self.client.force_login(subscriber)
- resp = self.client.post(reverse("content:follow-reactions", args=[article.pk]), {"follow": True})
- self.assertEqual(302, resp.status_code)
- public_count = PublishedContent.objects.count()
- self.client.logout()
- self.client.force_login(self.user_staff)
- result = self.client.post(
- reverse("validation:revoke", kwargs={"pk": article.pk, "slug": article.public_version.content_public_slug}),
- {"text": "This content was bad", "version": article.public_version.sha_public},
- follow=False,
- )
- self.assertEqual(302, result.status_code)
- self.assertEqual(public_count - 1, PublishedContent.objects.count())
- self.assertEqual(ContentReactionAnswerSubscription.objects.filter(is_active=False).count(), 1)
-
- def test_validation_history(self):
- published = PublishedContentFactory(author_list=[self.user_author])
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("content:edit", args=[published.pk, published.slug]),
- {
- "title": published.title,
- "description": published.description,
- "introduction": "crappy crap",
- "conclusion": "crappy crap",
- "type": "TUTORIAL",
- "licence": self.licence.pk,
- "subcategory": self.subcategory.pk,
- "last_hash": published.load_version().compute_hash(), # good hash
- },
- follow=True,
- )
- self.assertEqual(result.status_code, 200)
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": published.pk, "slug": published.slug}),
- {"text": "abcdefg", "version": published.load_version().current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertEqual(Validation.objects.count(), 1)
- self.client.logout()
- self.client.force_login(self.user_staff)
- result = self.client.get(reverse("validation:list") + "?type=tuto")
- self.assertIn('class="update_content"', str(result.content))
-
- def test_validation_history_for_new_content(self):
- publishable = PublishableContentFactory(author_list=[self.user_author])
- self.client.force_login(self.user_author)
-
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": publishable.pk, "slug": publishable.slug}),
- {"text": "abcdefg", "version": publishable.load_version().current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertEqual(Validation.objects.count(), 1)
- self.client.logout()
- self.client.force_login(self.user_staff)
- result = self.client.get(reverse("validation:list") + "?type=tuto")
- self.assertNotIn('class="update_content"', str(result.content))
-
- def test_ask_validation_update(self):
- """
- Test AskValidationView.
- """
- text_validation = "La validation on vous aime !"
- content = PublishableContentFactory(author_list=[self.user_author], type="ARTICLE")
- content.save()
- content_draft = content.load_version()
-
- self.assertEqual(Validation.objects.count(), 0)
-
- # login with user and ask validation
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}),
- {"text": text_validation, "version": content_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- self.assertEqual(Validation.objects.count(), 1)
-
- # login with staff and reserve the content
- self.client.force_login(self.user_staff)
- validation = Validation.objects.filter(content=content).last()
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # login with user, edit content and ask validation for update
- self.client.force_login(self.user_author)
- result = self.client.post(
- reverse("content:edit", args=[content_draft.pk, content_draft.slug]),
- {
- "title": content_draft.title + "2",
- "description": content_draft.description,
- "introduction": content_draft.introduction,
- "conclusion": content_draft.conclusion,
- "type": content_draft.type,
- "licence": self.licence.pk,
- "subcategory": self.subcategory.pk,
- "last_hash": content_draft.compute_hash(),
- "image": content_draft.image or "None",
- },
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}),
- {"text": text_validation, "version": content_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # ensure the validation is effective
- self.assertEqual(Validation.objects.count(), 2)
- self.assertIsNotNone(Validation.objects.last().date_reserve) # issue #3432
-
- def test_beta_article_closed_when_published(self):
- """Test that the beta of an article is locked when the content is published"""
-
- text_validation = "Valide moi ce truc !"
- text_publication = "Validation faite !"
-
- article = PublishableContentFactory(type="ARTICLE")
- article.authors.add(self.user_author)
- article.save()
- article_draft = article.load_version()
-
- # login with author:
- self.client.force_login(self.user_author)
-
- # set beta
- result = self.client.post(
- reverse("content:set-beta", kwargs={"pk": article.pk, "slug": article.slug}),
- {"version": article_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # ask validation
- self.assertEqual(Validation.objects.count(), 0)
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "version": article_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff
- self.client.force_login(self.user_staff)
-
- # reserve the article
- validation = Validation.objects.filter(content=article).last()
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # Check that the staff user doesn't have a notification for their reservation and their private topic is read.
- self.assertEqual(0, len(Notification.objects.get_unread_notifications_of(self.user_staff)))
- last_pm = PublishableContent.objects.get(pk=article.pk).validation_private_message
- # as staff decided the action, he does not need to be notified of the new mp
- self.assertFalse(is_privatetopic_unread(last_pm, self.user_staff))
- self.assertTrue(is_privatetopic_unread(last_pm, self.user_author))
- # publish the article
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- beta_topic = PublishableContent.objects.get(pk=article.pk).beta_topic
- self.assertIsNotNone(beta_topic)
- self.assertTrue(beta_topic.is_locked)
- last_message = beta_topic.last_message
- self.assertIsNotNone(last_message)
-
- # login with author to ensure that the beta is not closed if it was already closed (at a second validation).
- self.client.force_login(self.user_author)
-
- # ask validation
- result = self.client.post(
- reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "version": article_draft.current_version},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
-
- # login with staff
- self.client.force_login(self.user_staff)
-
- # reserve the article
- validation = Validation.objects.filter(content=article).last()
- result = self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.assertEqual(result.status_code, 302)
-
- # publish the article
- result = self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True},
- follow=False,
- )
- self.assertEqual(result.status_code, 302)
- beta_topic = PublishableContent.objects.get(pk=article.pk).beta_topic
- self.assertIsNotNone(beta_topic)
- self.assertTrue(beta_topic.is_locked)
- self.assertEqual(beta_topic.last_message, last_message)
-
- def test_obsolete(self):
- # check that this function is only available for staff
- self.client.force_login(self.user_author)
- result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False)
- self.assertEqual(result.status_code, 403)
- # login as staff
- self.client.force_login(self.user_staff)
- # check that when the content is not marked as obsolete, the alert is not shown
- result = self.client.get(self.tuto.get_absolute_url_online(), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertNotContains(result, _("Ce contenu est obsolète."))
- # now, let's mark the tutoriel as obsolete
- result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False)
- self.assertEqual(result.status_code, 302)
- # check that the alert is shown
- result = self.client.get(self.tuto.get_absolute_url_online(), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, _("Ce contenu est obsolète."))
- # and on a chapter
- result = self.client.get(self.chapter1.get_absolute_url_online(), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertContains(result, _("Ce contenu est obsolète."))
- # finally, check that this alert can be hidden
- result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False)
- self.assertEqual(result.status_code, 302)
- result = self.client.get(self.tuto.get_absolute_url_online(), follow=False)
- self.assertEqual(result.status_code, 200)
- self.assertNotContains(result, _("Ce contenu est obsolète."))
-
- def test_list_publications(self):
- """Test the behavior of the publication list"""
-
- category_1 = CategoryFactory()
- category_2 = CategoryFactory()
- subcategory_1 = SubCategoryFactory(category=category_1)
- subcategory_2 = SubCategoryFactory(category=category_1)
- subcategory_3 = SubCategoryFactory(category=category_2)
- subcategory_4 = SubCategoryFactory(category=category_2)
- tag_1 = Tag(title="random")
- tag_1.save()
-
- tuto_p_1 = PublishedContentFactory(author_list=[self.user_author])
- tuto_p_2 = PublishedContentFactory(author_list=[self.user_author])
- tuto_p_3 = PublishedContentFactory(author_list=[self.user_author])
-
- article_p_1 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE")
-
- tuto_p_1.subcategory.add(subcategory_1)
- tuto_p_1.subcategory.add(subcategory_2)
- tuto_p_1.save()
-
- tuto_p_2.subcategory.add(subcategory_1)
- tuto_p_2.subcategory.add(subcategory_2)
- tuto_p_2.save()
-
- tuto_p_3.subcategory.add(subcategory_3)
- tuto_p_3.save()
-
- article_p_1.subcategory.add(subcategory_4)
- article_p_1.tags.add(tag_1)
- article_p_1.save()
-
- tuto_1 = PublishedContent.objects.get(content=tuto_p_1.pk)
- tuto_2 = PublishedContent.objects.get(content=tuto_p_2.pk)
- tuto_3 = PublishedContent.objects.get(content=tuto_p_3.pk)
- article_1 = PublishedContent.objects.get(content=article_p_1.pk)
-
- self.assertEqual(PublishableContent.objects.filter(type="ARTICLE").count(), 1)
- self.assertEqual(PublishableContent.objects.filter(type="TUTORIAL").count(), 4)
-
- # 1. Publication list
- result = self.client.get(reverse("publication:list"))
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 1)
- self.assertEqual(len(result.context["last_tutorials"]), 4)
-
- # 2. Category page
- result = self.client.get(reverse("publication:category", kwargs={"slug": category_1.slug}))
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 0)
- self.assertEqual(len(result.context["last_tutorials"]), 2)
-
- pks = [x.pk for x in result.context["last_tutorials"]]
- self.assertIn(tuto_1.pk, pks)
- self.assertIn(tuto_2.pk, pks)
-
- result = self.client.get(reverse("publication:category", kwargs={"slug": category_2.slug}))
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 1)
- self.assertEqual(len(result.context["last_tutorials"]), 1)
-
- pks = [x.pk for x in result.context["last_tutorials"]]
- self.assertIn(tuto_3.pk, pks)
-
- pks = [x.pk for x in result.context["last_articles"]]
- self.assertIn(article_1.pk, pks)
-
- # 3. Subcategory page
- result = self.client.get(
- reverse("publication:subcategory", kwargs={"slug_category": category_1.slug, "slug": subcategory_1.slug})
- )
-
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 0)
- self.assertEqual(len(result.context["last_tutorials"]), 2)
-
- pks = [x.pk for x in result.context["last_tutorials"]]
- self.assertIn(tuto_1.pk, pks)
- self.assertIn(tuto_2.pk, pks)
-
- result = self.client.get(
- reverse("publication:subcategory", kwargs={"slug_category": category_1.slug, "slug": subcategory_2.slug})
- )
-
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 0)
- self.assertEqual(len(result.context["last_tutorials"]), 2)
-
- pks = [x.pk for x in result.context["last_tutorials"]]
- self.assertIn(tuto_1.pk, pks)
- self.assertIn(tuto_2.pk, pks)
-
- result = self.client.get(
- reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_3.slug})
- )
-
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 0)
- self.assertEqual(len(result.context["last_tutorials"]), 1)
-
- pks = [x.pk for x in result.context["last_tutorials"]]
- self.assertIn(tuto_3.pk, pks)
-
- result = self.client.get(
- reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_4.slug})
- )
-
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["last_articles"]), 1)
- self.assertEqual(len(result.context["last_tutorials"]), 0)
-
- pks = [x.pk for x in result.context["last_articles"]]
- self.assertIn(article_1.pk, pks)
-
- # 4. Final page and filters
- result = self.client.get(reverse("publication:list") + f"?category={category_1.slug}")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 2)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_1.pk, pks)
- self.assertIn(tuto_2.pk, pks)
-
- # filter by category and type
- result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 2)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_3.pk, pks)
- self.assertIn(article_1.pk, pks)
-
- result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}" + "&type=article")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 1)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(article_1.pk, pks)
-
- result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}" + "&type=tutorial")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 1)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_3.pk, pks)
-
- # filter by subcategory
- result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_1.slug}")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 2)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_1.pk, pks)
- self.assertIn(tuto_2.pk, pks)
-
- # filter by subcategory and type
- result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 1)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_3.pk, pks)
-
- result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}" + "&type=article")
- self.assertEqual(result.status_code, 200)
- self.assertEqual(len(result.context["filtered_contents"]), 0)
-
- result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}" + "&type=tutorial")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 1)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(tuto_3.pk, pks)
-
- # filter by tag
- result = self.client.get(reverse("publication:list") + f"?tag={tag_1.slug}" + "&type=article")
- self.assertEqual(result.status_code, 200)
-
- self.assertEqual(len(result.context["filtered_contents"]), 1)
- pks = [x.pk for x in result.context["filtered_contents"]]
- self.assertIn(article_1.pk, pks)
-
- # 5. Everything else results in 404
- wrong_urls = [
- # not existing (sub)categories, types or tags with slug "xxx"
- reverse("publication:list") + "?category=xxx",
- reverse("publication:list") + "?subcategory=xxx",
- reverse("publication:list") + "?type=xxx",
- reverse("publication:list") + "?tag=xxx",
- reverse("publication:category", kwargs={"slug": "xxx"}),
- reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": "xxx"}),
- # subcategory_1 does not belong to category_2:
- reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_1.slug}),
- ]
-
- for url in wrong_urls:
- self.assertEqual(self.client.get(url).status_code, 404, msg=url)
-
- def test_article_previous_link(self):
- """Test the behaviour of the article previous link."""
-
- article_1 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE")
- article_2 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE")
- article_3 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE")
- article_1.save()
- article_2.save()
- article_3.save()
-
- result = self.client.get(reverse("article:view", kwargs={"pk": article_3.pk, "slug": article_3.slug}))
-
- self.assertEqual(result.context["previous_content"].pk, article_2.public_version.pk)
-
- def test_opinion_link_is_not_related_to_the_author(self):
- """
- Test that the next and previous link in the opinion page take all the opinions
- into accounts and not only the ones of the author.
- """
-
- user_1_opinion_1 = PublishedContentFactory(author_list=[self.user_author], type="OPINION")
- user_2_opinion_1 = PublishedContentFactory(author_list=[self.user_guest], type="OPINION")
- user_1_opinion_2 = PublishedContentFactory(author_list=[self.user_author], type="OPINION")
- user_1_opinion_1.save()
- user_2_opinion_1.save()
- user_1_opinion_2.save()
-
- result = self.client.get(
- reverse("opinion:view", kwargs={"pk": user_1_opinion_2.pk, "slug": user_1_opinion_2.slug})
- )
-
- self.assertEqual(result.context["previous_content"].pk, user_2_opinion_1.public_version.pk)
-
- result = self.client.get(
- reverse("opinion:view", kwargs={"pk": user_2_opinion_1.pk, "slug": user_2_opinion_1.slug})
- )
-
- self.assertEqual(result.context["previous_content"].pk, user_1_opinion_1.public_version.pk)
- self.assertEqual(result.context["next_content"].pk, user_1_opinion_2.public_version.pk)
-
- def test_author_update(self):
- """Check that the author list of a content is updated when this content is updated."""
-
- text_validation = "Valide moi ce truc, please !"
- text_publication = "Validation faite !"
-
- tutorial = PublishedContentFactory(
- type="TUTORIAL", author_list=[self.user_author, self.user_guest, self.user_staff]
- )
-
- # Remove author to check if it's correct after major update
- tutorial.authors.remove(self.user_guest)
- tutorial.save()
- tutorial_draft = tutorial.load_version()
-
- # ask validation
- self.client.force_login(self.user_staff)
- self.client.post(
- reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}),
- {"text": text_validation, "version": tutorial_draft.current_version},
- follow=False,
- )
-
- # major update
- validation = Validation.objects.filter(content=tutorial).last()
- self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True},
- follow=False,
- )
- self.assertEqual(tutorial.public_version.authors.count(), 2)
-
- # Remove author to check if it's correct after minor update
- tutorial.authors.remove(self.user_author)
- tutorial.save()
- tutorial_draft = tutorial.load_version()
-
- # ask validation
- self.client.force_login(self.user_staff)
- self.client.post(
- reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}),
- {"text": text_validation, "version": tutorial_draft.current_version},
- follow=False,
- )
-
- # minor update
- validation = Validation.objects.filter(content=tutorial).last()
- self.client.post(
- reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False
- )
- self.client.post(
- reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": False},
- follow=False,
- )
-
- self.assertEqual(tutorial.public_version.authors.count(), 1)
-
- def test_add_help_tuto(self):
- self.client.force_login(self.user_author)
- tutorial = PublishableContentFactory(author_list=[self.user_author])
- help_wanted = HelpWritingFactory()
- resp = self.client.post(
- reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title}
- )
- self.assertEqual(302, resp.status_code)
- self.assertEqual(1, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count())
-
- def test_add_help_opinion(self):
- self.client.force_login(self.user_author)
- tutorial = PublishableContentFactory(author_list=[self.user_author], type="OPINION")
- help_wanted = HelpWritingFactory()
- resp = self.client.post(
- reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title}
- )
- self.assertEqual(400, resp.status_code)
- self.assertEqual(0, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count())
-
- def test_save_no_redirect(self):
- self.client.force_login(self.user_author)
- tutorial = PublishableContentFactory(author_list=[self.user_author])
- extract = ExtractFactory(db_object=tutorial, container=tutorial.load_version())
- tutorial = PublishableContent.objects.get(pk=tutorial.pk)
- resp = self.client.post(
- reverse("content:edit-extract", args=[tutorial.pk, tutorial.slug, extract.slug]),
- {
- "last_hash": extract.compute_hash(),
- "text": "a brand new text",
- "title": extract.title,
- "msg_commit": "a commit message",
- },
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- follow=False,
- )
- # no redirect
- self.assertEqual(200, resp.status_code)
- result = loads(resp.content.decode("utf-8"))
- self.assertEqual("ok", result.get("result", None))
- self.assertEqual(extract.compute_hash(), result.get("last_hash", None))
diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py
index 13fbe4821b..6591e1c50f 100644
--- a/zds/tutorialv2/tests/tests_views/tests_published.py
+++ b/zds/tutorialv2/tests/tests_views/tests_published.py
@@ -139,7 +139,7 @@ def test_public_access(self):
result = self.client.post(
reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "source": "", "version": article_draft.current_version},
+ {"text": text_validation, "version": article_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -157,7 +157,7 @@ def test_public_access(self):
# accept
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -198,7 +198,7 @@ def test_public_access(self):
# ask validation
result = self.client.post(
reverse("validation:ask", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto.slug}),
- {"text": text_validation, "source": "", "version": midsize_tuto_draft.current_version},
+ {"text": text_validation, "version": midsize_tuto_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -216,7 +216,7 @@ def test_public_access(self):
# accept
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -288,7 +288,7 @@ def test_public_access(self):
# ask validation
result = self.client.post(
reverse("validation:ask", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}),
- {"text": text_validation, "source": "", "version": bigtuto_draft.current_version},
+ {"text": text_validation, "version": bigtuto_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -306,7 +306,7 @@ def test_public_access(self):
# accept
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1214,7 +1214,7 @@ def test_republish_with_different_slug(self):
result = self.client.post(
reverse("validation:ask", kwargs={"pk": tuto.pk, "slug": tuto.slug}),
- {"text": text_validation, "source": "", "version": tuto.sha_draft},
+ {"text": text_validation, "version": tuto.sha_draft},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1232,7 +1232,7 @@ def test_republish_with_different_slug(self):
# accept
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": False, "source": ""}, # minor modification (just the title)
+ {"text": text_publication, "is_major": False}, # minor modification (just the title)
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1289,7 +1289,7 @@ def test_validation_list_has_good_title(self):
self.client.force_login(self.user_author)
result = self.client.post(
reverse("validation:ask", args=[tuto.pk, tuto.slug]),
- {"text": "something good", "source": "", "version": tuto.sha_draft},
+ {"text": "something good", "version": tuto.sha_draft},
follow=False,
)
old_title = tuto.title
@@ -1413,7 +1413,7 @@ def test_validation_history(self):
self.assertEqual(result.status_code, 200)
result = self.client.post(
reverse("validation:ask", kwargs={"pk": published.pk, "slug": published.slug}),
- {"text": "abcdefg", "source": "", "version": published.load_version().current_version},
+ {"text": "abcdefg", "version": published.load_version().current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1429,7 +1429,7 @@ def test_validation_history_for_new_content(self):
result = self.client.post(
reverse("validation:ask", kwargs={"pk": publishable.pk, "slug": publishable.slug}),
- {"text": "abcdefg", "source": "", "version": publishable.load_version().current_version},
+ {"text": "abcdefg", "version": publishable.load_version().current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1454,7 +1454,7 @@ def test_ask_validation_update(self):
self.client.force_login(self.user_author)
result = self.client.post(
reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}),
- {"text": text_validation, "source": "", "version": content_draft.current_version},
+ {"text": text_validation, "version": content_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1488,7 +1488,7 @@ def test_ask_validation_update(self):
self.assertEqual(result.status_code, 302)
result = self.client.post(
reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}),
- {"text": text_validation, "source": "", "version": content_draft.current_version},
+ {"text": text_validation, "version": content_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1523,7 +1523,7 @@ def test_beta_article_closed_when_published(self):
self.assertEqual(Validation.objects.count(), 0)
result = self.client.post(
reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "source": "", "version": article_draft.current_version},
+ {"text": text_validation, "version": article_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1546,7 +1546,7 @@ def test_beta_article_closed_when_published(self):
# publish the article
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1562,7 +1562,7 @@ def test_beta_article_closed_when_published(self):
# ask validation
result = self.client.post(
reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}),
- {"text": text_validation, "source": "", "version": article_draft.current_version},
+ {"text": text_validation, "version": article_draft.current_version},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1580,7 +1580,7 @@ def test_beta_article_closed_when_published(self):
# publish the article
result = self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(result.status_code, 302)
@@ -1883,7 +1883,7 @@ def test_author_update(self):
self.client.force_login(self.user_staff)
self.client.post(
reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}),
- {"text": text_validation, "source": "", "version": tutorial_draft.current_version},
+ {"text": text_validation, "version": tutorial_draft.current_version},
follow=False,
)
@@ -1894,7 +1894,7 @@ def test_author_update(self):
)
self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": True, "source": ""},
+ {"text": text_publication, "is_major": True},
follow=False,
)
self.assertEqual(tutorial.public_version.authors.count(), 2)
@@ -1908,7 +1908,7 @@ def test_author_update(self):
self.client.force_login(self.user_staff)
self.client.post(
reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}),
- {"text": text_validation, "source": "", "version": tutorial_draft.current_version},
+ {"text": text_validation, "version": tutorial_draft.current_version},
follow=False,
)
@@ -1919,7 +1919,7 @@ def test_author_update(self):
)
self.client.post(
reverse("validation:accept", kwargs={"pk": validation.pk}),
- {"text": text_publication, "is_major": False, "source": ""},
+ {"text": text_publication, "is_major": False},
follow=False,
)
diff --git a/zds/urls.py b/zds/urls.py
index 2ab157e360..64ada6e5e9 100644
--- a/zds/urls.py
+++ b/zds/urls.py
@@ -6,7 +6,7 @@
from zds.forum.models import ForumCategory, Forum, Topic, Tag
from zds.pages.views import home as home_view
-from zds.member.views import MemberDetail
+from zds.member.views.profile import MemberDetail
from zds.tutorialv2.models.database import PublishedContent
from django.conf import settings
From f96d51857cd8da0f2c208db4a83be6fef5352e0f Mon Sep 17 00:00:00 2001
From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com>
Date: Sat, 5 Mar 2022 12:48:41 +0100
Subject: [PATCH 2/3] Fix imports
---
zds/member/tests/views/tests_admin.py | 4 ++--
zds/member/tests/views/tests_emailproviders.py | 4 ++--
zds/member/tests/views/tests_hats.py | 4 ++--
zds/member/tests/views/tests_login.py | 2 +-
zds/member/tests/views/tests_moderation.py | 4 ++--
zds/member/tests/views/tests_password_recovery.py | 4 ++--
zds/member/tests/views/tests_profile.py | 4 ++--
zds/member/tests/views/tests_register.py | 10 +++++-----
zds/member/tests/views/tests_reports.py | 4 ++--
9 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/zds/member/tests/views/tests_admin.py b/zds/member/tests/views/tests_admin.py
index e85ca3c3bb..0d60d4fb45 100644
--- a/zds/member/tests/views/tests_admin.py
+++ b/zds/member/tests/views/tests_admin.py
@@ -4,9 +4,9 @@
from django.test import TestCase
from zds.notification.models import TopicAnswerSubscription
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.member.models import Profile
-from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory
class MemberTests(TestCase):
diff --git a/zds/member/tests/views/tests_emailproviders.py b/zds/member/tests/views/tests_emailproviders.py
index 348cfbd80d..a3039ab422 100644
--- a/zds/member/tests/views/tests_emailproviders.py
+++ b/zds/member/tests/views/tests_emailproviders.py
@@ -3,9 +3,9 @@
from django.urls import reverse
from django.test import TestCase
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.member.models import NewEmailProvider, BannedEmailProvider, TokenRegister
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
class EmailProvidersTests(TestCase):
diff --git a/zds/member/tests/views/tests_hats.py b/zds/member/tests/views/tests_hats.py
index 2dae508d62..05b87b0fab 100644
--- a/zds/member/tests/views/tests_hats.py
+++ b/zds/member/tests/views/tests_hats.py
@@ -4,8 +4,8 @@
from django.test import TestCase
from django.utils.translation import gettext_lazy as _
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
from zds.pages.models import GroupContact
from zds.utils.models import Hat, HatRequest
diff --git a/zds/member/tests/views/tests_login.py b/zds/member/tests/views/tests_login.py
index f0c6cfa530..f56c00e321 100644
--- a/zds/member/tests/views/tests_login.py
+++ b/zds/member/tests/views/tests_login.py
@@ -2,7 +2,7 @@
from django.test import TestCase
from django.utils.translation import gettext_lazy as _
-from zds.member.factories import ProfileFactory, NonAsciiProfileFactory
+from zds.member.tests.factories import ProfileFactory, NonAsciiProfileFactory
class MemberTests(TestCase):
diff --git a/zds/member/tests/views/tests_moderation.py b/zds/member/tests/views/tests_moderation.py
index 2a3f9bead6..ae1f8a71d9 100644
--- a/zds/member/tests/views/tests_moderation.py
+++ b/zds/member/tests/views/tests_moderation.py
@@ -8,9 +8,9 @@
from django.test import TestCase
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
from zds.member.views.moderation import member_from_ip
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.member.models import Profile, Ban, KarmaNote
diff --git a/zds/member/tests/views/tests_password_recovery.py b/zds/member/tests/views/tests_password_recovery.py
index d08e968454..0dd342247f 100644
--- a/zds/member/tests/views/tests_password_recovery.py
+++ b/zds/member/tests/views/tests_password_recovery.py
@@ -4,9 +4,9 @@
from django.urls import reverse
from django.test import TestCase
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.member.models import TokenForgotPassword
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
class MemberTests(TestCase):
diff --git a/zds/member/tests/views/tests_profile.py b/zds/member/tests/views/tests_profile.py
index 9b0aeb5bed..a8f115bdac 100644
--- a/zds/member/tests/views/tests_profile.py
+++ b/zds/member/tests/views/tests_profile.py
@@ -3,7 +3,7 @@
from django.urls import reverse
from django.test import TestCase
-from zds.member.factories import (
+from zds.member.tests.factories import (
ProfileFactory,
StaffProfileFactory,
UserFactory,
@@ -11,7 +11,7 @@
)
from zds.member.models import Profile
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
@override_for_contents()
diff --git a/zds/member/tests/views/tests_register.py b/zds/member/tests/views/tests_register.py
index 54398d1c52..2d3d9040a0 100644
--- a/zds/member/tests/views/tests_register.py
+++ b/zds/member/tests/views/tests_register.py
@@ -13,17 +13,17 @@
from django.utils.html import escape
from django.test import TestCase, override_settings
-from zds.member.factories import ProfileFactory, UserFactory, StaffProfileFactory
-from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory
+from zds.member.tests.factories import ProfileFactory, UserFactory, StaffProfileFactory
+from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory
from zds.member.models import KarmaNote, NewEmailProvider
from zds.mp.models import PrivatePost, PrivateTopic
from zds.member.models import TokenRegister, Ban
-from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory
+from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory
from zds.tutorialv2.models.database import PublishableContent, PublishedContent
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
-from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory
from zds.forum.models import Topic, Post
-from zds.gallery.factories import GalleryFactory, UserGalleryFactory
+from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory
from zds.gallery.models import Gallery, UserGallery
from zds.utils.models import CommentVote
diff --git a/zds/member/tests/views/tests_reports.py b/zds/member/tests/views/tests_reports.py
index 6fd95bcf0f..c5d8aaac1e 100644
--- a/zds/member/tests/views/tests_reports.py
+++ b/zds/member/tests/views/tests_reports.py
@@ -3,11 +3,11 @@
from django.urls import reverse
from django.test import TestCase
-from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory
+from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
from zds.mp.models import PrivateTopic
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
-from zds.forum.factories import ForumCategoryFactory, ForumFactory
+from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory
from zds.utils.models import Alert
From 2fa53c4c11694a2058744b45712eb77c580f7c61 Mon Sep 17 00:00:00 2001
From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com>
Date: Sun, 6 Mar 2022 22:35:57 +0100
Subject: [PATCH 3/3] =?UTF-8?q?Remets=20en=20place=20quelques=20tests=20in?=
=?UTF-8?q?justement=20=C3=A9limin=C3=A9s?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../tests/tests_views/tests_content.py | 12 ++---
.../tests/tests_views/tests_published.py | 45 ++++++++++++++++++-
2 files changed, 47 insertions(+), 10 deletions(-)
diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py
index bb1302acd4..4f4d51b8da 100644
--- a/zds/tutorialv2/tests/tests_views/tests_content.py
+++ b/zds/tutorialv2/tests/tests_views/tests_content.py
@@ -1,5 +1,4 @@
import datetime
-from json import loads
import shutil
import tempfile
import zipfile
@@ -10,7 +9,6 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import Group
-from django.core import mail
from django.urls import reverse
from django.test import TestCase
from django.utils.translation import gettext_lazy as _
@@ -21,7 +19,7 @@
from zds.gallery.models import GALLERY_WRITE, UserGallery, Gallery
from zds.gallery.models import Image
from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory
-from zds.mp.models import PrivateTopic, is_privatetopic_unread, PrivatePost
+from zds.mp.models import PrivateTopic, PrivatePost
from zds.notification.models import (
TopicAnswerSubscription,
ContentReactionAnswerSubscription,
@@ -40,20 +38,16 @@
PublishableContent,
Validation,
PublishedContent,
- ContentReaction,
- ContentRead,
)
from zds.tutorialv2.publication_utils import (
- publish_content,
PublicatorRegistry,
Publicator,
ZMarkdownRebberLatexPublicator,
ZMarkdownEpubPublicator,
)
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
-from zds.utils.models import HelpWriting, Alert, Tag, Hat
-from zds.utils.tests.factories import HelpWritingFactory, CategoryFactory, SubCategoryFactory, LicenceFactory
-from zds.utils.header_notifications import get_header_notifications
+from zds.utils.models import HelpWriting, Tag
+from zds.utils.tests.factories import HelpWritingFactory, SubCategoryFactory, LicenceFactory
from zds import json_handler
diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py
index 6591e1c50f..cc1a2998fc 100644
--- a/zds/tutorialv2/tests/tests_views/tests_published.py
+++ b/zds/tutorialv2/tests/tests_views/tests_published.py
@@ -1,4 +1,5 @@
import datetime
+from json import loads
from django.conf import settings
from django.contrib.auth.models import Group
@@ -29,7 +30,7 @@
from zds.tutorialv2.publication_utils import publish_content
from zds.tutorialv2.tests import TutorialTestMixin
from zds.utils.models import Alert, Tag, Hat
-from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory
+from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory, HelpWritingFactory
from zds.utils.header_notifications import get_header_notifications
from copy import deepcopy
from zds import json_handler
@@ -2040,3 +2041,45 @@ def test_social_cards_with_image(self):
self.assertEqual(result.status_code, 200)
self.check_images_socials(result, "media/galleries/", self.chapter1.title, self.tuto.description)
+
+ def test_add_help_tuto(self):
+ self.client.force_login(self.user_author)
+ tutorial = PublishableContentFactory(author_list=[self.user_author])
+ help_wanted = HelpWritingFactory()
+ resp = self.client.post(
+ reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title}
+ )
+ self.assertEqual(302, resp.status_code)
+ self.assertEqual(1, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count())
+
+ def test_add_help_opinion(self):
+ self.client.force_login(self.user_author)
+ tutorial = PublishableContentFactory(author_list=[self.user_author], type="OPINION")
+ help_wanted = HelpWritingFactory()
+ resp = self.client.post(
+ reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title}
+ )
+ self.assertEqual(400, resp.status_code)
+ self.assertEqual(0, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count())
+
+ def test_save_no_redirect(self):
+ self.client.force_login(self.user_author)
+ tutorial = PublishableContentFactory(author_list=[self.user_author])
+ extract = ExtractFactory(db_object=tutorial, container=tutorial.load_version())
+ tutorial = PublishableContent.objects.get(pk=tutorial.pk)
+ resp = self.client.post(
+ reverse("content:edit-extract", args=[tutorial.pk, tutorial.slug, extract.slug]),
+ {
+ "last_hash": extract.compute_hash(),
+ "text": "a brand new text",
+ "title": extract.title,
+ "msg_commit": "a commit message",
+ },
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ follow=False,
+ )
+ # no redirect
+ self.assertEqual(200, resp.status_code)
+ result = loads(resp.content.decode("utf-8"))
+ self.assertEqual("ok", result.get("result", None))
+ self.assertEqual(extract.compute_hash(), result.get("last_hash", None))