From d19997dcab3c6a0b6c931d391b9ffa8650d85432 Mon Sep 17 00:00:00 2001 From: Antonin Date: Mon, 28 Nov 2016 20:47:48 +0000 Subject: [PATCH 01/78] First commit --- zds/forum/api/permissions.py | 14 + zds/forum/api/serializer.py | 84 ++++++ zds/forum/api/tests.py | 265 +++++++++++++++++++ zds/forum/api/urls.py | 12 +- zds/forum/api/views.py | 485 ++++++++++++++++++++++++++++++++++- zds/forum/models.py | 40 ++- 6 files changed, 892 insertions(+), 8 deletions(-) create mode 100644 zds/forum/api/permissions.py create mode 100644 zds/forum/api/serializer.py diff --git a/zds/forum/api/permissions.py b/zds/forum/api/permissions.py new file mode 100644 index 0000000000..fb95de6879 --- /dev/null +++ b/zds/forum/api/permissions.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from rest_framework import permissions +from django.contrib.auth.models import AnonymousUser + + + +class IsStaffUser(permissions.BasePermission): + """ + Allows access only to staff users. + """ + + def has_permission(self, request, view): + return request.user and request.user.has_perm("forum.create_forum") diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py new file mode 100644 index 0000000000..50dbc5093b --- /dev/null +++ b/zds/forum/api/serializer.py @@ -0,0 +1,84 @@ +from rest_framework.serializers import ModelSerializer, SerializerMethodField +from zds.forum.models import Forum, Topic, Post +from dry_rest_permissions.generics import DRYPermissionsField +from dry_rest_permissions.generics import DRYPermissions +from django.shortcuts import get_object_or_404 + +class ForumSerializer(ModelSerializer): + class Meta: + model = Forum + permissions_classes = DRYPermissions + + +class ForumActionSerializer(ModelSerializer): + """ + Serializer to create a new forum + """ + permissions = DRYPermissionsField() + + class Meta: + model = Forum + #fields = ('id', 'text', 'text_html', 'permissions') + # read_only_fields = ('text_html', 'permissions') + read_only_fields = ('slug',) + + def create(self, validated_data): + new_forum = Forum.objects.create(**validated_data) + return new_forum + + + +class TopicSerializer(ModelSerializer): + class Meta: + model = Topic + #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') + permissions_classes = DRYPermissions + + +class TopicActionSerializer(ModelSerializer): + """ + Serializer to create a new topic. + """ + permissions = DRYPermissionsField() + + class Meta: + model = Topic + read_only_fields = ('slug','author') + + def create(self, validated_data): + new_topic = Topic.objects.create(**validated_data) + return new_topic + + + +class PostSerializer(ModelSerializer): + class Meta: + model = Post + #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') + permissions_classes = DRYPermissions + +class PostActionSerializer(ModelSerializer): + """ + Serializer to send a post in a topic + """ + permissions = DRYPermissionsField() + + class Meta: + model = Post + fields = ('id', 'text', 'text_html', 'permissions') + read_only_fields = ('text_html', 'permissions') + # TODO a voir quel champ en read only + + def create(self, validated_data): + # Get topic + pk_topic = validated_data.get('topic_id') + topic = get_object_or_404(Topic, pk=(pk_topic)) + + new_post = Post.objects.create(**validated_data) + + + return topic.last_message + + # Todo a t on besoin d'un validateur + #def throw_error(self, key=None, message=None): + #raise serializers.ValidationError(message) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index b0e6686b64..b8cd3bf36f 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -7,7 +7,10 @@ from rest_framework import status from rest_framework.test import APIClient from rest_framework.test import APITestCase +from oauth2_provider.models import Application, AccessToken from rest_framework_extensions.settings import extensions_api_settings +from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM +from zds.member.factories import ProfileFactory, StaffProfileFactory, ProfileNotSyncFactory from zds.forum.factories import PostFactory from zds.forum.tests.tests_views import create_category, add_topic_in_a_forum @@ -196,3 +199,265 @@ def test_get_post_voters(self): self.assertEqual(1, len(response.data['dislike']['users'])) self.assertEqual(1, response.data['like']['count']) self.assertEqual(1, response.data['dislike']['count']) + +# Lister les forums vide +# Lister les forum, 200 une seule page +# Liste des forum plusieurs page +# Lister les forums avec page size +# Lister les forums avec une page de trop +# Liste les forums avec page size et accéder a la page deux +# Liste les forums avec un staff (on boit les forums privé) + + +class ForumAPITest(APITestCase): + def setUp(self): + self.client = APIClient() + + + self.profile = ProfileFactory() + client_oauth2 = create_oauth2_client(self.profile.user) + client_authenticated = APIClient() + authenticate_client(client_authenticated, client_oauth2, self.profile.user.username, 'hostel77') + + caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() + + def create_multiple_forums(self, number_of_forum=REST_PAGE_SIZE): + for forum in xrange(0, number_of_forum): + category, forum = create_category() + + def test_list_of_forums_empty(self): + """ + Gets empty list of forums in the database. + """ + response = self.client.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_forums(self): + """ + Gets list of forums not empty in the database. + """ + self.create_multiple_forums() + + response = self.client.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_forums_with_several_pages(self): + """ + Gets list of forums with several pages in the database. + """ + self.create_multiple_forums(REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + + response = self.client.get(reverse('api:forum:list') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), 1) + + def test_list_of_forums_for_a_page_given(self): + """ + Gets list of forums with several pages and gets a page different that the first one. + """ + self.create_multiple_forums(REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 11) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + + def test_list_of_forums_for_a_wrong_page_given(self): + """ + Gets an error when the forums asks a wrong page. + """ + response = self.client.get(reverse('api:forum:list') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_of_forums_with_a_custom_page_size(self): + """ + Gets list of forums with a custom page size. DRF allows to specify a custom + size for the pagination. + """ + self.create_multiple_forums(REST_PAGE_SIZE * 2) + + page_size = 'page_size' + response = self.client.get(reverse('api:forum:list') + '?{}=20'.format(page_size)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 20) + self.assertEqual(len(response.data.get('results')), 20) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_PAGE_SIZE_QUERY_PARAM, page_size) + + def test_list_of_forums_with_a_wrong_custom_page_size(self): + """ + Gets list of forums with a custom page size but not good according to the + value in settings. + """ + page_size_value = REST_MAX_PAGE_SIZE + 1 + self.create_multiple_forums(page_size_value) + + response = self.client.get(reverse('api:forum:list') + '?page_size={}'.format(page_size_value)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), page_size_value) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) + +# TODO regler le probleme de l-user admin +#done Créer un forum en étant anonyme 401 +#done Créer un forum en étant membre 401 +#done Créer un forum en étant staff 200 +#done Creer un fórum avec un titre qui existe deja pour tester le slug +#done Creer un fórum sans categorie +#done Crrer un fórum sans titre +#done Crrer un fórum sans soustitre +#done creer un forum sans categorie +#avec titre vide/soustitre vide et categorie vide +# creer une forum dans une categorie qui n exist epas + + def test_new_forum_with_anonymous(self): + """ + Tries to create a new forum with an anonymous (non authentified) user. + """ + data = { + 'titre': 'Flask', + 'subtitle': 'Flask is the best framework EVER !', + 'categorie': '2' + } + + self.create_multiple_forums(5) + response = self.client.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + + + def test_new_forum_with_user(self): + """ + Tries to create a new forum with an user. + """ + data = { + 'titre': 'Flask', + 'subtitle': 'Flask is the best framework EVER !', + 'categorie': '2' + } + + self.create_multiple_forums(5) + client = APIClient() + response = client.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + + def test_new_forum_with_staff(self): + """ + Tries to create a new forum with an staff user. + """ + data = { + 'titre': 'Flask', + 'subtitle': 'Flask is the best framework EVER !', + 'categorie': '2' + } + + self.create_multiple_forums(5) + self.staff = StaffProfileFactory() + client_oauth2 = create_oauth2_client(self.staff.user) + self.client_authenticated_staff = APIClient() + authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') + + response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + + def test_new_forum_slug_collision(self): + """ + Tries to create two forums with the same title to see if the slug generated is different. + """ + data = { + 'titre': 'Flask', + 'subtitle': 'Flask is the best framework EVER !', + 'categorie': '2' + } + + self.create_multiple_forums(5) + response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response2 = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response2.status_code, status.HTTP_201_CREATED) + self.assertNotEqual(response.data.get('slug'), response2.data.get('slug')) + + def test_new_forum_without_title(self): + """ + Tries to create a new forum with an staff user, without a title. + """ + data = { + 'subtitle': 'Flask is the best framework EVER !', + 'categorie': '2' + } + + self.create_multiple_forums(5) + response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_forum_without_subtitle(self): + """ + Tries to create a new forum with an staff user, without a subtitle. + """ + data = { + 'titre': 'Flask', + 'categorie': '2' + } + + self.create_multiple_forums(5) + response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_forum_without_category(self): + """ + Tries to create a new forum with an staff user without a category. + """ + data = { + 'titre': 'Flask', + 'subtitle': 'Flask is the best framework EVER !', + } + + self.create_multiple_forums(5) + response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + + +def create_oauth2_client(user): + client = Application.objects.create(user=user, + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_PASSWORD) + client.save() + return client + + +def authenticate_client(client, client_auth, username, password): + client.post('/oauth2/token/', { + 'client_id': client_auth.client_id, + 'client_secret': client_auth.client_secret, + 'username': username, + 'password': password, + 'grant_type': 'password' + }) + access_token = AccessToken.objects.get(user__username=username) + client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) diff --git a/zds/forum/api/urls.py b/zds/forum/api/urls.py index 76bdf2862c..fd631f75b8 100644 --- a/zds/forum/api/urls.py +++ b/zds/forum/api/urls.py @@ -1,7 +1,15 @@ from django.conf.urls import url -from .views import PostKarmaView - +from .views import PostKarmaView, ForumListAPI, ForumDetailAPI, PostListAPI, TopicListAPI, TopicDetailAPI, UserTopicListAPI, MemberPostListAPI, UserPostListAPI, PostDetailAPI urlpatterns = [ + url(r'^$', ForumListAPI.as_view(), name='list'), + url(r'^(?P[0-9]+)/?$', ForumDetailAPI.as_view(), name='detail'), + url(r'^sujets/?$', TopicListAPI.as_view(), name='list-topic'), + url(r'^membre/sujets/?$', UserTopicListAPI.as_view(), name='list-usertopic'), url(r'^message/(?P\d+)/karma/?$', PostKarmaView.as_view(), name='post-karma'), + url(r'^sujets/(?P[0-9]+)/messages?$', PostListAPI.as_view(), name='list-post'), + url(r'^sujets/(?P[0-9]+)/?$', TopicDetailAPI.as_view(), name='detail-topic'), + url(r'^membres/(?P[0-9]+)/messages/?$', MemberPostListAPI.as_view(), name='list-memberpost'), + url(r'^membres/messages/?$', UserPostListAPI.as_view(), name='list-userpost'), + url(r'^sujets/(?P[0-9]+)/messages/(?P[0-9]+)/?$', PostDetailAPI.as_view(), name='detail-post') ] diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 2995d65770..5e66a19e3a 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -1,11 +1,490 @@ # coding: utf-8 -from rest_framework.permissions import IsAuthenticatedOrReadOnly from zds.member.api.permissions import CanReadTopic, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly from zds.utils.api.views import KarmaView -from zds.forum.models import Post - +from zds.forum.models import Post, Forum, Topic +import datetime +from django.core.cache import cache +from django.db.models.signals import post_save, post_delete +from rest_framework import filters +from rest_framework.generics import ListAPIView +from rest_framework.generics import ListCreateAPIView, RetrieveUpdateAPIView +from rest_framework_extensions.key_constructor.constructors import DefaultKeyConstructor +from rest_framework_extensions.cache.decorators import cache_response +from rest_framework_extensions.etag.decorators import etag +from rest_framework_extensions.key_constructor import bits +from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit +from zds.utils import slugify +from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, PostSerializer, PostActionSerializer, ForumActionSerializer +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticatedOrReadOnly, AllowAny, IsAuthenticated +from dry_rest_permissions.generics import DRYPermissions +from zds.forum.api.permissions import IsStaffUser +from django.http import HttpResponseRedirect class PostKarmaView(KarmaView): queryset = Post.objects.all() permission_classes = (IsAuthenticatedOrReadOnly, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, CanReadTopic) + + +class PagingSearchListKeyConstructor(DefaultKeyConstructor): + pagination = DJRF3xPaginationKeyBit() + list_sql_query = bits.ListSqlQueryKeyBit() + unique_view_id = bits.UniqueViewIdKeyBit() + user = bits.UserKeyBit() + updated_at = UpdatedAtKeyBit('api_updated_forum') + +class DetailKeyConstructor(DefaultKeyConstructor): + format = bits.FormatKeyBit() + language = bits.LanguageKeyBit() + retrieve_sql_query = bits.RetrieveSqlQueryKeyBit() + unique_view_id = bits.UniqueViewIdKeyBit() + user = bits.UserKeyBit() + updated_at = UpdatedAtKeyBit('api_updated_forum') + + + +def change_api_forum_updated_at(sender=None, instance=None, *args, **kwargs): + cache.set('forum_updated_tag', datetime.datetime.utcnow()) + + +post_save.connect(receiver=change_api_forum_updated_at, sender=Forum) +post_delete.connect(receiver=change_api_forum_updated_at, sender=Forum) + + +class ForumListAPI(ListCreateAPIView): + """ + Profile resource to list all forum + """ + + queryset = Forum.objects.all() + list_key_func = PagingSearchListKeyConstructor() + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all forum in the system. + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + """ + Creates a new forum. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + - name: text + description: Content of the post in markdown. + required: true + paramType: form + responseMessages: + - code: 400 + message: Bad Request + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied + """ + + forum_slug = slugify(request.data.get('title')) + serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) + serializer.is_valid(raise_exception=True) + serializer.save(slug=forum_slug) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_serializer_class(self): + if self.request.method == 'GET': + return ForumSerializer + elif self.request.method == 'POST': + return ForumActionSerializer + + def get_permissions(self): + permission_classes = [AllowAny, ] + if self.request.method == 'POST': + permission_classes.append(DRYPermissions) + permission_classes.append(IsStaffUser) + return [permission() for permission in permission_classes] + + +class ForumDetailAPI(RetrieveUpdateAPIView): + """ + Profile resource to display or update details of a forum. + """ + + queryset = Forum.objects.all() + obj_key_func = DetailKeyConstructor() + serializer_class = ForumSerializer + + @etag(obj_key_func) + @cache_response(key_func=obj_key_func) + def get(self, request, *args, **kwargs): + """ + Gets a forum given by its identifier. + --- + responseMessages: + - code: 404 + message: Not Found + """ + forum = self.get_object() + serializer = self.get_serializer(forum) + + return self.retrieve(request, *args, **kwargs) + + +class TopicListAPI(ListCreateAPIView): + """ + Profile resource to list all topic + """ + queryset = Topic.objects.all() + serializer_class = TopicSerializer + filter_backends = (filters.DjangoFilterBackend,) + filter_fields = ('forum','author','tags__title') + list_key_func = PagingSearchListKeyConstructor() + + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all topic in a forum. + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + """ + Creates a new topic. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + - name: text + description: Content of the post in markdown. + required: true + paramType: form + responseMessages: + - code: 400 + message: Bad Request + - code: 401 + message: Not Authenticated + """ + + author = request.user + + serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) + serializer.is_valid(raise_exception=True) + topic = serializer.save(author_id=3) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_serializer_class(self): + if self.request.method == 'GET': + return TopicSerializer + elif self.request.method == 'POST': + return TopicActionSerializer + + + def get_permissions(self): + permission_classes = [AllowAny, ] + if self.request.method == 'POST': + permission_classes.append(DRYPermissions) + permission_classes.append(IsAuthenticated) + return [permission() for permission in permission_classes] + + + +class UserTopicListAPI(ListAPIView): + """ + Profile resource to list all topic from current user + """ + + serializer_class = TopicSerializer + filter_backends = (filters.DjangoFilterBackend,) + filter_fields = ('forum','tags__title') + list_key_func = PagingSearchListKeyConstructor() + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all topic from current user. + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + # TODO code d'auth manquant en commentaire + return self.list(request, *args, **kwargs) + + def get_queryset(self): + topics = Topic.objects.filter(author=self.request.user) + return topics + + +class TopicDetailAPI(RetrieveUpdateAPIView): + """ + Profile resource to display details of a given topic + """ + + queryset = Topic.objects.all() + obj_key_func = DetailKeyConstructor() + serializer_class = TopicSerializer + + @etag(obj_key_func) + @cache_response(key_func=obj_key_func) + def get(self, request, *args, **kwargs): + """ + Gets a topic given by its identifier. + --- + responseMessages: + - code: 404 + message: Not Found + """ + topic = self.get_object() + #serializer = self.get_serializer(topic) + + return self.retrieve(request, *args, **kwargs) + + +class PostListAPI(ListCreateAPIView): + """ + Profile resource to list all message in a topic + """ + + list_key_func = PagingSearchListKeyConstructor() + #serializer_class = PostSerializer + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all posts in a topic + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + return self.list(request, *args, **kwargs) + # TODO si message cache ? Le cacher dans l'API + + def post(self, request, *args, **kwargs): + """ + Creates a new post in a topic. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + - name: text + description: Content of the post in markdown. + required: true + paramType: form + responseMessages: + - code: 400 + message: Bad Request + - code: 401 + message: Not Authenticated + """ + + #TODO GERE les droits et l'authentification --> en cours : tester avec connection pour voir si cela fonctionne + #TODO passer les arguments corrects a save + author = request.user + + + serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) + serializer.is_valid(raise_exception=True) + topic = serializer.save(position=2,author_id=3,topic_id=1) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_serializer_class(self): + if self.request.method == 'GET': + return PostSerializer + elif self.request.method == 'POST': + return PostActionSerializer + + + def get_queryset(self): + if self.request.method == 'GET': + posts = Post.objects.filter(topic=self.kwargs.get('pk')) + return posts + + + def get_current_user(self): + return self.request.user.profile + + def get_permissions(self): + permission_classes = [AllowAny, ] + if self.request.method == 'POST': + permission_classes.append(DRYPermissions) + permission_classes.append(IsAuthenticated) + return [permission() for permission in permission_classes] + + +class MemberPostListAPI(ListAPIView): + """ + Profile resource to list all posts from a member + """ + + list_key_func = PagingSearchListKeyConstructor() + serializer_class = PostSerializer + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all posts from a member + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + return self.list(request, *args, **kwargs) + # TODO fonctionne mais error xml sur certains post http://zds-anto59290.c9users.io/api/forums/membres/3/sujets + + + def get_queryset(self): + if self.request.method == 'GET': + posts = Post.objects.filter(author=self.kwargs.get('pk')) + return posts + + +class UserPostListAPI(ListAPIView): + """ + Profile resource to list all message from current user + """ + + list_key_func = PagingSearchListKeyConstructor() + serializer_class = PostSerializer + + + @etag(list_key_func) + @cache_response(key_func=list_key_func) + def get(self, request, *args, **kwargs): + """ + Lists all posts from a member + --- + + parameters: + - name: page + description: Restricts output to the given page number. + required: false + paramType: query + - name: page_size + description: Sets the number of forum per page. + required: false + paramType: query + responseMessages: + - code: 404 + message: Not Found + """ + return self.list(request, *args, **kwargs) + + def get_queryset(self): + if self.request.method == 'GET': + posts = Post.objects.filter(author=self.request.user) + return posts + + +class PostDetailAPI(RetrieveUpdateAPIView): + """ + Profile resource to display details of given message + """ + + queryset = Post.objects.all() + obj_key_func = DetailKeyConstructor() + serializer_class = PostSerializer + + @etag(obj_key_func) + @cache_response(key_func=obj_key_func) + def get(self, request, *args, **kwargs): + """ + Gets a post given by its identifier. + --- + responseMessages: + - code: 404 + message: Not Found + """ + post = self.get_object() + + return self.retrieve(request, *args, **kwargs) + + + +# TODO global identier quand masquer les messages +# TOD gerer l'antispam \ No newline at end of file diff --git a/zds/forum/models.py b/zds/forum/models.py index e39f216e71..000c026488 100644 --- a/zds/forum/models.py +++ b/zds/forum/models.py @@ -151,6 +151,10 @@ def can_read(self, user): pk=self.pk).exists() else: return False + + @staticmethod + def has_write_permission(request): + return request.user.has_perm("member.change_forum") class Topic(models.Model): @@ -378,6 +382,24 @@ def old_post_warning(self): return False + @staticmethod + def has_read_permission(request): + return True + + def has_object_read_permission(self, request): + return Topic.has_read_permission(request) # TODO gerer fofo prives + + @staticmethod + def has_write_permission(request): + return request.user.is_authenticated() + + def has_object_write_permission(self, request): + return Topic.has_write_permission(request) # TODO gerer les fofo prives + + def has_object_update_permission(self, request): + return Topic.has_write_permission(request) and (Topic.author == request.user) + + class Post(Comment): """ @@ -404,7 +426,19 @@ def get_absolute_url(self): self.topic.get_absolute_url(), page, self.pk) - + + @staticmethod + def has_write_permission(request): + return request.user.is_authenticated() and request.user.profile.can_write_now() + + def has_object_write_permission(self, request): + return Topic.has_write_permission(request) + # TODO verifier que ce n'est pas un forum prive + + def has_object_update_permission(self, request): + return self.is_author(request.user) + # TODO peut on editer quand un topic est ferme ? + # TODO a tester, l'auteur avait acces a ubn forum prive, mais ce n'est plus le cas, peut il editer ses messages class TopicRead(models.Model): """ @@ -425,8 +459,8 @@ def __unicode__(self): return u''.format(self.topic, self.user, self.post.pk) - - + + def is_read(topic, user=None): """ Checks if the user has read the **last post** of the topic. From 12e6167d5b0b0a4edae2b66fc3150a2aff31b041 Mon Sep 17 00:00:00 2001 From: Antonin Date: Mon, 5 Dec 2016 14:29:27 +0000 Subject: [PATCH 02/78] ajout de routes, netoyage, test --- zds/forum/api/serializer.py | 42 +++++-- zds/forum/api/tests.py | 225 ++++++++++++++++++++++++++++++++---- zds/forum/api/views.py | 102 +++++++++------- zds/member/factories.py | 57 +++++++++ 4 files changed, 349 insertions(+), 77 deletions(-) diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index 50dbc5093b..bf1e9a8eb3 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -1,4 +1,5 @@ -from rest_framework.serializers import ModelSerializer, SerializerMethodField +from rest_framework.serializers import ModelSerializer +from rest_framework import serializers from zds.forum.models import Forum, Topic, Post from dry_rest_permissions.generics import DRYPermissionsField from dry_rest_permissions.generics import DRYPermissions @@ -8,8 +9,8 @@ class ForumSerializer(ModelSerializer): class Meta: model = Forum permissions_classes = DRYPermissions - +# Renomer en ForumPostSerializer class ForumActionSerializer(ModelSerializer): """ Serializer to create a new forum @@ -21,11 +22,31 @@ class Meta: #fields = ('id', 'text', 'text_html', 'permissions') # read_only_fields = ('text_html', 'permissions') read_only_fields = ('slug',) - + def create(self, validated_data): new_forum = Forum.objects.create(**validated_data) return new_forum +class ForumUpdateSerializer(ModelSerializer): + """ + Serializer to update a forum. + """ + can_be_empty = True + title = serializers.CharField(required=False, allow_blank=True) + subtitle = serializers.CharField(required=False, allow_blank=True) + # Ajouter category et eventuellement autre TODO + permissions = DRYPermissionsField() + + class Meta: + model = Forum + fields = ('id', 'title', 'subtitle','permissions',) + read_only_fields = ('id','slug','permissions',) # TODO a voir si besoin d'autres champs + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance class TopicSerializer(ModelSerializer): @@ -34,7 +55,7 @@ class Meta: #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') permissions_classes = DRYPermissions - +# Idem renommer class TopicActionSerializer(ModelSerializer): """ Serializer to create a new topic. @@ -43,20 +64,20 @@ class TopicActionSerializer(ModelSerializer): class Meta: model = Topic - read_only_fields = ('slug','author') - + read_only_fields = ('slug','author',) + def create(self, validated_data): new_topic = Topic.objects.create(**validated_data) return new_topic - class PostSerializer(ModelSerializer): class Meta: model = Post #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') permissions_classes = DRYPermissions - + + class PostActionSerializer(ModelSerializer): """ Serializer to send a post in a topic @@ -73,10 +94,7 @@ def create(self, validated_data): # Get topic pk_topic = validated_data.get('topic_id') topic = get_object_or_404(Topic, pk=(pk_topic)) - - new_post = Post.objects.create(**validated_data) - - + Post.objects.create(**validated_data) return topic.last_message # Todo a t on besoin d'un validateur diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index b8cd3bf36f..1c0bbbb0c1 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -3,24 +3,29 @@ from django.conf import settings from django.core.cache import caches from django.core.urlresolvers import reverse -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from rest_framework import status from rest_framework.test import APIClient from rest_framework.test import APITestCase from oauth2_provider.models import Application, AccessToken from rest_framework_extensions.settings import extensions_api_settings from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM -from zds.member.factories import ProfileFactory, StaffProfileFactory, ProfileNotSyncFactory - +from zds.member.factories import ProfileFactory, StaffProfileFactory, AdminProfileFactory from zds.forum.factories import PostFactory from zds.forum.tests.tests_views import create_category, add_topic_in_a_forum -from zds.member.factories import ProfileFactory from zds.utils.models import CommentVote class ForumPostKarmaAPITest(APITestCase): def setUp(self): + self.client = APIClient() + self.profile = ProfileFactory() + + client_oauth2 = create_oauth2_client(self.profile.user) + self.client_authenticated = APIClient() + authenticate_client(self.client_authenticated, client_oauth2, self.profile.user.username, 'hostel77') + caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() def test_failure_post_karma_with_client_unauthenticated(self): @@ -213,18 +218,38 @@ class ForumAPITest(APITestCase): def setUp(self): self.client = APIClient() - self.profile = ProfileFactory() client_oauth2 = create_oauth2_client(self.profile.user) client_authenticated = APIClient() authenticate_client(client_authenticated, client_oauth2, self.profile.user.username, 'hostel77') + self.client_admin = APIClient() + self.profile_admin = AdminProfileFactory() + + print(self.profile_admin.user) + print(self.profile_admin.user.username) + + admin_user = User.objects.get(username=self.profile_admin.user.username) + + client_oauth2_admin = create_oauth2_client(self.profile_admin.user) + self.client_authenticated_admin = APIClient() + authenticate_client(self.client_authenticated_admin, client_oauth2_admin, admin_user.username, 'admin') + + caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() def create_multiple_forums(self, number_of_forum=REST_PAGE_SIZE): for forum in xrange(0, number_of_forum): category, forum = create_category() + def create_multiple_forums_with_topics(self, number_of_forum=REST_PAGE_SIZE, number_of_topic=REST_PAGE_SIZE): + profile = ProfileFactory() + for forum in xrange(0, number_of_forum): + category, forum = create_category() + for topic in xrange(0, number_of_topic): + new_topic = add_topic_in_a_forum(forum, profile) + + def test_list_of_forums_empty(self): """ Gets empty list of forums in the database. @@ -364,23 +389,22 @@ def test_new_forum_with_user(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_new_forum_with_staff(self): + def test_new_forum_with_admin(self): """ - Tries to create a new forum with an staff user. + Tries to create a new forum with an admin user. """ data = { - 'titre': 'Flask', + 'title': 'Flask', 'subtitle': 'Flask is the best framework EVER !', - 'categorie': '2' + 'category': '2' } self.create_multiple_forums(5) - self.staff = StaffProfileFactory() - client_oauth2 = create_oauth2_client(self.staff.user) - self.client_authenticated_staff = APIClient() - authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') - response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) + authenticate_client(self.client_authenticated_admin, client_oauth2, self.admin.user.username, 'hostel77') + + response = self.client_authenticated_admin.post(reverse('api:forum:list'), data) + print(response) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -389,11 +413,11 @@ def test_new_forum_slug_collision(self): Tries to create two forums with the same title to see if the slug generated is different. """ data = { - 'titre': 'Flask', + 'title': 'Flask', 'subtitle': 'Flask is the best framework EVER !', - 'categorie': '2' + 'category': '2' } - + self.create_multiple_forums(5) response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -408,7 +432,7 @@ def test_new_forum_without_title(self): """ data = { 'subtitle': 'Flask is the best framework EVER !', - 'categorie': '2' + 'category': '2' } self.create_multiple_forums(5) @@ -420,8 +444,8 @@ def test_new_forum_without_subtitle(self): Tries to create a new forum with an staff user, without a subtitle. """ data = { - 'titre': 'Flask', - 'categorie': '2' + 'title': 'Flask', + 'category': '2' } self.create_multiple_forums(5) @@ -433,15 +457,174 @@ def test_new_forum_without_category(self): Tries to create a new forum with an staff user without a category. """ data = { - 'titre': 'Flask', + 'title': 'Flask', 'subtitle': 'Flask is the best framework EVER !', } self.create_multiple_forums(5) response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + + def test_details_forum(self): + """ + Tries to get the details of a forum. + """ + + category, forum = create_category() + response = self.client.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + + def test_details_unknown_forum(self): + """ + Tries to get the details of a forum that does not exists. + """ + + self.create_multiple_forums(1) + response = self.client.get(reverse('api:forum:detail', args=[3])) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +# TODO Récupérer un forum prive unthaurized si anonyme ou membre, 200 sinon + +#Récupérer la liste des sujets en filtrant sur l'auteur +#Récupérer la liste des sujets en filtrant sur le tag +#Récupérer la liste des sujets en filtrant sur le forum +#Récupérer la liste des sujets en filtrant tag, forum, auteur +#Idem avec un tag inexistant NE MARCHE PAS + + def test_list_of_topics_empty(self): + """ + Gets empty list of topics in the database. + """ + response = self.client.get(reverse('api:forum:list-topic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_topics(self): + """ + Gets list of topics not empty in the database. + """ + self.create_multiple_forums_with_topics(1) + response = self.client.get(reverse('api:forum:list-topic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_topics_with_several_pages(self): + """ + Gets list of topics with several pages in the database. + """ + self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list-topic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + + response = self.client.get(reverse('api:forum:list-topic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), 1) + + def test_list_of_topics_for_a_page_given(self): + """ + Gets list of topics with several pages and gets a page different that the first one. + """ + self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list-topic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 11) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + def test_list_of_topics_for_a_wrong_page_given(self): + """ + Gets an error when the topics asks a wrong page. + """ + response = self.client.get(reverse('api:forum:list-topic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_list_of_topics_with_a_custom_page_size(self): + """ + Gets list of topics with a custom page size. DRF allows to specify a custom + size for the pagination. + """ + self.create_multiple_forums_with_topics(1, REST_PAGE_SIZE * 2) + + page_size = 'page_size' + response = self.client.get(reverse('api:forum:list-topic') + '?{}=20'.format(page_size)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 20) + self.assertEqual(len(response.data.get('results')), 20) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_PAGE_SIZE_QUERY_PARAM, page_size) + + def test_list_of_topics_with_a_wrong_custom_page_size(self): + """ + Gets list of topics with a custom page size but not good according to the + value in settings. + """ + page_size_value = REST_MAX_PAGE_SIZE + 1 + self.create_multiple_forums_with_topics(1, page_size_value) + + response = self.client.get(reverse('api:forum:list-topic') + '?page_size={}'.format(page_size_value)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), page_size_value) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) + + def test_list_of_topics_with_forum_filter_empty(self): + """ + Gets an empty list of topics in a forum. + """ + self.create_multiple_forums_with_topics(1) + response = self.client.get(reverse('api:forum:list-topic') + '?forum=3') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_topics_with_author_filter_empty(self): + """ + Gets an empty list of topics created by an user. + """ + self.create_multiple_forums_with_topics(1) + response = self.client.get(reverse('api:forum:list-topic') + '?author=6') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + # TODO ne marche pas + def test_list_of_topics_with_tag_filter_empty(self): + """ + Gets an empty list of topics with a specific tag. + """ + self.create_multiple_forums_with_topics(1) + response = self.client.get(reverse('api:forum:list-topic') + '?tag=ilovezozor') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + def create_oauth2_client(user): client = Application.objects.create(user=user, diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 5e66a19e3a..e7742222c3 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -7,21 +7,20 @@ from django.core.cache import cache from django.db.models.signals import post_save, post_delete from rest_framework import filters -from rest_framework.generics import ListAPIView -from rest_framework.generics import ListCreateAPIView, RetrieveUpdateAPIView +from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpdateAPIView from rest_framework_extensions.key_constructor.constructors import DefaultKeyConstructor from rest_framework_extensions.cache.decorators import cache_response from rest_framework_extensions.etag.decorators import etag from rest_framework_extensions.key_constructor import bits -from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit -from zds.utils import slugify -from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, PostSerializer, PostActionSerializer, ForumActionSerializer from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticatedOrReadOnly, AllowAny, IsAuthenticated from dry_rest_permissions.generics import DRYPermissions +from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit +from zds.utils import slugify +from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, PostSerializer, PostActionSerializer, ForumActionSerializer, ForumUpdateSerializer from zds.forum.api.permissions import IsStaffUser -from django.http import HttpResponseRedirect + class PostKarmaView(KarmaView): queryset = Post.objects.all() @@ -35,6 +34,7 @@ class PagingSearchListKeyConstructor(DefaultKeyConstructor): user = bits.UserKeyBit() updated_at = UpdatedAtKeyBit('api_updated_forum') + class DetailKeyConstructor(DefaultKeyConstructor): format = bits.FormatKeyBit() language = bits.LanguageKeyBit() @@ -44,7 +44,6 @@ class DetailKeyConstructor(DefaultKeyConstructor): updated_at = UpdatedAtKeyBit('api_updated_forum') - def change_api_forum_updated_at(sender=None, instance=None, *args, **kwargs): cache.set('forum_updated_tag', datetime.datetime.utcnow()) @@ -61,7 +60,6 @@ class ForumListAPI(ListCreateAPIView): queryset = Forum.objects.all() list_key_func = PagingSearchListKeyConstructor() - @etag(list_key_func) @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): @@ -83,7 +81,7 @@ def get(self, request, *args, **kwargs): message: Not Found """ return self.list(request, *args, **kwargs) - + def post(self, request, *args, **kwargs): """ Creates a new forum. @@ -114,6 +112,7 @@ def post(self, request, *args, **kwargs): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def get_serializer_class(self): if self.request.method == 'GET': return ForumSerializer @@ -132,7 +131,7 @@ class ForumDetailAPI(RetrieveUpdateAPIView): """ Profile resource to display or update details of a forum. """ - + queryset = Forum.objects.all() obj_key_func = DetailKeyConstructor() serializer_class = ForumSerializer @@ -149,9 +148,39 @@ def get(self, request, *args, **kwargs): """ forum = self.get_object() serializer = self.get_serializer(forum) - + return self.retrieve(request, *args, **kwargs) - + + def put(self, request, *args, **kwargs): + """ + Updates a forum, must be admin to perform this action. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + responseMessages: + - code: 400 + message: Bad Request + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied + - code: 404 + message: Not Found + """ + # TODO doc incppmplete + return self.update(request, *args, **kwargs) + + + def get_serializer_class(self): + if self.request.method == 'GET': + return ForumSerializer + elif self.request.method == 'PUT': + return ForumUpdateSerializer + class TopicListAPI(ListCreateAPIView): """ @@ -160,11 +189,9 @@ class TopicListAPI(ListCreateAPIView): queryset = Topic.objects.all() serializer_class = TopicSerializer filter_backends = (filters.DjangoFilterBackend,) - filter_fields = ('forum','author','tags__title') + filter_fields = ('forum', 'author', 'tags__title') list_key_func = PagingSearchListKeyConstructor() - - @etag(list_key_func) @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): @@ -185,9 +212,9 @@ def get(self, request, *args, **kwargs): - code: 404 message: Not Found """ - + return self.list(request, *args, **kwargs) - + def post(self, request, *args, **kwargs): """ Creates a new topic. @@ -210,7 +237,7 @@ def post(self, request, *args, **kwargs): """ author = request.user - + serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) topic = serializer.save(author_id=3) @@ -230,8 +257,8 @@ def get_permissions(self): permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) return [permission() for permission in permission_classes] - - + + class UserTopicListAPI(ListAPIView): """ @@ -240,7 +267,7 @@ class UserTopicListAPI(ListAPIView): serializer_class = TopicSerializer filter_backends = (filters.DjangoFilterBackend,) - filter_fields = ('forum','tags__title') + filter_fields = ('forum', 'tags__title') list_key_func = PagingSearchListKeyConstructor() @@ -266,7 +293,7 @@ def get(self, request, *args, **kwargs): """ # TODO code d'auth manquant en commentaire return self.list(request, *args, **kwargs) - + def get_queryset(self): topics = Topic.objects.filter(author=self.request.user) return topics @@ -276,7 +303,6 @@ class TopicDetailAPI(RetrieveUpdateAPIView): """ Profile resource to display details of a given topic """ - queryset = Topic.objects.all() obj_key_func = DetailKeyConstructor() serializer_class = TopicSerializer @@ -293,18 +319,15 @@ def get(self, request, *args, **kwargs): """ topic = self.get_object() #serializer = self.get_serializer(topic) - + return self.retrieve(request, *args, **kwargs) - + class PostListAPI(ListCreateAPIView): """ Profile resource to list all message in a topic """ - list_key_func = PagingSearchListKeyConstructor() - #serializer_class = PostSerializer - @etag(list_key_func) @cache_response(key_func=list_key_func) @@ -328,7 +351,7 @@ def get(self, request, *args, **kwargs): """ return self.list(request, *args, **kwargs) # TODO si message cache ? Le cacher dans l'API - + def post(self, request, *args, **kwargs): """ Creates a new post in a topic. @@ -353,11 +376,10 @@ def post(self, request, *args, **kwargs): #TODO GERE les droits et l'authentification --> en cours : tester avec connection pour voir si cela fonctionne #TODO passer les arguments corrects a save author = request.user - serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) - topic = serializer.save(position=2,author_id=3,topic_id=1) + topic = serializer.save(position=2, author_id=3, topic_id=1) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) @@ -367,16 +389,14 @@ def get_serializer_class(self): elif self.request.method == 'POST': return PostActionSerializer - def get_queryset(self): if self.request.method == 'GET': posts = Post.objects.filter(topic=self.kwargs.get('pk')) return posts - - + def get_current_user(self): return self.request.user.profile - + def get_permissions(self): permission_classes = [AllowAny, ] if self.request.method == 'POST': @@ -389,11 +409,9 @@ class MemberPostListAPI(ListAPIView): """ Profile resource to list all posts from a member """ - list_key_func = PagingSearchListKeyConstructor() serializer_class = PostSerializer - @etag(list_key_func) @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): @@ -417,7 +435,6 @@ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) # TODO fonctionne mais error xml sur certains post http://zds-anto59290.c9users.io/api/forums/membres/3/sujets - def get_queryset(self): if self.request.method == 'GET': posts = Post.objects.filter(author=self.kwargs.get('pk')) @@ -432,7 +449,6 @@ class UserPostListAPI(ListAPIView): list_key_func = PagingSearchListKeyConstructor() serializer_class = PostSerializer - @etag(list_key_func) @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): @@ -465,7 +481,7 @@ class PostDetailAPI(RetrieveUpdateAPIView): """ Profile resource to display details of given message """ - + queryset = Post.objects.all() obj_key_func = DetailKeyConstructor() serializer_class = PostSerializer @@ -481,10 +497,8 @@ def get(self, request, *args, **kwargs): message: Not Found """ post = self.get_object() - - return self.retrieve(request, *args, **kwargs) - + return self.retrieve(request, *args, **kwargs) # TODO global identier quand masquer les messages -# TOD gerer l'antispam \ No newline at end of file +# TOD gerer l'antispam diff --git a/zds/member/factories.py b/zds/member/factories.py index 179063c3d9..4cc4222db9 100644 --- a/zds/member/factories.py +++ b/zds/member/factories.py @@ -68,6 +68,44 @@ def _prepare(cls, create, **kwargs): return user +class AdminFactory(factory.DjangoModelFactory): + """ + This factory creates admin User. + WARNING: Don't try to directly use `AdminFactory`, this didn't create associated Profile then don't work! + Use `AdminProfileFactory` instead. + """ + class Meta: + model = User + + username = factory.Sequence('admin{0}'.format) + email = factory.Sequence('firmstaff{0}@zestedesavoir.com'.format) + password = 'hostel77' + + is_superuser = True + is_staff = True + is_active = True + + @classmethod + def _prepare(cls, create, **kwargs): + password = kwargs.pop('password', None) + user = super(AdminFactory, cls)._prepare(create, **kwargs) + if password: + user.set_password(password) + if create: + user.save() + group_staff = Group.objects.filter(name="staff").first() + if group_staff is None: + group_staff = Group(name="staff") + group_staff.save() + + perms = Permission.objects.all() + group_staff.permissions = perms + user.groups.add(group_staff) + + user.save() + return user + + class ProfileFactory(factory.DjangoModelFactory): """ Use this factory when you need a complete Profile for a standard user. @@ -113,6 +151,25 @@ def biography(self): sign = 'Please look my flavour' +class AdminProfileFactory(factory.DjangoModelFactory): + """ + Use this factory when you need a complete admin Profile for a user. + """ + class Meta: + model = Profile + + user = factory.SubFactory(AdminFactory) + + last_ip_address = '192.168.2.1' + site = 'www.zestedesavoir.com' + + @factory.lazy_attribute + def biography(self): + return u'My name is {0} and I i\'m the guy who control the guy that kill the bad guys '.format(self.user.username.lower()) + + sign = 'Please look my flavour' + + class NonAsciiUserFactory(UserFactory): """ This factory creates standard user with non-ASCII characters in its username. From 1d6c8888a5b87c27e4e95c10a4baafc0cc74a99a Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 15 Dec 2016 10:08:54 +0000 Subject: [PATCH 03/78] Remove useless routes, add tests, add permissions, typos, styling --- zds/forum/api/serializer.py | 132 ++-- zds/forum/api/tests.py | 1160 ++++++++++++++++++++++++++++++----- zds/forum/api/urls.py | 5 +- zds/forum/api/views.py | 244 +++++--- zds/forum/models.py | 11 +- 5 files changed, 1253 insertions(+), 299 deletions(-) diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index bf1e9a8eb3..ecb0d34f1b 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -1,46 +1,77 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers from zds.forum.models import Forum, Topic, Post +from zds.utils.models import Alert from dry_rest_permissions.generics import DRYPermissionsField from dry_rest_permissions.generics import DRYPermissions from django.shortcuts import get_object_or_404 + class ForumSerializer(ModelSerializer): class Meta: model = Forum permissions_classes = DRYPermissions -# Renomer en ForumPostSerializer -class ForumActionSerializer(ModelSerializer): + +class TopicSerializer(ModelSerializer): + class Meta: + model = Topic + #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') + permissions_classes = DRYPermissions + + +# Idem renommer TODO +class TopicActionSerializer(ModelSerializer): """ - Serializer to create a new forum + Serializer to create a new topic. """ + text = serializers.SerializerMethodField() permissions = DRYPermissionsField() class Meta: - model = Forum - #fields = ('id', 'text', 'text_html', 'permissions') - # read_only_fields = ('text_html', 'permissions') - read_only_fields = ('slug',) - + model = Topic + fields = ('id', 'title', 'subtitle', 'text', 'forum', + 'author', 'last_message', 'pubdate', + 'permissions', 'slug') + read_only_fields = ('id', 'author', 'last_message', 'pubdate', 'permissions') +# TODO je pense qu'avec cette config on peut deplacer un sujet en tant qu'user +# TODO le text deconne def create(self, validated_data): - new_forum = Forum.objects.create(**validated_data) - return new_forum + + text = self._fields.pop('text') + new_topic = Topic.objects.create(**validated_data) + first_message = Post.objects.create(topic=new_topic,text=text,position=0,author=new_topic.author) + new_topic.last_message = first_message + new_topic.save() + return new_topic +""" -class ForumUpdateSerializer(ModelSerializer): + def create(self, validated_data): + # This hack is necessary because `text` isn't a field of PrivateTopic. + self._fields.pop('text') + return send_mp(self.context.get('request').user, + validated_data.get('participants'), + validated_data.get('title'), + validated_data.get('subtitle') or '', + validated_data.get('text'), + True, + False) +""" + +class TopicUpdateSerializer(ModelSerializer): """ - Serializer to update a forum. + Serializer to update a topic. """ - can_be_empty = True + can_be_empty = True # TODO verifier l'interet de cette ligne title = serializers.CharField(required=False, allow_blank=True) subtitle = serializers.CharField(required=False, allow_blank=True) - # Ajouter category et eventuellement autre TODO + # TODO ajouter les tag et la resolution permissions = DRYPermissionsField() class Meta: - model = Forum - fields = ('id', 'title', 'subtitle','permissions',) - read_only_fields = ('id','slug','permissions',) # TODO a voir si besoin d'autres champs + model = Topic + fields = ('id', 'title', 'subtitle', 'permissions',) + read_only_fields = ('id', 'permissions',) def update(self, instance, validated_data): for attr, value in validated_data.items(): @@ -49,35 +80,13 @@ def update(self, instance, validated_data): return instance -class TopicSerializer(ModelSerializer): - class Meta: - model = Topic - #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') - permissions_classes = DRYPermissions - -# Idem renommer -class TopicActionSerializer(ModelSerializer): - """ - Serializer to create a new topic. - """ - permissions = DRYPermissionsField() - - class Meta: - model = Topic - read_only_fields = ('slug','author',) - - def create(self, validated_data): - new_topic = Topic.objects.create(**validated_data) - return new_topic - - class PostSerializer(ModelSerializer): class Meta: model = Post #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') permissions_classes = DRYPermissions - +# TODO renommer class PostActionSerializer(ModelSerializer): """ Serializer to send a post in a topic @@ -100,3 +109,46 @@ def create(self, validated_data): # Todo a t on besoin d'un validateur #def throw_error(self, key=None, message=None): #raise serializers.ValidationError(message) + +class PostUpdateSerializer(ModelSerializer): + """ + Serializer to update a post. + """ + can_be_empty = True # TODO verifier l'interet de cette ligne + text = serializers.CharField(required=True, allow_blank=True) + permissions = DRYPermissionsField() + + class Meta: + model = Topic + fields = ('id', 'text', 'permissions',) + read_only_fields = ('id', 'permissions',) + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + + +class AlertSerializer(ModelSerializer): + """ + Serializer to alert a post. + """ + permissions = DRYPermissionsField() + + class Meta: + model = Alert + fields = ('id', 'text', 'permissions',) + read_only_fields = ('permissions',) + + + def create(self, validated_data): + # Get topic + pk_post = validated_data.get('comment') + post = get_object_or_404(Post, pk=(pk_post)) + Alert.objects.create(**validated_data) + return topic.last_message + + # Todo a t on besoin d'un validateur + #def throw_error(self, key=None, message=None): + #raise serializers.ValidationError(message) \ No newline at end of file diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 1c0bbbb0c1..de51d09f69 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -3,14 +3,15 @@ from django.conf import settings from django.core.cache import caches from django.core.urlresolvers import reverse -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import Group from rest_framework import status from rest_framework.test import APIClient from rest_framework.test import APITestCase from oauth2_provider.models import Application, AccessToken from rest_framework_extensions.settings import extensions_api_settings from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM -from zds.member.factories import ProfileFactory, StaffProfileFactory, AdminProfileFactory +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.forum.models import Forum, Topic, Post from zds.forum.factories import PostFactory from zds.forum.tests.tests_views import create_category, add_topic_in_a_forum from zds.utils.models import CommentVote @@ -18,7 +19,7 @@ class ForumPostKarmaAPITest(APITestCase): def setUp(self): - + self.client = APIClient() self.profile = ProfileFactory() @@ -205,36 +206,22 @@ def test_get_post_voters(self): self.assertEqual(1, response.data['like']['count']) self.assertEqual(1, response.data['dislike']['count']) -# Lister les forums vide -# Lister les forum, 200 une seule page -# Liste des forum plusieurs page -# Lister les forums avec page size -# Lister les forums avec une page de trop -# Liste les forums avec page size et accéder a la page deux # Liste les forums avec un staff (on boit les forums privé) class ForumAPITest(APITestCase): def setUp(self): self.client = APIClient() - + self.profile = ProfileFactory() client_oauth2 = create_oauth2_client(self.profile.user) - client_authenticated = APIClient() - authenticate_client(client_authenticated, client_oauth2, self.profile.user.username, 'hostel77') - - self.client_admin = APIClient() - self.profile_admin = AdminProfileFactory() - - print(self.profile_admin.user) - print(self.profile_admin.user.username) - - admin_user = User.objects.get(username=self.profile_admin.user.username) - - client_oauth2_admin = create_oauth2_client(self.profile_admin.user) - self.client_authenticated_admin = APIClient() - authenticate_client(self.client_authenticated_admin, client_oauth2_admin, admin_user.username, 'admin') + self.client_authenticated = APIClient() + authenticate_client(self.client_authenticated, client_oauth2, self.profile.user.username, 'hostel77') + self.staff = StaffProfileFactory() + client_oauth2 = create_oauth2_client(self.staff.user) + self.client_authenticated_staff = APIClient() + authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() @@ -242,13 +229,27 @@ def create_multiple_forums(self, number_of_forum=REST_PAGE_SIZE): for forum in xrange(0, number_of_forum): category, forum = create_category() - def create_multiple_forums_with_topics(self, number_of_forum=REST_PAGE_SIZE, number_of_topic=REST_PAGE_SIZE): - profile = ProfileFactory() + def create_multiple_forums_with_topics(self, number_of_forum=REST_PAGE_SIZE, number_of_topic=REST_PAGE_SIZE, profile=None): + if profile is None: + profile = ProfileFactory() for forum in xrange(0, number_of_forum): category, forum = create_category() for topic in xrange(0, number_of_topic): new_topic = add_topic_in_a_forum(forum, profile) + if number_of_forum == 1 and number_of_topic == 1: + return new_topic + + def create_topic_with_post(self, number_of_post=REST_PAGE_SIZE, profile=None): + if profile is None: + profile = ProfileFactory() + category, forum = create_category() + new_topic = add_topic_in_a_forum(forum, profile) + + for post in xrange(0, number_of_post): + PostFactory(topic=new_topic.id, author=profile.user, position=2) + + return new_topic def test_list_of_forums_empty(self): """ @@ -345,127 +346,6 @@ def test_list_of_forums_with_a_wrong_custom_page_size(self): self.assertIsNone(response.data.get('previous')) self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) -# TODO regler le probleme de l-user admin -#done Créer un forum en étant anonyme 401 -#done Créer un forum en étant membre 401 -#done Créer un forum en étant staff 200 -#done Creer un fórum avec un titre qui existe deja pour tester le slug -#done Creer un fórum sans categorie -#done Crrer un fórum sans titre -#done Crrer un fórum sans soustitre -#done creer un forum sans categorie -#avec titre vide/soustitre vide et categorie vide -# creer une forum dans une categorie qui n exist epas - - def test_new_forum_with_anonymous(self): - """ - Tries to create a new forum with an anonymous (non authentified) user. - """ - data = { - 'titre': 'Flask', - 'subtitle': 'Flask is the best framework EVER !', - 'categorie': '2' - } - - self.create_multiple_forums(5) - response = self.client.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - - - def test_new_forum_with_user(self): - """ - Tries to create a new forum with an user. - """ - data = { - 'titre': 'Flask', - 'subtitle': 'Flask is the best framework EVER !', - 'categorie': '2' - } - - self.create_multiple_forums(5) - client = APIClient() - response = client.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - - def test_new_forum_with_admin(self): - """ - Tries to create a new forum with an admin user. - """ - data = { - 'title': 'Flask', - 'subtitle': 'Flask is the best framework EVER !', - 'category': '2' - } - - self.create_multiple_forums(5) - - authenticate_client(self.client_authenticated_admin, client_oauth2, self.admin.user.username, 'hostel77') - - response = self.client_authenticated_admin.post(reverse('api:forum:list'), data) - print(response) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - - def test_new_forum_slug_collision(self): - """ - Tries to create two forums with the same title to see if the slug generated is different. - """ - data = { - 'title': 'Flask', - 'subtitle': 'Flask is the best framework EVER !', - 'category': '2' - } - - self.create_multiple_forums(5) - response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response2 = self.client_authenticated_staff.post(reverse('api:forum:list'), data) - self.assertEqual(response2.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(response.data.get('slug'), response2.data.get('slug')) - - def test_new_forum_without_title(self): - """ - Tries to create a new forum with an staff user, without a title. - """ - data = { - 'subtitle': 'Flask is the best framework EVER !', - 'category': '2' - } - - self.create_multiple_forums(5) - response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_new_forum_without_subtitle(self): - """ - Tries to create a new forum with an staff user, without a subtitle. - """ - data = { - 'title': 'Flask', - 'category': '2' - } - - self.create_multiple_forums(5) - response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_new_forum_without_category(self): - """ - Tries to create a new forum with an staff user without a category. - """ - data = { - 'title': 'Flask', - 'subtitle': 'Flask is the best framework EVER !', - } - - self.create_multiple_forums(5) - response = self.client_authenticated_staff.post(reverse('api:forum:list'), data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_details_forum(self): """ Tries to get the details of a forum. @@ -474,7 +354,13 @@ def test_details_forum(self): category, forum = create_category() response = self.client.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) - + self.assertEqual(response.data.get('id'), forum.id) + self.assertEqual(response.data.get('title'), forum.title) + self.assertEqual(response.data.get('subtitle'), forum.subtitle) + self.assertEqual(response.data.get('slug'), forum.slug) + self.assertEqual(response.data.get('category'), forum.category) + self.assertEqual(response.data.get('position_in_category'), forum.position_in_category) + self.assertEqual(response.data.get('group'), forum.group) def test_details_unknown_forum(self): """ @@ -485,14 +371,29 @@ def test_details_unknown_forum(self): response = self.client.get(reverse('api:forum:detail', args=[3])) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_details_private_forum_user(self): + """ + Tries to get the details of a private forum with a normal user, staff user and anonymous one. + """ + group = Group.objects.create(name="staff") + category, forum = create_category(group) + + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authentificated.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) -# TODO Récupérer un forum prive unthaurized si anonyme ou membre, 200 sinon + response = self.client_authentificated_staff.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) -#Récupérer la liste des sujets en filtrant sur l'auteur -#Récupérer la liste des sujets en filtrant sur le tag -#Récupérer la liste des sujets en filtrant sur le forum -#Récupérer la liste des sujets en filtrant tag, forum, auteur -#Idem avec un tag inexistant NE MARCHE PAS +# TODO +# Récupérer la liste des sujets en filtrant sur l'auteur (resulat non vide) +# Récupérer la liste des sujets en filtrant sur le tag (resulat non vide) +# Récupérer la liste des sujets en filtrant sur le forum (resulat non vide) +# Récupérer la liste des sujets en filtrant tag, forum, auteur (resulat non vide) +# Idem avec un tag inexistant NE MARCHE PAS def test_list_of_topics_empty(self): """ @@ -521,7 +422,7 @@ def test_list_of_topics_with_several_pages(self): """ Gets list of topics with several pages in the database. """ - self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE + 1) + self.create_multiple_forums_with_topics(1, REST_PAGE_SIZE + 1) response = self.client.get(reverse('api:forum:list-topic')) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -541,7 +442,7 @@ def test_list_of_topics_for_a_page_given(self): """ Gets list of topics with several pages and gets a page different that the first one. """ - self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE + 1) + self.create_multiple_forums_with_topics(1, REST_PAGE_SIZE + 1) response = self.client.get(reverse('api:forum:list-topic') + '?page=2') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -587,7 +488,7 @@ def test_list_of_topics_with_a_wrong_custom_page_size(self): self.assertIsNotNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) - + def test_list_of_topics_with_forum_filter_empty(self): """ Gets an empty list of topics in a forum. @@ -612,7 +513,6 @@ def test_list_of_topics_with_author_filter_empty(self): self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) - # TODO ne marche pas def test_list_of_topics_with_tag_filter_empty(self): """ Gets an empty list of topics with a specific tag. @@ -624,7 +524,949 @@ def test_list_of_topics_with_tag_filter_empty(self): self.assertEqual(response.data.get('results'), []) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) - + + def test_new_topic_with_user(self): + """ + Post a new topic in a forum with an user. + """ + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'subtitle': 'Is it the best framework ?', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 1 + } + + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + topics = Topic.objects.filter(author=self.profile.user.id) + print(topics[0]) + self.assertEqual(1, len(topics)) + self.assertEqual(response.data.get('title'), topics[0].title) + self.assertEqual(response.data.get('subtitle'), topics[0].subtitle) + # Todo ne fonctionne pas self.assertEqual(data.get('text'), topics[0].last_message.text) + self.assertEqual(response.data.get('author'), self.profile.user.id) + self.assertIsNotNone(response.data.get('last_message')) + self.assertIsNotNone(response.data.get('pubdate')) + + def test_new_topic_with_anonymous(self): + """ + Post a new topic in a forum with an anonymous user. + """ + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'subtitle': 'Is it the best framework ?', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 1 + } + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_new_topic_private_forum(self): + """ + Post a new topic in a private forum (staff only) with an anonymous user, normal user and staff user. + """ + + group = Group.objects.create(name="staff") + + profile = ProfileFactory() + group.user_set.add(profile.user) + category, forum = create_category(group) + data = { + 'title': 'Have you seen the guy flooding ?', + 'subtitle': 'He is asking to many question about flask.', + 'text': 'Should we ban him ? I think we should.', + 'forum': forum.id + } + + # Anonymous + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # User + response = self.client_authentificated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Staff + response = self.client_authentificated_staff.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_new_topic_with_banned_user(self): + + profile = ProfileFactory() + profile.can_read = False + profile.can_write = False + profile.save() + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'subtitle': 'Is it the best framework ?', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 1 + } + + self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) + response = self.client.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_new_topic_without_title(self): + """ + Try to post a new topic in a forum without the title + """ + self.create_multiple_forums() + data = { + 'subtitle': 'Is it the best framework ?', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 1 + } + + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_topic_without_subtitle(self): + """ + Try to post a new topic in a forum without the title + """ + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 1 + } + + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_new_topic_without_text(self): + """ + Try to post a new topic in a forum without the text. + """ + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'subtitle': 'Is it the best framework ?', + 'forum': 1 + } + + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_topic_in_unknow_forum(self): + """ + Try to post a new topic in a forum that does not exists. + """ + self.create_multiple_forums() + data = { + 'title': 'Flask 4 Ever !', + 'subtitle': 'Is it the best framework ?', + 'text': 'I head that Flask is the best framework ever, is that true ?', + 'forum': 666 + } + + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_details_topic(self): + """ + Get details of a topic. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail-topic', args=(topic.id,))) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('title'), topic.title) + self.assertEqual(response.data.get('subtitle'), topic.subtitle) + self.assertEqual(response.data.get('forum'), topic.forum.id) + self.assertIsNotNone(response.data.get('title')) + self.assertIsNotNone(response.data.get('forum')) + + def test_details_unknown_topic(self): + """ + Get details of a topic that doesw not exist. + """ + + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail-topic', args=(666))) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_details_topic_private(self): + """ + Tries to get details of a topic that is in a private forum. + """ + + group = Group.objects.create(name="staff") + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, self.staff) + + # Anonymous + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail-topic', args=(topic.id))) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # User + response = self.client_authenticated.get(reverse('api:forum:detail-topic', args=(topic.id))) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Staff + response = self.client_authenticated_staff.get(reverse('api:forum:detail-topic', args=(topic.id))) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_new_post_anonymous(self): + """ + Try to post a new post with anonymous user. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + data = { + 'text': 'I head that Flask is the best framework ever, is that true ?' + } + + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-post', args=(topic.id)), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_new_post_user(self): + """ + Try to post a new post with an user. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + data = { + 'text': 'I head that Flask is the best framework ever, is that true ?' + } + + self.client = APIClient() + response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id)), data) + topic = Topic.objects.filter(id=topic.id) + print(topic) + last_message = topic.get_last_post() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data.get('text'), last_message.text) + + def test_new_post_user_with_restrictions(self): + """ + Try to post a new post with an user that has some restrictions . + """ + profile = ProfileFactory() + profile.can_read = False + profile.can_write = False + profile.save() + topic = self.create_multiple_forums_with_topics(1, 1) + data = { + 'text': 'I head that Flask is the best framework ever, is that true ?' + } + + self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) + response = self.client.post(reverse('api:forum:list-post', args=(topic.id,)), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + profile = ProfileFactory() + profile.can_write = False + profile.save() + + self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) + response = self.client.post(reverse('api:forum:list-post', args=(topic.id,)), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_new_post_no_text(self): + """ + Try to post a new post without a text. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + data = {} + response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id,)), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_post_empty_text(self): + """ + Try to post a new post with an empty text. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + data = { + 'text': '' + } + response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id,)), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_new_post_unknown_topic(self): + """ + Try to post a new post in a topic that does not exists. + """ + data = { + 'text': 'Where should I go now ?' + } + response = self.client_authenticated.post(reverse('api:forum:list-post', args=(666,)), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +# Edite un sujet sans changement +# Édite un sujet qvec user en ls +# Édite un sujet avec user banni +# Édite un sujet en vidant le titre +# Édite un sujet en le passant en resolu +# Editer dans un forum privé ? Verifier les auths +# TODO + + def test_update_topic_details_title(self): + """ + Updates title of a topic. + """ + data = { + 'title': 'Mon nouveau titre' + } + topic = self.create_multiple_forums_with_topics(1, 1, self.profile) + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('title'), data.get('title')) + + def test_update_topic_details_subtitle(self): + """ + Updates subtitle of a topic. + """ + data = { + 'subtitle': 'Mon nouveau sous-titre' + } + topic = self.create_multiple_forums_with_topics(1, 1, self.profile) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('subtitle'), data.get('subtitle')) + + def test_update_topic_anonymous(self): + """ + Tries to update a Topic with an anonymous user. + """ + data = { + 'title': 'Mon nouveau titre' + } + topic = self.create_multiple_forums_with_topics(1, 1) + self.client = APIClient() + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_update_topic_staff(self): + """ + Updates title of a topic with a staff member. + """ + data = { + 'title': 'Mon nouveau titre' + } + topic = self.create_multiple_forums_with_topics(1, 1, self.profile) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('title'), data.get('title')) + + def test_update_topic_other_user(self): + """ + Tries to update title of a topic posted by another user. + """ + data = { + 'title': 'Mon nouveau titre' + } + profile = ProfileFactory() + topic = self.create_multiple_forums_with_topics(1, 1, profile) + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_unknown_topic(self): + """ + Tries to update title of a non existing topic. + """ + data = { + 'title': 'Mon nouveau titre' + } + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[666]), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_topic_forum_user(self): + """ + Tries to move (change forum in which the topic is) with an user. + """ + data = { + 'forum': 5 + } + self.create_multiple_forums_with_topics(5, 1, self.profile) + topic = Topic.objects.filter(forum=1).first() + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_topic_forum_staff(self): + """ + Tries to move (change forum in which the topic is) with a staff member. + """ + data = { + 'forum': 5 + } + self.create_multiple_forums_with_topics(5, 1, self.profile) + topic = Topic.objects.filter(forum=1).first() + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + print(response) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('forum'), data.get('forum')) + + def test_list_of_posts_unknown(self): + """ + Tries to get a list of posts in an unknown topic + """ + response = self.client.get(reverse('api:forum:list-post', args=[666])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_list_of_posts(self): + """ + Gets list of posts in a topic. + """ + topic = self.create_topic_with_post() + + response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_posts_private_forum(self): + """ + Get a list of posts in a topic of a private forum. + """ + group = Group.objects.create(name="DummyGroup_1") + + profile = ProfileFactory() + group.user_set.add(profile.user) + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, profile) + # def add_topic_in_a_forum(forum, profile, is_sticky=False, is_solved=False, is_locked=False): + + response = profile.client.get(reverse('api:forum:list-post', args=[topic.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 1) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_posts_private_forum_user(self): + """ + Tries to get a list of posts in a topic of a private forum with a normal user. + """ + group = Group.objects.create(name="staff") + + profile = ProfileFactory() + group.user_set.add(profile.user) + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, profile) + + response = self.client_authenticated.get(reverse('api:forum:list-post', args=[topic.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 1) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_posts_with_several_pages(self): + """ + Gets list of posts with several pages in the database. + """ + topic = self.create_topic_with_post(REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 2) # Note : when creating a Topic a first post is created, explaining the +1 + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + + response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 2) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), 2) + + def test_list_of_posts_for_a_page_given(self): + """ + Gets list of posts with several pages and gets a page different that the first one. + """ + topic = self.create_topic_with_post(REST_PAGE_SIZE + 1) + + response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 12) + self.assertEqual(len(response.data.get('results')), 2) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + + def test_list_of_posts_for_a_wrong_page_given(self): + """ + Gets an error when the posts asks a wrong page. + """ + topic = self.create_topic_with_post(1) + response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page=2') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_of_posts_with_a_custom_page_size(self): + """ + Gets list of forums with a custom page size. DRF allows to specify a custom + size for the pagination. + """ + topic = self.create_topic_with_post(REST_PAGE_SIZE * 2) + print (topic) + + page_size = 'page_size' + response = self.client.get(reverse('api:forum:list-post') + '?{}=20'.format(page_size), args=[topic.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 20) + self.assertEqual(len(response.data.get('results')), 20) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_PAGE_SIZE_QUERY_PARAM, page_size) + + def test_list_of_posts_in_topic_with_a_wrong_custom_page_size(self): + """ + Gets list of posts with a custom page size but not good according to the + value in settings. + """ + page_size_value = REST_MAX_PAGE_SIZE + 1 + topic = self.create_topic_with_post(page_size_value) + + response = self.client.get(reverse('api:forum:list-post') + '?page_size={}'.format(page_size_value), args=[topic.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), page_size_value) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) + + def test_list_of_posts_in_unknown_topic(self): + """ + Tries to list the posts of an non existing Topic. + """ + topic = self.create_topic_with_post() + + response = self.client.get(reverse('api:forum:list-post'), args=[topic.id]) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_of_user_topics_empty(self): + """ + Gets empty list of topic that the user created. + """ + response = self.client_authenticated.get(reverse('api:forum:list-usertopic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_user_topics(self): + """ + Gets list of user's topics not empty in the database. + """ + self.create_multiple_forums_with_topics(10, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-usertopic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 10) + self.assertEqual(len(response.data.get('results')), 10) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_user_topics_with_several_pages(self): + """ + Gets list of user's topics with several pages in the database. + """ + self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-usertopic')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + + response = self.client_authenticated.get(reverse('api:forum:list-usertopic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), 1) + + def test_list_of_user_topics_for_a_page_given(self): + """ + Gets list of user's topics with several pages and gets a page different that the first one. + """ + self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-usertopic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 11) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + + def test_list_of_users_topics_for_a_wrong_page_given(self): + """ + Gets an error when the user's topics asks a wrong page. + """ + response = self.client_authenticated.get(reverse('api:forum:list-usertopic') + '?page=2') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_of_user_topics_with_a_custom_page_size(self): + """ + Gets list of user's topics with a custom page size. DRF allows to specify a custom + size for the pagination. + """ + self.create_multiple_forums_with_topics(REST_PAGE_SIZE * 2, 1, self.profile) + + page_size = 'page_size' + response = self.client.get(reverse('api:forum:list-usertopic') + '?{}=20'.format(page_size)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 20) + self.assertEqual(len(response.data.get('results')), 20) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_PAGE_SIZE_QUERY_PARAM, page_size) + + def test_list_of_user_topics_with_a_wrong_custom_page_size(self): + """ + Gets list of user's topic with a custom page size but not good according to the + value in settings. + """ + page_size_value = REST_MAX_PAGE_SIZE + 1 + self.create_multiple_forums_with_topics(page_size_value, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-usertopic') + '?page_size={}'.format(page_size_value)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), page_size_value) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) + + def test_list_of_user_topics_anonymous(self): + """ + Tries to get a list of users topic with an anonymous user. + """ + + response = self.client.get(reverse('api:forum:list-usertopic')) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +# DONE Créer un message 200 +# DONE Créer un message avec un contenu vide +# DONECréer un message dans un sujet qui n'existe pas +# DONE Créer un message en anonymous +# Créer un message dans un sujet qui en contient deja +# DONE Créer un message dans un forum privé en user +# DONE Créer un message dans un forum privé en staff +# Créer un message dans un sujet fermé en user +# Créer un message dans un sujet fermé en staff +# Créer un message pour tester l'antiflood + + def test_create_post_with_no_field(self): + """ + Creates a post in a topic but not with according field. + """ + response = self.client.post(reverse('api:forum:list-post', args=[self.private_topic.id]), {}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_post_with_empty_field(self): + """ + Creates a post in a topic but with no text. + """ + data = { + 'text': '' + } + response = self.client.post(reverse('api:forum:list-post', args=[self.private_topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_post_unauthenticated(self): + """ + Creates a post in a topic with unauthenticated client. + """ + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-post', args=[0]), {}) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_create_post_with_bad_topic_id(self): + """ + Creates a post in a topic with a bad topic id. + """ + data = { + 'text': 'Welcome to this post!' + } + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[666]), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_create_post(self): + """ + Creates a post in a topic. + """ + data = { + 'text': 'Welcome to this post!' + } + topic = create_multiple_forums_with_topics(1, 1) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + post = Post.objects.filter(topic=topic.id) + self.assertEqual(response.data.get('text'), data.get('text')) + self.assertEqual(response.data.get('text'), post.text) + self.assertEqual(response.data.get('is_userful'), post.is_userful) + self.assertEqual(response.data.get('author'), post.author) + self.assertEqual(response.data.get('position'), post.position) + self.assertEqual(response.data.get('pubdate'), post.pubdate) + + def test_failure_post_in_a_forum_we_cannot_read(self): + """ + Tries to create a post in a private topic with a normal user. + """ + group = Group.objects.create(name="staff") + + profile = StaffProfileFactory() + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, profile) + data = { + 'text': 'Welcome to this post!' + } + response = self.client.post(reverse('api:forum:list-post', args=(topic.pk,)), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_post_in_a_private_forum(self): + """ + Post in a private topic with a user that has access right. + """ + group = Group.objects.create(name="staff") + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, profile) + data = { + 'text': 'Welcome to this post!' + } + response = self.client_authentificated.post(reverse('api:forum:list-post', args=(topic.pk,)), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + post = Post.objects.filter(topic=topic.id) + self.assertEqual(response.data.get('text'), data.get('text')) + self.assertEqual(response.data.get('text'), post.text) + self.assertEqual(response.data.get('is_userful'), post.is_userful) + self.assertEqual(response.data.get('author'), post.author) + self.assertEqual(response.data.get('position'), post.position) + self.assertEqual(response.data.get('pubdate'), post.pubdate) + + def test_detail_post(self): + """ + Gets all information about a post. + """ + + topic = self.create_topic_with_post() + post = Post.objects.filter(topic=topic.id).first() + response = self.client.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(post.id, response.data.get('id')) + self.assertIsNotNone(response.data.get('text')) + self.assertIsNone(response.data.get('text_html')) + self.assertIsNotNone(response.data.get('pubdate')) + self.assertIsNone(response.data.get('update')) + self.assertEqual(post.position_in_topic, response.data.get('position_in_topic')) + self.assertEqual(topic.author, response.data.get('author')) + + def test_detail_of_a_private_post_not_present(self): + """ + Gets an error 404 when the post isn't present in the database. + """ + topic = self.create_multiple_forums_with_topics(1, 1) + response = self.client.get(reverse('api:forum:detail-post', args=[topic.id, 666])) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_detail_of_private_post(self): + """ + Tries to get all the data about a post in a private topic (and forum) with different users. + """ + group = Group.objects.create(name="staff") + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, self.profile) + post = Post.objects.filter(topic=topic.id).first() + + # Anonymous + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # User + response = self.client_authentificated.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Staff user + response = self.client_authentificated_staff.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +# TODO +# Lister les message d'un membre staff en étant staff +# Lister les message d'un membre staff en étant anonyme +# Lister les messages d'un membre staff en étant user + + def test_list_of_member_posts_empty(self): + """ + Gets empty list of posts that that a specified member created. + """ + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('results'), []) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_member_posts(self): + """ + Gets list of a member posts not empty in the database. + """ + self.create_multiple_forums_with_topics(10, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 10) + self.assertEqual(len(response.data.get('results')), 10) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + + def test_list_of_member_posts_with_several_pages(self): + """ + Gets list of a member topics with several pages in the database. + """ + self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + self.assertEqual(len(response.data.get('results')), 1) + + def test_list_of_member_posts_for_a_page_given(self): + """ + Gets list of a member topics with several pages and gets a page different that the first one. + """ + + self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost')+ '?page=2', args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 11) + self.assertEqual(len(response.data.get('results')), 1) + self.assertIsNone(response.data.get('next')) + self.assertIsNotNone(response.data.get('previous')) + + def test_list_of_member_post_for_a_wrong_page_given(self): + """ + Gets an error when the member posts asks a wrong page. + """ + response = self.client_authenticated.get(reverse('api:forum:list-memberpost') + '?page=2', args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_of_member_posts_with_a_custom_page_size(self): + """ + Gets list of user's topics with a custom page size. DRF allows to specify a custom + size for the pagination. + """ + self.create_topic_with_post(REST_PAGE_SIZE * 2, 1, self.profile) + + page_size = 'page_size' + response = self.client.get(reverse('api:forum:list-memberpost') + '?{}=20'.format(page_size), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 20) + self.assertEqual(len(response.data.get('results')), 20) + self.assertIsNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_PAGE_SIZE_QUERY_PARAM, page_size) + + def test_list_of_member_posts_with_a_wrong_custom_page_size(self): + """ + Gets list of member posts with a custom page size but not good according to the + value in settings. + """ + page_size_value = REST_MAX_PAGE_SIZE + 1 + self.create_multiple_forums_with_topics(page_size_value, 1, self.profile) + + response = self.client_authenticated.get(reverse('api:forum:list-memberpost') + '?page_size={}'.format(page_size_value), args=[self.profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), page_size_value) + self.assertIsNotNone(response.data.get('next')) + self.assertIsNone(response.data.get('previous')) + self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) + + def test_list_of_unknow_member_posts(self): + """ + Gets empty list of posts for a member that does not exists. + """ + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[666]) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_alert_post(self): + """ + Tries to alert a post in a public forum with different type of users + """ + profile = ProfileFactory() + category, forum = create_category() + topic = add_topic_in_a_forum(forum, profile) + another_profile = ProfileFactory() + post = PostFactory(topic=topic, author=another_profile.user, position=1) + data = { + 'text': 'There is a guy flooding about Flask, con you do something about it ?', + } + + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authenticated.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + # VERIFIER EN BDD TODO + + def test_alert_post_in_private_forum(self): + """ + Tries to alert a post in a public forum with different type of users + """ + profile = StaffProfileFactory() + group = Group.objects.create(name="staff") + category, forum = create_category() + topic = add_topic_in_a_forum(forum, profile) + post = PostFactory(topic=topic, author=profile.user, position=1) + data = { + 'text': 'There is a guy flooding about Flask, con you do something about it ?', + } + + self.client = APIClient() + response = self.client.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authenticated.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client_authenticated_staff.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + # VERIFIER EN BDD TODO + + def test_alert_post_not_found(self): + """ + Tries to alert a post in a public forum with different type of users + """ + profile = ProfileFactory() + category, forum = create_category() + topic = add_topic_in_a_forum(forum, profile) + post = PostFactory(topic=topic, author=profile.user, position=1) + data = { + 'text': 'There is a guy flooding about Flask, con you do something about it ?', + } + + response = self.client_authentificated.post(reverse('api:forum:list-topic', args=[666, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + response = self.client_authentificated.post(reverse('api:forum:list-topic', args=[topic.id, 666]), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def create_oauth2_client(user): client = Application.objects.create(user=user, diff --git a/zds/forum/api/urls.py b/zds/forum/api/urls.py index fd631f75b8..a30fdb4665 100644 --- a/zds/forum/api/urls.py +++ b/zds/forum/api/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url -from .views import PostKarmaView, ForumListAPI, ForumDetailAPI, PostListAPI, TopicListAPI, TopicDetailAPI, UserTopicListAPI, MemberPostListAPI, UserPostListAPI, PostDetailAPI +from .views import PostKarmaView, ForumListAPI, ForumDetailAPI, PostListAPI, TopicListAPI, TopicDetailAPI, UserTopicListAPI, MemberPostListAPI, UserPostListAPI, PostDetailAPI, PostAlertAPI urlpatterns = [ url(r'^$', ForumListAPI.as_view(), name='list'), url(r'^(?P[0-9]+)/?$', ForumDetailAPI.as_view(), name='detail'), @@ -11,5 +11,6 @@ url(r'^sujets/(?P[0-9]+)/?$', TopicDetailAPI.as_view(), name='detail-topic'), url(r'^membres/(?P[0-9]+)/messages/?$', MemberPostListAPI.as_view(), name='list-memberpost'), url(r'^membres/messages/?$', UserPostListAPI.as_view(), name='list-userpost'), - url(r'^sujets/(?P[0-9]+)/messages/(?P[0-9]+)/?$', PostDetailAPI.as_view(), name='detail-post') + url(r'^sujets/(?P[0-9]+)/messages/(?P[0-9]+)/?$', PostDetailAPI.as_view(), name='detail-post'), + url(r'^sujets/(?P[0-9]+)/messages/(?P[0-9]+)/alert/?$', PostAlertAPI.as_view(), name='alert-post') ] diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index e7742222c3..4040a9a4d7 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -1,13 +1,13 @@ # coding: utf-8 -from zds.member.api.permissions import CanReadTopic, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly +from zds.member.api.permissions import CanReadTopic, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser from zds.utils.api.views import KarmaView from zds.forum.models import Post, Forum, Topic import datetime from django.core.cache import cache from django.db.models.signals import post_save, post_delete from rest_framework import filters -from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpdateAPIView +from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveAPIView, CreateAPIView from rest_framework_extensions.key_constructor.constructors import DefaultKeyConstructor from rest_framework_extensions.cache.decorators import cache_response from rest_framework_extensions.etag.decorators import etag @@ -18,7 +18,7 @@ from dry_rest_permissions.generics import DRYPermissions from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit from zds.utils import slugify -from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, PostSerializer, PostActionSerializer, ForumActionSerializer, ForumUpdateSerializer +from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, TopicUpdateSerializer, PostSerializer, PostActionSerializer, PostUpdateSerializer, AlertSerializer from zds.forum.api.permissions import IsStaffUser @@ -58,6 +58,7 @@ class ForumListAPI(ListCreateAPIView): """ queryset = Forum.objects.all() + serializer_class = ForumSerializer list_key_func = PagingSearchListKeyConstructor() @etag(list_key_func) @@ -82,54 +83,11 @@ def get(self, request, *args, **kwargs): """ return self.list(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - """ - Creates a new forum. - --- - - parameters: - - name: Authorization - description: Bearer token to make an authenticated request. - required: true - paramType: header - - name: text - description: Content of the post in markdown. - required: true - paramType: form - responseMessages: - - code: 400 - message: Bad Request - - code: 401 - message: Not Authenticated - - code: 403 - message: Permission Denied - """ - - forum_slug = slugify(request.data.get('title')) - serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) - serializer.is_valid(raise_exception=True) - serializer.save(slug=forum_slug) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - - def get_serializer_class(self): - if self.request.method == 'GET': - return ForumSerializer - elif self.request.method == 'POST': - return ForumActionSerializer - - def get_permissions(self): - permission_classes = [AllowAny, ] - if self.request.method == 'POST': - permission_classes.append(DRYPermissions) - permission_classes.append(IsStaffUser) - return [permission() for permission in permission_classes] - -class ForumDetailAPI(RetrieveUpdateAPIView): +class ForumDetailAPI(RetrieveAPIView): """ - Profile resource to display or update details of a forum. + Profile resource to display details of a forum. + --- """ queryset = Forum.objects.all() @@ -147,39 +105,11 @@ def get(self, request, *args, **kwargs): message: Not Found """ forum = self.get_object() - serializer = self.get_serializer(forum) return self.retrieve(request, *args, **kwargs) - def put(self, request, *args, **kwargs): - """ - Updates a forum, must be admin to perform this action. - --- - - parameters: - - name: Authorization - description: Bearer token to make an authenticated request. - required: true - paramType: header - responseMessages: - - code: 400 - message: Bad Request - - code: 401 - message: Not Authenticated - - code: 403 - message: Permission Denied - - code: 404 - message: Not Found - """ - # TODO doc incppmplete - return self.update(request, *args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return ForumSerializer - elif self.request.method == 'PUT': - return ForumUpdateSerializer + return ForumSerializer class TopicListAPI(ListCreateAPIView): @@ -187,7 +117,6 @@ class TopicListAPI(ListCreateAPIView): Profile resource to list all topic """ queryset = Topic.objects.all() - serializer_class = TopicSerializer filter_backends = (filters.DjangoFilterBackend,) filter_fields = ('forum', 'author', 'tags__title') list_key_func = PagingSearchListKeyConstructor() @@ -225,8 +154,20 @@ def post(self, request, *args, **kwargs): description: Bearer token to make an authenticated request. required: true paramType: header + - name: title + description: Title of the Topic. + required: true + paramType: form + - name: subtitle + description: Subtitle of the Topic. + required: false + paramType: form + - name: forum + description: Identifier of the forum in which the Topic should be posted. + required: false + paramType: form - name: text - description: Content of the post in markdown. + description: Content of the first post in markdown. required: true paramType: form responseMessages: @@ -236,11 +177,10 @@ def post(self, request, *args, **kwargs): message: Not Authenticated """ - author = request.user - + author = request.user.id serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) - topic = serializer.save(author_id=3) + serializer.save(author_id=author) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) @@ -250,7 +190,6 @@ def get_serializer_class(self): elif self.request.method == 'POST': return TopicActionSerializer - def get_permissions(self): permission_classes = [AllowAny, ] if self.request.method == 'POST': @@ -259,7 +198,6 @@ def get_permissions(self): return [permission() for permission in permission_classes] - class UserTopicListAPI(ListAPIView): """ Profile resource to list all topic from current user @@ -270,7 +208,6 @@ class UserTopicListAPI(ListAPIView): filter_fields = ('forum', 'tags__title') list_key_func = PagingSearchListKeyConstructor() - @etag(list_key_func) @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): @@ -279,6 +216,10 @@ def get(self, request, *args, **kwargs): --- parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header - name: page description: Restricts output to the given page number. required: false @@ -291,7 +232,6 @@ def get(self, request, *args, **kwargs): - code: 404 message: Not Found """ - # TODO code d'auth manquant en commentaire return self.list(request, *args, **kwargs) def get_queryset(self): @@ -305,7 +245,6 @@ class TopicDetailAPI(RetrieveUpdateAPIView): """ queryset = Topic.objects.all() obj_key_func = DetailKeyConstructor() - serializer_class = TopicSerializer @etag(obj_key_func) @cache_response(key_func=obj_key_func) @@ -318,10 +257,49 @@ def get(self, request, *args, **kwargs): message: Not Found """ topic = self.get_object() - #serializer = self.get_serializer(topic) return self.retrieve(request, *args, **kwargs) + def put(self, request, *args, **kwargs): + """ + Updates a topic. Said post must be owned by the authenticated member. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + # TODO doc manquante + responseMessages: + - code: 400 + message: Bad Request if you specify a bad identifier + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied + - code: 404 + message: Not Found + """ + return self.update(request, *args, **kwargs) + + def get_serializer_class(self): + if self.request.method == 'GET': + return TopicSerializer + elif self.request.method == 'PUT': + return TopicUpdateSerializer + + def get_permissions(self): + permission_classes = [] + if self.request.method == 'GET': + permission_classes.append(DRYPermissions) + elif self.request.method == 'PUT': + permission_classes.append(DRYPermissions) + permission_classes.append(IsAuthenticatedOrReadOnly) + permission_classes.append(IsOwnerOrReadOnly) + permission_classes.append(CanReadTopic) + return [permission() for permission in permission_classes] + class PostListAPI(ListCreateAPIView): """ @@ -366,20 +344,19 @@ def post(self, request, *args, **kwargs): description: Content of the post in markdown. required: true paramType: form + # TODO doc manquante responseMessages: - code: 400 message: Bad Request - code: 401 message: Not Authenticated """ - - #TODO GERE les droits et l'authentification --> en cours : tester avec connection pour voir si cela fonctionne - #TODO passer les arguments corrects a save - author = request.user + author = request.user.id + topic = self.kwargs.get('pk') serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) - topic = serializer.save(position=2, author_id=3, topic_id=1) + serializer.save(position=0, author_id=author, topic_id=topic) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) @@ -479,12 +456,11 @@ def get_queryset(self): class PostDetailAPI(RetrieveUpdateAPIView): """ - Profile resource to display details of given message + Profile resource to display details of given post """ queryset = Post.objects.all() obj_key_func = DetailKeyConstructor() - serializer_class = PostSerializer @etag(obj_key_func) @cache_response(key_func=obj_key_func) @@ -500,5 +476,79 @@ def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) + def put(self, request, *args, **kwargs): + """ + Updates a post. Said post must be owned by the authenticated member. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + # TODO doc manquante + responseMessages: + - code: 400 + message: Bad Request if you specify a bad identifier + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied + - code: 404 + message: Not Found + """ + return self.update(request, *args, **kwargs) + + def get_serializer_class(self): + if self.request.method == 'GET': + return PostSerializer + elif self.request.method == 'PUT': + return PostUpdateSerializer + + +class PostAlertAPI(CreateAPIView): + """ + Alert a topic post to the staff. + """ + + serializer_class = AlertSerializer + + def post(self, request, *args, **kwargs): + """ + Alert a topic post to the staff. + --- + + parameters: + - name: Authorization + description: Bearer token to make an authenticated request. + required: true + paramType: header + - name: text + description: Content of the post in markdown. + required: true + paramType: form + # TODO doc manquante + responseMessages: + - code: 400 + message: Bad Request + - code: 401 + message: Not Authenticated + """ + author = request.user.id + post = self.kwargs.get('pk') + + serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) + serializer.is_valid(raise_exception=True) + serializer.save(position=0, author=author, comment=post) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def get_permissions(self): + permission_classes = [AllowAny, ] + permission_classes.append(IsAuthenticated) + + # TODO global identier quand masquer les messages -# TOD gerer l'antispam +# TODO gerer l'antispam +# TODO alerter un post A tester +# TODO editer un post A tester diff --git a/zds/forum/models.py b/zds/forum/models.py index 000c026488..b0d50c0e4e 100644 --- a/zds/forum/models.py +++ b/zds/forum/models.py @@ -426,7 +426,16 @@ def get_absolute_url(self): self.topic.get_absolute_url(), page, self.pk) - + + def is_author(self, user): + """ + Check if the user given is the author of the message. + + :param user: Potential author of the message. + :return: true if the user is the author. + """ + return self.author == user + @staticmethod def has_write_permission(request): return request.user.is_authenticated() and request.user.profile.can_write_now() From 9fb78c1f5e3b6e671d0932eed6078e37656ce6ca Mon Sep 17 00:00:00 2001 From: Antonin Date: Mon, 19 Dec 2016 08:15:28 +0000 Subject: [PATCH 04/78] Ajout de test, retrait de todo --- zds/forum/api/tests.py | 65 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index de51d09f69..d4e95cedd2 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -206,8 +206,6 @@ def test_get_post_voters(self): self.assertEqual(1, response.data['like']['count']) self.assertEqual(1, response.data['dislike']['count']) -# Liste les forums avec un staff (on boit les forums privé) - class ForumAPITest(APITestCase): def setUp(self): @@ -274,6 +272,24 @@ def test_list_of_forums(self): self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) + + def test_list_of_forums_private(self): + """ + Gets list of forums not empty in the database. + """ + group = Group.objects.create(name="staff") + category, forum = create_category(group) + + self.client = APIClient() + response = self.client.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authentificated.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_UNAUTHORIZED) + + response = self.client_authentificated_staff.get(reverse('api:forum:list')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_list_of_forums_with_several_pages(self): """ @@ -361,6 +377,25 @@ def test_details_forum(self): self.assertEqual(response.data.get('category'), forum.category) self.assertEqual(response.data.get('position_in_category'), forum.position_in_category) self.assertEqual(response.data.get('group'), forum.group) + + + def test_details_forum_private(self): + """ + Tries to get the details of a private forum with different users. + """ + + group = Group.objects.create(name="staff") + category, forum = create_category(group) + + self.client = APIClient() + response = self.client.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authentificated.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authentificated_staff.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_details_unknown_forum(self): """ @@ -1296,12 +1331,6 @@ def test_detail_of_private_post(self): response = self.client_authentificated_staff.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) - -# TODO -# Lister les message d'un membre staff en étant staff -# Lister les message d'un membre staff en étant anonyme -# Lister les messages d'un membre staff en étant user - def test_list_of_member_posts_empty(self): """ Gets empty list of posts that that a specified member created. @@ -1325,6 +1354,26 @@ def test_list_of_member_posts(self): self.assertEqual(len(response.data.get('results')), 10) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) + + def test_list_of_staff_posts(self): + """ + Gets list of a staff posts. + """ + group = Group.objects.create(name="staff") + + profile = StaffProfileFactory() + category, forum = create_category(group) + topic = add_topic_in_a_forum(forum, profile) + + self.client = APIClient() + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[profile.id]) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[profile.id]) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client_authenticated_staff.get(reverse('api:forum:list-memberpost'), args=[profile.id]) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_list_of_member_posts_with_several_pages(self): """ From 2ff88ac919117e04770c5fdd88cd5685b0d19408 Mon Sep 17 00:00:00 2001 From: Antonin Date: Fri, 23 Dec 2016 08:35:38 +0000 Subject: [PATCH 05/78] Testing --- zds/forum/api/tests.py | 160 ++++++++++++++++++++++++++-------- zds/forum/api/views.py | 8 +- zds/member/api/permissions.py | 11 ++- zds/settings.py | 1 + 4 files changed, 141 insertions(+), 39 deletions(-) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index d4e95cedd2..cc41a5197a 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -14,7 +14,7 @@ from zds.forum.models import Forum, Topic, Post from zds.forum.factories import PostFactory from zds.forum.tests.tests_views import create_category, add_topic_in_a_forum -from zds.utils.models import CommentVote +from zds.utils.models import CommentVote, Alert class ForumPostKarmaAPITest(APITestCase): @@ -220,8 +220,13 @@ def setUp(self): client_oauth2 = create_oauth2_client(self.staff.user) self.client_authenticated_staff = APIClient() authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') + + self.group_staff = Group.objects.create(name="staff") caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() + + def tearDown(self): + self.group_staff.delete() def create_multiple_forums(self, number_of_forum=REST_PAGE_SIZE): for forum in xrange(0, number_of_forum): @@ -243,11 +248,12 @@ def create_topic_with_post(self, number_of_post=REST_PAGE_SIZE, profile=None): category, forum = create_category() new_topic = add_topic_in_a_forum(forum, profile) + posts=[] for post in xrange(0, number_of_post): - PostFactory(topic=new_topic.id, author=profile.user, position=2) + posts.append(PostFactory(topic=new_topic.id, author=profile.user, position=2)) - return new_topic + return new_topic, posts def test_list_of_forums_empty(self): """ @@ -277,8 +283,7 @@ def test_list_of_forums_private(self): """ Gets list of forums not empty in the database. """ - group = Group.objects.create(name="staff") - category, forum = create_category(group) + category, forum = create_category(self.group_staff) self.client = APIClient() response = self.client.get(reverse('api:forum:list')) @@ -383,9 +388,7 @@ def test_details_forum_private(self): """ Tries to get the details of a private forum with different users. """ - - group = Group.objects.create(name="staff") - category, forum = create_category(group) + category, forum = create_category(self.group_staff) self.client = APIClient() response = self.client.get(reverse('api:forum:detail', args=[forum.id])) @@ -410,8 +413,7 @@ def test_details_private_forum_user(self): """ Tries to get the details of a private forum with a normal user, staff user and anonymous one. """ - group = Group.objects.create(name="staff") - category, forum = create_category(group) + category, forum = create_category(self.group_staff) self.client = APIClient() response = self.client.get(reverse('api:forum:detail', args=[forum.id])) @@ -604,12 +606,9 @@ def test_new_topic_private_forum(self): """ Post a new topic in a private forum (staff only) with an anonymous user, normal user and staff user. """ - - group = Group.objects.create(name="staff") - profile = ProfileFactory() - group.user_set.add(profile.user) - category, forum = create_category(group) + self.group_staff.user_set.add(profile.user) + category, forum = create_category(self.group_staff) data = { 'title': 'Have you seen the guy flooding ?', 'subtitle': 'He is asking to many question about flask.', @@ -733,9 +732,7 @@ def test_details_topic_private(self): """ Tries to get details of a topic that is in a private forum. """ - - group = Group.objects.create(name="staff") - category, forum = create_category(group) + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, self.staff) # Anonymous @@ -983,11 +980,9 @@ def test_list_of_posts_private_forum_user(self): """ Tries to get a list of posts in a topic of a private forum with a normal user. """ - group = Group.objects.create(name="staff") - profile = ProfileFactory() - group.user_set.add(profile.user) - category, forum = create_category(group) + self.group_staff.user_set.add(profile.user) + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) response = self.client_authenticated.get(reverse('api:forum:list-post', args=[topic.id])) @@ -1252,10 +1247,8 @@ def test_failure_post_in_a_forum_we_cannot_read(self): """ Tries to create a post in a private topic with a normal user. """ - group = Group.objects.create(name="staff") - profile = StaffProfileFactory() - category, forum = create_category(group) + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) data = { 'text': 'Welcome to this post!' @@ -1267,8 +1260,8 @@ def test_post_in_a_private_forum(self): """ Post in a private topic with a user that has access right. """ - group = Group.objects.create(name="staff") - category, forum = create_category(group) + + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) data = { 'text': 'Welcome to this post!' @@ -1313,8 +1306,7 @@ def test_detail_of_private_post(self): """ Tries to get all the data about a post in a private topic (and forum) with different users. """ - group = Group.objects.create(name="staff") - category, forum = create_category(group) + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, self.profile) post = Post.objects.filter(topic=topic.id).first() @@ -1359,10 +1351,9 @@ def test_list_of_staff_posts(self): """ Gets list of a staff posts. """ - group = Group.objects.create(name="staff") profile = StaffProfileFactory() - category, forum = create_category(group) + category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) self.client = APIClient() @@ -1472,14 +1463,18 @@ def test_alert_post(self): response = self.client_authenticated.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # VERIFIER EN BDD TODO + + alerte = Alert.objects.latest('pubdate') + self.assertEqual(alerte.text, data.get('text')) + self.assertEqual(alerte.author, self.client_authenticated) + self.assertEqual(alerte.comment, post.id) + def test_alert_post_in_private_forum(self): """ Tries to alert a post in a public forum with different type of users """ profile = StaffProfileFactory() - group = Group.objects.create(name="staff") category, forum = create_category() topic = add_topic_in_a_forum(forum, profile) post = PostFactory(topic=topic, author=profile.user, position=1) @@ -1496,7 +1491,11 @@ def test_alert_post_in_private_forum(self): response = self.client_authenticated_staff.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # VERIFIER EN BDD TODO + + alerte = Alert.objects.latest('pubdate') + self.assertEqual(alerte.text, data.get('text')) + self.assertEqual(alerte.author, self.client_authenticated_staff) + self.assertEqual(alerte.comment, post.id) def test_alert_post_not_found(self): """ @@ -1515,6 +1514,93 @@ def test_alert_post_not_found(self): response = self.client_authentificated.post(reverse('api:forum:list-topic', args=[topic.id, 666]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +# Edite un message en anonymous DONE +# Edite un message avec le bon user DONE +# Edite un message avec un autre user DONE +# Edite un message avec un staff DONE +# Edite un message d'un sujet fermé user +# Edite un message d'un sujet fermé staff +# Edite un message dans un forum privé user +# Edite un message dans un forum privé anonymous DONE +# Edite un message dans un forum privé staff +# Edite un message un message qui n'exite pas DONE +# TODO + + + def test_update_post_anonymous(self): + """ + Tries to update a post with anonymous user. + """ + data = { + 'text': 'I made an error I want to edit.' + } + self.client = APIClient(); + topic, posts = self.create_topic_with_post() + response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_update_post_other_user(self): + """ + Tries to update a post with another user that the one who posted on the first time. + """ + data = { + 'text': 'I made an error I want to edit.' + } + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, self.profile) + response = self.client_authentificated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(data.get('text'), response.data.get('text')) + + def test_update_post(self): + """ + Updates a post with user. + """ + data = { + 'text': 'I made an error I want to edit.' + } + topic, posts = self.create_topic_with_post(REST) + response = self.client_authentificated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_post_staff(self): + """ + Update a post with a staff user. + """ + data = { + 'text': 'I am Vladimir Lupin, I do want I want.' + } + topic, posts = self.create_topic_with_post() + response = self.client_authentificated_staff.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(reponse.data.get('text'), data.get('text')) + + def test_update_unknow_post(self): + """ + Tries to update post that does not exist. + """ + data = { + 'text': 'I am Vladimir Lupin, I do want I want.' + } + response = self.client_authentificated_staff.put(reverse('api:forum:detail-post', args=[666, 42]), data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_post_in_private_topic_anonymous(self): + """ + Tries to update a post in a private forum (and topic) with anonymous user. + """ + data = { + 'text': 'I made an error I want to edit.' + } + self.client = APIClient(); + category, forum = create_category(self.group_staff) + topic = add_topic_in_a_forum(forum, self.staff) + post = PostFactory(topic=topic, author=self.staff, position=1) + + response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def create_oauth2_client(user): @@ -1535,3 +1621,9 @@ def authenticate_client(client, client_auth, username, password): }) access_token = AccessToken.objects.get(user__username=username) client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + +# TODO +# Reorganiser le code de test en differentes classes +# Renommer les serializer +# Voir ou on a besoin de read only +# Voir ou a besoin de validator \ No newline at end of file diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 4040a9a4d7..78359fbc54 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -1,6 +1,6 @@ # coding: utf-8 -from zds.member.api.permissions import CanReadTopic, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser +from zds.member.api.permissions import CanReadTopic, CanReadPost, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser from zds.utils.api.views import KarmaView from zds.forum.models import Post, Forum, Topic import datetime @@ -24,7 +24,7 @@ class PostKarmaView(KarmaView): queryset = Post.objects.all() - permission_classes = (IsAuthenticatedOrReadOnly, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, CanReadTopic) + permission_classes = (IsAuthenticatedOrReadOnly, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, CanReadPost) class PagingSearchListKeyConstructor(DefaultKeyConstructor): @@ -241,7 +241,7 @@ def get_queryset(self): class TopicDetailAPI(RetrieveUpdateAPIView): """ - Profile resource to display details of a given topic + Profile resource to display and updates details of a given topic """ queryset = Topic.objects.all() obj_key_func = DetailKeyConstructor() @@ -375,7 +375,7 @@ def get_current_user(self): return self.request.user.profile def get_permissions(self): - permission_classes = [AllowAny, ] + permission_classes = [AllowAny, CanReadPost] if self.request.method == 'POST': permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) diff --git a/zds/member/api/permissions.py b/zds/member/api/permissions.py index 56652da73d..311c74b5ef 100644 --- a/zds/member/api/permissions.py +++ b/zds/member/api/permissions.py @@ -86,4 +86,13 @@ class CanReadTopic(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): - return obj.topic.forum.can_read(request.user) + return obj.forum.can_read(request.user) + + +class CanReadPost(permissions.BasePermission): + """ + Checks if the user can read that post + """ + + def has_object_permission(self, request, view, obj): + return obj.topic.forum.can_read(request.user) \ No newline at end of file diff --git a/zds/settings.py b/zds/settings.py index 73b5bb720b..af63907337 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -31,6 +31,7 @@ }, } +ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. From 520c8bda2f922091fb1b1a9eb2a2d2bc4a52b399 Mon Sep 17 00:00:00 2001 From: Antonin Date: Sat, 24 Dec 2016 12:41:21 +0000 Subject: [PATCH 06/78] Tests --- zds/forum/api/tests.py | 155 +++++++++++++++++----------------- zds/forum/api/views.py | 36 ++++++-- zds/forum/models.py | 9 ++ zds/member/api/permissions.py | 10 ++- 4 files changed, 123 insertions(+), 87 deletions(-) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index cc41a5197a..192e0bf4dd 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -221,12 +221,9 @@ def setUp(self): self.client_authenticated_staff = APIClient() authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') - self.group_staff = Group.objects.create(name="staff") + self.group_staff = Group.objects.filter(name="staff").first() caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() - - def tearDown(self): - self.group_staff.delete() def create_multiple_forums(self, number_of_forum=REST_PAGE_SIZE): for forum in xrange(0, number_of_forum): @@ -251,7 +248,7 @@ def create_topic_with_post(self, number_of_post=REST_PAGE_SIZE, profile=None): posts=[] for post in xrange(0, number_of_post): - posts.append(PostFactory(topic=new_topic.id, author=profile.user, position=2)) + posts.append(PostFactory(topic=new_topic, author=profile.user, position=2)) return new_topic, posts @@ -289,10 +286,10 @@ def test_list_of_forums_private(self): response = self.client.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - response = self.client_authentificated.get(reverse('api:forum:list')) + response = self.client_authenticated.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_UNAUTHORIZED) - response = self.client_authentificated_staff.get(reverse('api:forum:list')) + response = self.client_authenticated_staff.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -379,7 +376,7 @@ def test_details_forum(self): self.assertEqual(response.data.get('title'), forum.title) self.assertEqual(response.data.get('subtitle'), forum.subtitle) self.assertEqual(response.data.get('slug'), forum.slug) - self.assertEqual(response.data.get('category'), forum.category) + self.assertEqual(response.data.get('category'), forum.category.id) self.assertEqual(response.data.get('position_in_category'), forum.position_in_category) self.assertEqual(response.data.get('group'), forum.group) @@ -394,10 +391,10 @@ def test_details_forum_private(self): response = self.client.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - response = self.client_authentificated.get(reverse('api:forum:detail', args=[forum.id])) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + response = self.client_authenticated.get(reverse('api:forum:detail', args=[forum.id])) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client_authentificated_staff.get(reverse('api:forum:detail', args=[forum.id])) + response = self.client_authenticated_staff.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_details_unknown_forum(self): @@ -419,10 +416,10 @@ def test_details_private_forum_user(self): response = self.client.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - response = self.client_authentificated.get(reverse('api:forum:detail', args=[forum.id])) + response = self.client_authenticated.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client_authentificated_staff.get(reverse('api:forum:detail', args=[forum.id])) + response = self.client_authenticated_staff.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) # TODO @@ -622,11 +619,11 @@ def test_new_topic_private_forum(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) # User - response = self.client_authentificated.post(reverse('api:forum:list-topic'), data) + response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # Staff - response = self.client_authentificated_staff.post(reverse('api:forum:list-topic'), data) + response = self.client_authenticated_staff.post(reverse('api:forum:list-topic'), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_new_topic_with_banned_user(self): @@ -725,7 +722,7 @@ def test_details_unknown_topic(self): """ self.client = APIClient() - response = self.client.get(reverse('api:forum:detail-topic', args=(666))) + response = self.client.get(reverse('api:forum:detail-topic', args=[666])) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_details_topic_private(self): @@ -737,15 +734,15 @@ def test_details_topic_private(self): # Anonymous self.client = APIClient() - response = self.client.get(reverse('api:forum:detail-topic', args=(topic.id))) + response = self.client.get(reverse('api:forum:detail-topic', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) # User - response = self.client_authenticated.get(reverse('api:forum:detail-topic', args=(topic.id))) + response = self.client_authenticated.get(reverse('api:forum:detail-topic', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # Staff - response = self.client_authenticated_staff.get(reverse('api:forum:detail-topic', args=(topic.id))) + response = self.client_authenticated_staff.get(reverse('api:forum:detail-topic', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_new_post_anonymous(self): @@ -758,7 +755,7 @@ def test_new_post_anonymous(self): } self.client = APIClient() - response = self.client.post(reverse('api:forum:list-post', args=(topic.id)), data) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_new_post_user(self): @@ -771,8 +768,8 @@ def test_new_post_user(self): } self.client = APIClient() - response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id)), data) - topic = Topic.objects.filter(id=topic.id) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) + topic = Topic.objects.filter(id=topic.id).first() print(topic) last_message = topic.get_last_post() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -792,7 +789,7 @@ def test_new_post_user_with_restrictions(self): } self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) - response = self.client.post(reverse('api:forum:list-post', args=(topic.id,)), data) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) profile = ProfileFactory() @@ -800,7 +797,7 @@ def test_new_post_user_with_restrictions(self): profile.save() self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) - response = self.client.post(reverse('api:forum:list-post', args=(topic.id,)), data) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_new_post_no_text(self): @@ -809,7 +806,7 @@ def test_new_post_no_text(self): """ topic = self.create_multiple_forums_with_topics(1, 1) data = {} - response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id,)), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id,]), data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_new_post_empty_text(self): @@ -820,7 +817,7 @@ def test_new_post_empty_text(self): data = { 'text': '' } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=(topic.id,)), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id,]), data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_new_post_unknown_topic(self): @@ -830,7 +827,7 @@ def test_new_post_unknown_topic(self): data = { 'text': 'Where should I go now ?' } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=(666,)), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[666,]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Edite un sujet sans changement @@ -898,7 +895,7 @@ def test_update_topic_other_user(self): } profile = ProfileFactory() topic = self.create_multiple_forums_with_topics(1, 1, profile) - response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_update_unknown_topic(self): @@ -920,7 +917,7 @@ def test_update_topic_forum_user(self): } self.create_multiple_forums_with_topics(5, 1, self.profile) topic = Topic.objects.filter(forum=1).first() - response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_update_topic_forum_staff(self): @@ -933,7 +930,6 @@ def test_update_topic_forum_staff(self): self.create_multiple_forums_with_topics(5, 1, self.profile) topic = Topic.objects.filter(forum=1).first() response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) - print(response) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('forum'), data.get('forum')) @@ -948,7 +944,7 @@ def test_list_of_posts(self): """ Gets list of posts in a topic. """ - topic = self.create_topic_with_post() + topic, posts = self.create_topic_with_post() response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -969,7 +965,7 @@ def test_list_of_posts_private_forum(self): topic = add_topic_in_a_forum(forum, profile) # def add_topic_in_a_forum(forum, profile, is_sticky=False, is_solved=False, is_locked=False): - response = profile.client.get(reverse('api:forum:list-post', args=[topic.id])) + response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 1) self.assertEqual(len(response.data.get('results')), 1) @@ -996,7 +992,7 @@ def test_list_of_posts_with_several_pages(self): """ Gets list of posts with several pages in the database. """ - topic = self.create_topic_with_post(REST_PAGE_SIZE + 1) + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE + 1) response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1016,7 +1012,7 @@ def test_list_of_posts_for_a_page_given(self): """ Gets list of posts with several pages and gets a page different that the first one. """ - topic = self.create_topic_with_post(REST_PAGE_SIZE + 1) + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE + 1) response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page=2') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1029,7 +1025,7 @@ def test_list_of_posts_for_a_wrong_page_given(self): """ Gets an error when the posts asks a wrong page. """ - topic = self.create_topic_with_post(1) + topic, posts = self.create_topic_with_post(1) response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page=2') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -1038,11 +1034,11 @@ def test_list_of_posts_with_a_custom_page_size(self): Gets list of forums with a custom page size. DRF allows to specify a custom size for the pagination. """ - topic = self.create_topic_with_post(REST_PAGE_SIZE * 2) + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE * 2) print (topic) page_size = 'page_size' - response = self.client.get(reverse('api:forum:list-post') + '?{}=20'.format(page_size), args=[topic.id]) + response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?{}=20'.format(page_size)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 20) self.assertEqual(len(response.data.get('results')), 20) @@ -1056,11 +1052,13 @@ def test_list_of_posts_in_topic_with_a_wrong_custom_page_size(self): value in settings. """ page_size_value = REST_MAX_PAGE_SIZE + 1 - topic = self.create_topic_with_post(page_size_value) + topic, posts = self.create_topic_with_post(page_size_value) - response = self.client.get(reverse('api:forum:list-post') + '?page_size={}'.format(page_size_value), args=[topic.id]) + response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page_size={}'.format(page_size_value)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('count'), page_size_value) + + # We will have page_size_value + 1 because a post is added at topic creation. + self.assertEqual(response.data.get('count'), page_size_value + 1) self.assertIsNotNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) self.assertEqual(REST_MAX_PAGE_SIZE, len(response.data.get('results'))) @@ -1069,9 +1067,9 @@ def test_list_of_posts_in_unknown_topic(self): """ Tries to list the posts of an non existing Topic. """ - topic = self.create_topic_with_post() + topic, posts = self.create_topic_with_post() - response = self.client.get(reverse('api:forum:list-post'), args=[topic.id]) + response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_list_of_user_topics_empty(self): @@ -1146,6 +1144,7 @@ def test_list_of_user_topics_with_a_custom_page_size(self): self.create_multiple_forums_with_topics(REST_PAGE_SIZE * 2, 1, self.profile) page_size = 'page_size' + self.client = APIClient() response = self.client.get(reverse('api:forum:list-usertopic') + '?{}=20'.format(page_size)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 20) @@ -1173,7 +1172,7 @@ def test_list_of_user_topics_anonymous(self): """ Tries to get a list of users topic with an anonymous user. """ - + self.client = APIClient() response = self.client.get(reverse('api:forum:list-usertopic')) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -1193,7 +1192,8 @@ def test_create_post_with_no_field(self): """ Creates a post in a topic but not with according field. """ - response = self.client.post(reverse('api:forum:list-post', args=[self.private_topic.id]), {}) + topic = self.create_multiple_forums_with_topics(1, 1) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), {}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_create_post_with_empty_field(self): @@ -1203,7 +1203,8 @@ def test_create_post_with_empty_field(self): data = { 'text': '' } - response = self.client.post(reverse('api:forum:list-post', args=[self.private_topic.id]), data) + topic = self.create_multiple_forums_with_topics(1, 1) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_create_post_unauthenticated(self): @@ -1231,7 +1232,7 @@ def test_create_post(self): data = { 'text': 'Welcome to this post!' } - topic = create_multiple_forums_with_topics(1, 1) + topic = self.create_multiple_forums_with_topics(1, 1) response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -1253,7 +1254,7 @@ def test_failure_post_in_a_forum_we_cannot_read(self): data = { 'text': 'Welcome to this post!' } - response = self.client.post(reverse('api:forum:list-post', args=(topic.pk,)), data) + response = self.client.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_post_in_a_private_forum(self): @@ -1262,11 +1263,11 @@ def test_post_in_a_private_forum(self): """ category, forum = create_category(self.group_staff) - topic = add_topic_in_a_forum(forum, profile) + topic = add_topic_in_a_forum(forum, self.profile) data = { 'text': 'Welcome to this post!' } - response = self.client_authentificated.post(reverse('api:forum:list-post', args=(topic.pk,)), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) post = Post.objects.filter(topic=topic.id) @@ -1282,13 +1283,13 @@ def test_detail_post(self): Gets all information about a post. """ - topic = self.create_topic_with_post() - post = Post.objects.filter(topic=topic.id).first() + topic, posts = self.create_topic_with_post() + post = posts[0] response = self.client.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(post.id, response.data.get('id')) self.assertIsNotNone(response.data.get('text')) - self.assertIsNone(response.data.get('text_html')) + self.assertIsNotNone(response.data.get('text_html')) self.assertIsNotNone(response.data.get('pubdate')) self.assertIsNone(response.data.get('update')) self.assertEqual(post.position_in_topic, response.data.get('position_in_topic')) @@ -1316,18 +1317,18 @@ def test_detail_of_private_post(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) # User - response = self.client_authentificated.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + response = self.client_authenticated.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # Staff user - response = self.client_authentificated_staff.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) + response = self.client_authenticated_staff.get(reverse('api:forum:detail-post', args=[topic.id, post.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_list_of_member_posts_empty(self): """ Gets empty list of posts that that a specified member created. """ - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 0) self.assertEqual(response.data.get('results'), []) @@ -1340,7 +1341,7 @@ def test_list_of_member_posts(self): """ self.create_multiple_forums_with_topics(10, 1, self.profile) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 10) self.assertEqual(len(response.data.get('results')), 10) @@ -1357,13 +1358,13 @@ def test_list_of_staff_posts(self): topic = add_topic_in_a_forum(forum, profile) self.client = APIClient() - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[profile.id])) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[profile.id]) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client_authenticated_staff.get(reverse('api:forum:list-memberpost'), args=[profile.id]) + response = self.client_authenticated_staff.get(reverse('api:forum:list-memberpost', args=[profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_list_of_member_posts_with_several_pages(self): @@ -1372,14 +1373,14 @@ def test_list_of_member_posts_with_several_pages(self): """ self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) self.assertIsNotNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) self.assertIsNone(response.data.get('next')) @@ -1392,7 +1393,7 @@ def test_list_of_member_posts_for_a_page_given(self): """ self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost')+ '?page=2', args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])+ '?page=2') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 11) self.assertEqual(len(response.data.get('results')), 1) @@ -1403,7 +1404,7 @@ def test_list_of_member_post_for_a_wrong_page_given(self): """ Gets an error when the member posts asks a wrong page. """ - response = self.client_authenticated.get(reverse('api:forum:list-memberpost') + '?page=2', args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id]) + '?page=2') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_list_of_member_posts_with_a_custom_page_size(self): @@ -1430,7 +1431,7 @@ def test_list_of_member_posts_with_a_wrong_custom_page_size(self): page_size_value = REST_MAX_PAGE_SIZE + 1 self.create_multiple_forums_with_topics(page_size_value, 1, self.profile) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost') + '?page_size={}'.format(page_size_value), args=[self.profile.id]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id]) + '?page_size={}'.format(page_size_value)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), page_size_value) self.assertIsNotNone(response.data.get('next')) @@ -1441,7 +1442,7 @@ def test_list_of_unknow_member_posts(self): """ Gets empty list of posts for a member that does not exists. """ - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[666]) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[666])) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_alert_post(self): @@ -1458,10 +1459,10 @@ def test_alert_post(self): } self.client = APIClient() - response = self.client.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + response = self.client.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - response = self.client_authenticated.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) alerte = Alert.objects.latest('pubdate') @@ -1483,13 +1484,13 @@ def test_alert_post_in_private_forum(self): } self.client = APIClient() - response = self.client.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + response = self.client.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - response = self.client_authenticated.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client_authenticated_staff.post(reverse('api:forum:list-topic', args=[topic.id, post.id]), data) + response = self.client_authenticated_staff.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) alerte = Alert.objects.latest('pubdate') @@ -1509,10 +1510,10 @@ def test_alert_post_not_found(self): 'text': 'There is a guy flooding about Flask, con you do something about it ?', } - response = self.client_authentificated.post(reverse('api:forum:list-topic', args=[666, post.id]), data) + response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[666, post.id]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client_authentificated.post(reverse('api:forum:list-topic', args=[topic.id, 666]), data) + response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[topic.id, 666]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Edite un message en anonymous DONE @@ -1548,7 +1549,7 @@ def test_update_post_other_user(self): 'text': 'I made an error I want to edit.' } topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, self.profile) - response = self.client_authentificated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(data.get('text'), response.data.get('text')) @@ -1559,8 +1560,8 @@ def test_update_post(self): data = { 'text': 'I made an error I want to edit.' } - topic, posts = self.create_topic_with_post(REST) - response = self.client_authentificated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + topic, posts = self.create_topic_with_post() + response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_update_post_staff(self): @@ -1571,7 +1572,7 @@ def test_update_post_staff(self): 'text': 'I am Vladimir Lupin, I do want I want.' } topic, posts = self.create_topic_with_post() - response = self.client_authentificated_staff.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(reponse.data.get('text'), data.get('text')) @@ -1582,7 +1583,7 @@ def test_update_unknow_post(self): data = { 'text': 'I am Vladimir Lupin, I do want I want.' } - response = self.client_authentificated_staff.put(reverse('api:forum:detail-post', args=[666, 42]), data) + response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[666, 42]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_update_post_in_private_topic_anonymous(self): @@ -1595,7 +1596,7 @@ def test_update_post_in_private_topic_anonymous(self): self.client = APIClient(); category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, self.staff) - post = PostFactory(topic=topic, author=self.staff, position=1) + posts = PostFactory(topic=topic, author=self.staff.user, position=1) response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 78359fbc54..279dfcfe0f 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -1,6 +1,6 @@ # coding: utf-8 -from zds.member.api.permissions import CanReadTopic, CanReadPost, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser +from zds.member.api.permissions import CanReadTopic, CanReadPost, CanReadForum, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser from zds.utils.api.views import KarmaView from zds.forum.models import Post, Forum, Topic import datetime @@ -110,6 +110,10 @@ def get(self, request, *args, **kwargs): def get_serializer_class(self): return ForumSerializer + + def get_permissions(self): + permission_classes = [AllowAny, CanReadForum] + return [permission() for permission in permission_classes] class TopicListAPI(ListCreateAPIView): @@ -193,8 +197,9 @@ def get_serializer_class(self): def get_permissions(self): permission_classes = [AllowAny, ] if self.request.method == 'POST': - permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) + permission_classes.append(CanReadAndWriteNowOrReadOnly) + permission_classes.append(CanReadTopic) return [permission() for permission in permission_classes] @@ -237,6 +242,10 @@ def get(self, request, *args, **kwargs): def get_queryset(self): topics = Topic.objects.filter(author=self.request.user) return topics + + def get_permissions(self): + permission_classes = [AllowAny, IsAuthenticated] + return [permission() for permission in permission_classes] class TopicDetailAPI(RetrieveUpdateAPIView): @@ -292,9 +301,8 @@ def get_serializer_class(self): def get_permissions(self): permission_classes = [] if self.request.method == 'GET': - permission_classes.append(DRYPermissions) + permission_classes.append(CanReadTopic) elif self.request.method == 'PUT': - permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticatedOrReadOnly) permission_classes.append(IsOwnerOrReadOnly) permission_classes.append(CanReadTopic) @@ -375,10 +383,11 @@ def get_current_user(self): return self.request.user.profile def get_permissions(self): - permission_classes = [AllowAny, CanReadPost] + permission_classes = [AllowAny, CanReadPost, CanReadTopic] if self.request.method == 'POST': permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) + permission_classes.append(CanReadAndWriteNowOrReadOnly) return [permission() for permission in permission_classes] @@ -505,6 +514,14 @@ def get_serializer_class(self): elif self.request.method == 'PUT': return PostUpdateSerializer + def get_permissions(self): + permission_classes = [AllowAny, CanReadPost] + if self.request.method == 'PUT': + permission_classes.append(IsAuthenticated) + permission_classes.append(IsNotOwnerOrReadOnly) + permission_classes.append(CanReadAndWriteNowOrReadOnly) + return [permission() for permission in permission_classes] + class PostAlertAPI(CreateAPIView): """ @@ -534,8 +551,8 @@ def post(self, request, *args, **kwargs): - code: 401 message: Not Authenticated """ - author = request.user.id - post = self.kwargs.get('pk') + author = request.user + post = Post.objects.get(id = self.kwargs.get('pk')) serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) @@ -544,11 +561,12 @@ def post(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def get_permissions(self): - permission_classes = [AllowAny, ] - permission_classes.append(IsAuthenticated) + permission_classes = [AllowAny, IsAuthenticated] + return [permission() for permission in permission_classes] # TODO global identier quand masquer les messages # TODO gerer l'antispam # TODO alerter un post A tester # TODO editer un post A tester +# TODO empecher les ls et les ban de faire des alertes ? diff --git a/zds/forum/models.py b/zds/forum/models.py index b0d50c0e4e..6552647a36 100644 --- a/zds/forum/models.py +++ b/zds/forum/models.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from django.db import models +from zds.member.api.permissions import CanReadTopic, CanReadPost, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser from zds.forum.managers import TopicManager, ForumManager, PostManager, TopicReadManager from zds.notification import signals from zds.settings import ZDS_APP @@ -436,6 +437,14 @@ def is_author(self, user): """ return self.author == user + + @staticmethod + def has_read_permission(request): + return True + + def has_object_read_permission(self, request): + return Post.has_read_permission(request) + @staticmethod def has_write_permission(request): return request.user.is_authenticated() and request.user.profile.can_write_now() diff --git a/zds/member/api/permissions.py b/zds/member/api/permissions.py index 311c74b5ef..4bd14e6656 100644 --- a/zds/member/api/permissions.py +++ b/zds/member/api/permissions.py @@ -87,7 +87,15 @@ class CanReadTopic(permissions.BasePermission): def has_object_permission(self, request, view, obj): return obj.forum.can_read(request.user) - + + +class CanReadForum(permissions.BasePermission): + """ + Checks if the user can read that forum + """ + + def has_object_permission(self, request, view, obj): + return obj.can_read(request.user) class CanReadPost(permissions.BasePermission): """ From a86b075ea3b052d035854d3a55c6e99d0ea160a9 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 29 Dec 2016 14:16:38 +0000 Subject: [PATCH 07/78] Add validator, test --- zds/forum/api/serializer.py | 15 ++++---- zds/forum/api/tests.py | 67 ++++++++++++++++++++------------- zds/forum/api/views.py | 16 ++++++-- zds/forum/forms.py | 3 +- zds/forum/models.py | 5 +-- zds/tutorialv2/forms.py | 2 +- zds/utils/forms.py | 67 --------------------------------- zds/utils/tests/tests_models.py | 2 +- 8 files changed, 68 insertions(+), 109 deletions(-) diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index ecb0d34f1b..59f1495c4c 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -5,6 +5,7 @@ from dry_rest_permissions.generics import DRYPermissionsField from dry_rest_permissions.generics import DRYPermissions from django.shortcuts import get_object_or_404 +from zds.utils.validators import TitleValidator, TextValidator class ForumSerializer(ModelSerializer): @@ -21,7 +22,7 @@ class Meta: # Idem renommer TODO -class TopicActionSerializer(ModelSerializer): +class TopicActionSerializer(ModelSerializer, TitleValidator, TextValidator): """ Serializer to create a new topic. """ @@ -32,7 +33,7 @@ class Meta: model = Topic fields = ('id', 'title', 'subtitle', 'text', 'forum', 'author', 'last_message', 'pubdate', - 'permissions', 'slug') + 'permissions', 'slug', 'position') read_only_fields = ('id', 'author', 'last_message', 'pubdate', 'permissions') # TODO je pense qu'avec cette config on peut deplacer un sujet en tant qu'user # TODO le text deconne @@ -58,7 +59,7 @@ def create(self, validated_data): False) """ -class TopicUpdateSerializer(ModelSerializer): +class TopicUpdateSerializer(ModelSerializer, TitleValidator, TextValidator): """ Serializer to update a topic. """ @@ -70,8 +71,8 @@ class TopicUpdateSerializer(ModelSerializer): class Meta: model = Topic - fields = ('id', 'title', 'subtitle', 'permissions',) - read_only_fields = ('id', 'permissions',) + fields = ('id', 'title', 'subtitle', 'permissions', 'forum') + read_only_fields = ('id', 'permissions', 'forum') def update(self, instance, validated_data): for attr, value in validated_data.items(): @@ -87,7 +88,7 @@ class Meta: permissions_classes = DRYPermissions # TODO renommer -class PostActionSerializer(ModelSerializer): +class PostActionSerializer(ModelSerializer, TextValidator): """ Serializer to send a post in a topic """ @@ -110,7 +111,7 @@ def create(self, validated_data): #def throw_error(self, key=None, message=None): #raise serializers.ValidationError(message) -class PostUpdateSerializer(ModelSerializer): +class PostUpdateSerializer(ModelSerializer, TextValidator): """ Serializer to update a post. """ diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 192e0bf4dd..ac9d5c77c2 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -278,7 +278,7 @@ def test_list_of_forums(self): def test_list_of_forums_private(self): """ - Gets list of forums not empty in the database. + Gets list of private forums not empty in the database (only for staff). """ category, forum = create_category(self.group_staff) @@ -833,7 +833,6 @@ def test_new_post_unknown_topic(self): # Edite un sujet sans changement # Édite un sujet qvec user en ls # Édite un sujet avec user banni -# Édite un sujet en vidant le titre # Édite un sujet en le passant en resolu # Editer dans un forum privé ? Verifier les auths # TODO @@ -846,9 +845,22 @@ def test_update_topic_details_title(self): 'title': 'Mon nouveau titre' } topic = self.create_multiple_forums_with_topics(1, 1, self.profile) - response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + print('test_update_topic_details_title') + print(response) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('title'), data.get('title')) + + def test_update_topic_details_title_empty(self): + """ + Updates title of a topic, tries to put an empty sting + """ + data = { + 'title': '' + } + topic = self.create_multiple_forums_with_topics(1, 1, self.profile) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_update_topic_details_subtitle(self): """ @@ -915,7 +927,8 @@ def test_update_topic_forum_user(self): data = { 'forum': 5 } - self.create_multiple_forums_with_topics(5, 1, self.profile) + profile = ProfileFactory() + self.create_multiple_forums_with_topics(5, 1, profile) topic = Topic.objects.filter(forum=1).first() response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -944,11 +957,12 @@ def test_list_of_posts(self): """ Gets list of posts in a topic. """ - topic, posts = self.create_topic_with_post() + # A post is already included with the topic + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE - 1) response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) @@ -1034,7 +1048,7 @@ def test_list_of_posts_with_a_custom_page_size(self): Gets list of forums with a custom page size. DRF allows to specify a custom size for the pagination. """ - topic, posts = self.create_topic_with_post(REST_PAGE_SIZE * 2) + topic, posts = self.create_topic_with_post((REST_PAGE_SIZE * 2) - 1) print (topic) page_size = 'page_size' @@ -1067,9 +1081,8 @@ def test_list_of_posts_in_unknown_topic(self): """ Tries to list the posts of an non existing Topic. """ - topic, posts = self.create_topic_with_post() - response = self.client.get(reverse('api:forum:list-post', args=[topic.id])) + response = self.client.get(reverse('api:forum:list-post', args=[666])) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_list_of_user_topics_empty(self): @@ -1254,7 +1267,7 @@ def test_failure_post_in_a_forum_we_cannot_read(self): data = { 'text': 'Welcome to this post!' } - response = self.client.post(reverse('api:forum:list-post', args=[topic.pk,]), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_post_in_a_private_forum(self): @@ -1292,7 +1305,9 @@ def test_detail_post(self): self.assertIsNotNone(response.data.get('text_html')) self.assertIsNotNone(response.data.get('pubdate')) self.assertIsNone(response.data.get('update')) - self.assertEqual(post.position_in_topic, response.data.get('position_in_topic')) + print('test_detail_post') + print(response.data) + self.assertEqual(post.position, response.data.get('position_in_topic')) self.assertEqual(topic.author, response.data.get('author')) def test_detail_of_a_private_post_not_present(self): @@ -1371,7 +1386,8 @@ def test_list_of_member_posts_with_several_pages(self): """ Gets list of a member topics with several pages in the database. """ - self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) + # When we create a Topic a post is also added. + self.create_multiple_forums_with_topics(REST_PAGE_SIZE, 1, self.profile) response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1380,7 +1396,7 @@ def test_list_of_member_posts_with_several_pages(self): self.assertIsNone(response.data.get('previous')) self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) - response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) + response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id]) + '?page=2') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), REST_PAGE_SIZE + 1) self.assertIsNone(response.data.get('next')) @@ -1409,13 +1425,13 @@ def test_list_of_member_post_for_a_wrong_page_given(self): def test_list_of_member_posts_with_a_custom_page_size(self): """ - Gets list of user's topics with a custom page size. DRF allows to specify a custom + Gets list of user's posts with a custom page size. DRF allows to specify a custom size for the pagination. """ - self.create_topic_with_post(REST_PAGE_SIZE * 2, 1, self.profile) + self.create_topic_with_post(REST_PAGE_SIZE * 2, self.profile) page_size = 'page_size' - response = self.client.get(reverse('api:forum:list-memberpost') + '?{}=20'.format(page_size), args=[self.profile.id]) + response = self.client.get(reverse('api:forum:list-memberpost', args=[self.profile.id]) + '?{}=20'.format(page_size)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 20) self.assertEqual(len(response.data.get('results')), 20) @@ -1455,7 +1471,7 @@ def test_alert_post(self): another_profile = ProfileFactory() post = PostFactory(topic=topic, author=another_profile.user, position=1) data = { - 'text': 'There is a guy flooding about Flask, con you do something about it ?', + 'text': 'There is a guy flooding about Flask, con you do something about it ?' } self.client = APIClient() @@ -1480,7 +1496,7 @@ def test_alert_post_in_private_forum(self): topic = add_topic_in_a_forum(forum, profile) post = PostFactory(topic=topic, author=profile.user, position=1) data = { - 'text': 'There is a guy flooding about Flask, con you do something about it ?', + 'text': 'There is a guy flooding about Flask, con you do something about it ?' } self.client = APIClient() @@ -1507,7 +1523,7 @@ def test_alert_post_not_found(self): topic = add_topic_in_a_forum(forum, profile) post = PostFactory(topic=topic, author=profile.user, position=1) data = { - 'text': 'There is a guy flooding about Flask, con you do something about it ?', + 'text': 'There is a guy flooding about Flask, con you do something about it ?' } response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[666, post.id]), data) @@ -1550,9 +1566,8 @@ def test_update_post_other_user(self): } topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, self.profile) response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(data.get('text'), response.data.get('text')) - + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_update_post(self): """ Updates a post with user. @@ -1560,7 +1575,7 @@ def test_update_post(self): data = { 'text': 'I made an error I want to edit.' } - topic, posts = self.create_topic_with_post() + topic, posts = self.create_topic_with_post(1, self.profile) response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -1574,7 +1589,7 @@ def test_update_post_staff(self): topic, posts = self.create_topic_with_post() response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(reponse.data.get('text'), data.get('text')) + self.assertEqual(response.data.get('text'), data.get('text')) def test_update_unknow_post(self): """ @@ -1596,9 +1611,9 @@ def test_update_post_in_private_topic_anonymous(self): self.client = APIClient(); category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, self.staff) - posts = PostFactory(topic=topic, author=self.staff.user, position=1) + post = PostFactory(topic=topic, author=self.staff.user, position=1) - response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) + response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 279dfcfe0f..7977f3f0f8 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -82,6 +82,10 @@ def get(self, request, *args, **kwargs): message: Not Found """ return self.list(request, *args, **kwargs) + + def get_permissions(self): + permission_classes = [AllowAny, CanReadForum] + return [permission() for permission in permission_classes] class ForumDetailAPI(RetrieveAPIView): @@ -200,6 +204,7 @@ def get_permissions(self): permission_classes.append(IsAuthenticated) permission_classes.append(CanReadAndWriteNowOrReadOnly) permission_classes.append(CanReadTopic) + permission_classes.append(CanReadForum) return [permission() for permission in permission_classes] @@ -377,6 +382,8 @@ def get_serializer_class(self): def get_queryset(self): if self.request.method == 'GET': posts = Post.objects.filter(topic=self.kwargs.get('pk')) + #if posts is None: + # raise Http404("Topic with pk {} was not found".format(self.kwargs.get('pk'))) return posts def get_current_user(self): @@ -388,6 +395,7 @@ def get_permissions(self): permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) permission_classes.append(CanReadAndWriteNowOrReadOnly) + permission_classes.append(CanReadPost) return [permission() for permission in permission_classes] @@ -528,8 +536,6 @@ class PostAlertAPI(CreateAPIView): Alert a topic post to the staff. """ - serializer_class = AlertSerializer - def post(self, request, *args, **kwargs): """ Alert a topic post to the staff. @@ -556,13 +562,17 @@ def post(self, request, *args, **kwargs): serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) - serializer.save(position=0, author=author, comment=post) + serializer.save(comment=post) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def get_permissions(self): permission_classes = [AllowAny, IsAuthenticated] return [permission() for permission in permission_classes] + + def get_serializer_class(self): + return AlertSerializer + # TODO global identier quand masquer les messages diff --git a/zds/forum/forms.py b/zds/forum/forms.py index 61d4db3f4d..40027cb0eb 100644 --- a/zds/forum/forms.py +++ b/zds/forum/forms.py @@ -8,7 +8,8 @@ from crispy_forms.layout import Layout, Field, Hidden from crispy_forms.bootstrap import StrictButton from zds.forum.models import Forum, Topic -from zds.utils.forms import CommonLayoutEditor, TagValidator +from zds.utils.forms import CommonLayoutEditor +from zds.utils.validators import TagValidator from django.utils.translation import ugettext_lazy as _ diff --git a/zds/forum/models.py b/zds/forum/models.py index 6552647a36..26c7c39411 100644 --- a/zds/forum/models.py +++ b/zds/forum/models.py @@ -388,14 +388,14 @@ def has_read_permission(request): return True def has_object_read_permission(self, request): - return Topic.has_read_permission(request) # TODO gerer fofo prives + return Topic.has_read_permission(request) @staticmethod def has_write_permission(request): return request.user.is_authenticated() def has_object_write_permission(self, request): - return Topic.has_write_permission(request) # TODO gerer les fofo prives + return Topic.has_write_permission(request) def has_object_update_permission(self, request): return Topic.has_write_permission(request) and (Topic.author == request.user) @@ -451,7 +451,6 @@ def has_write_permission(request): def has_object_write_permission(self, request): return Topic.has_write_permission(request) - # TODO verifier que ce n'est pas un forum prive def has_object_update_permission(self, request): return self.is_author(request.user) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 3a4a77f417..578fd7f9f4 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _ from zds.member.models import Profile from zds.tutorialv2.utils import slugify_raise_on_invalid, InvalidSlugError -from zds.utils.forms import TagValidator +from zds.utils.validators import TagValidator class FormWithTitle(forms.Form): diff --git a/zds/utils/forms.py b/zds/utils/forms.py index c48256fd39..b40b063b3c 100644 --- a/zds/utils/forms.py +++ b/zds/utils/forms.py @@ -66,70 +66,3 @@ def __init__(self, *args, **kwargs): Field('text'), *args, **kwargs ) - - -class TagValidator(object): - """ - validate tags - """ - def __init__(self): - self.__errors = [] - self.logger = logging.getLogger("zds.utils.forms") - self.__clean = [] - - def validate_raw_string(self, raw_string): - """ - validate a string composed as ``tag1,tag2``. - - :param raw_string: the string to be validate. If ``None`` this is considered as a empty str - :type raw_string: basestring - :return: ``True`` if ``raw_string`` is fully valid, ``False`` if at least one error appears. See ``self.errors`` - to get all internationalized error. - """ - if raw_string is None or not isinstance(raw_string, basestring): - return self.validate_string_list([]) - return self.validate_string_list(raw_string.split(",")) - - def validate_length(self, tag): - """ - Check the length is in correct range. See ``Tag.label`` max length to have the true upper bound. - - :param tag: the tag lavel to validate - :return: ``True`` if length is valid - """ - if len(tag) > Tag._meta.get_field("title").max_length: - self.errors.append(_(u"Le tag {} est trop long (maximum {} caractères)".format( - tag, Tag._meta.get_field("title").max_length))) - self.logger.debug("%s est trop long expected=%d got=%d", tag, - Tag._meta.get_field("title").max_length, len(tag)) - return False - return True - - def validate_string_list(self, string_list): - """ - Same as ``validate_raw_string`` but with a list of tag labels. - - :param string_list: - :return: ``True`` if ``v`` is fully valid, ``False`` if at least one error appears. See ``self.errors`` - to get all internationalized error. - """ - self.__clean = list(filter(self.validate_length, string_list)) - self.__clean = list(filter(self.validate_utf8mb4, self.__clean)) - return len(string_list) == len(self.__clean) - - def validate_utf8mb4(self, tag): - """ - Checks the tag does not contain utf8mb4 chars. - - :param tag: - :return: ``True`` if no utf8mb4 string is found - """ - if contains_utf8mb4(tag): - self.errors.append(_(u"Le tag {} contient des caractères utf8mb4").format(tag)) - self.logger.warn("%s contains utf8mb4 char", tag) - return False - return True - - @property - def errors(self): - return self.__errors diff --git a/zds/utils/tests/tests_models.py b/zds/utils/tests/tests_models.py index 3c768af76f..18d40a8713 100644 --- a/zds/utils/tests/tests_models.py +++ b/zds/utils/tests/tests_models.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.db import IntegrityError, transaction -from zds.utils.forms import TagValidator +from zds.utils.validators import TagValidator from zds.utils.models import Tag From d64117f4282af83659c92d590922c6363adacc8d Mon Sep 17 00:00:00 2001 From: Antonin Date: Mon, 2 Jan 2017 09:05:01 +0000 Subject: [PATCH 08/78] Fixing tests, adding tests --- zds/forum/api/serializer.py | 85 +++++++-------- zds/forum/api/tests.py | 211 +++++++++++++++++++++++++----------- zds/forum/api/views.py | 141 +++++++++++++++++------- zds/utils/validators.py | 121 +++++++++++++++++++++ 4 files changed, 407 insertions(+), 151 deletions(-) create mode 100644 zds/utils/validators.py diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index 59f1495c4c..1a8519a95e 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -1,4 +1,3 @@ -from rest_framework.serializers import ModelSerializer from rest_framework import serializers from zds.forum.models import Forum, Topic, Post from zds.utils.models import Alert @@ -8,87 +7,78 @@ from zds.utils.validators import TitleValidator, TextValidator -class ForumSerializer(ModelSerializer): +class ForumSerializer(serializers.ModelSerializer): class Meta: model = Forum - permissions_classes = DRYPermissions - -class TopicSerializer(ModelSerializer): +class TopicSerializer(serializers.ModelSerializer): class Meta: model = Topic #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') permissions_classes = DRYPermissions -# Idem renommer TODO -class TopicActionSerializer(ModelSerializer, TitleValidator, TextValidator): + +class TopicCreateSerializer(serializers.ModelSerializer, TitleValidator, TextValidator): """ Serializer to create a new topic. """ - text = serializers.SerializerMethodField() + text = serializers.CharField() permissions = DRYPermissionsField() class Meta: model = Topic - fields = ('id', 'title', 'subtitle', 'text', 'forum', + fields = ('id', 'title', 'subtitle', 'forum', 'text', 'author', 'last_message', 'pubdate', - 'permissions', 'slug', 'position') - read_only_fields = ('id', 'author', 'last_message', 'pubdate', 'permissions') -# TODO je pense qu'avec cette config on peut deplacer un sujet en tant qu'user -# TODO le text deconne + 'permissions', 'slug') + read_only_fields = ('id', 'author', 'last_message', 'pubdate', 'permissions', 'slug') + def create(self, validated_data): - text = self._fields.pop('text') + # Remember that text ist not a field for a Topic but for a post. + text = validated_data.pop('text') new_topic = Topic.objects.create(**validated_data) + + # Instead we use text here. first_message = Post.objects.create(topic=new_topic,text=text,position=0,author=new_topic.author) new_topic.last_message = first_message new_topic.save() + + # And serve it here so it appears in response. + new_topic.text = text return new_topic -""" - def create(self, validated_data): - # This hack is necessary because `text` isn't a field of PrivateTopic. - self._fields.pop('text') - return send_mp(self.context.get('request').user, - validated_data.get('participants'), - validated_data.get('title'), - validated_data.get('subtitle') or '', - validated_data.get('text'), - True, - False) -""" - -class TopicUpdateSerializer(ModelSerializer, TitleValidator, TextValidator): + +class TopicUpdateSerializer(serializers.ModelSerializer, TitleValidator, TextValidator): """ Serializer to update a topic. """ - can_be_empty = True # TODO verifier l'interet de cette ligne title = serializers.CharField(required=False, allow_blank=True) subtitle = serializers.CharField(required=False, allow_blank=True) - # TODO ajouter les tag et la resolution permissions = DRYPermissionsField() class Meta: model = Topic - fields = ('id', 'title', 'subtitle', 'permissions', 'forum') - read_only_fields = ('id', 'permissions', 'forum') - + fields = ('id', 'title', 'subtitle', 'permissions', 'forum', 'is_locked', 'is_solved', 'tags') + read_only_fields = ('id', 'permissions', 'forum', 'is_locked') +# Todo : lors de l'update on a le droit de mettre un titre vide ? def update(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance - - -class PostSerializer(ModelSerializer): + + def throw_error(self, key=None, message=None): + raise serializers.ValidationError(message) + +class PostSerializer(serializers.ModelSerializer): class Meta: model = Post - #fields = ('id', 'title', 'subtitle', 'slug', 'category', 'position_in_category') + exclude = ('ip_address', 'text_hidden',) + read_only = ('ip_address', 'text_hidden',) permissions_classes = DRYPermissions -# TODO renommer -class PostActionSerializer(ModelSerializer, TextValidator): +class PostCreateSerializer(serializers.ModelSerializer, TextValidator): """ Serializer to send a post in a topic """ @@ -104,18 +94,17 @@ def create(self, validated_data): # Get topic pk_topic = validated_data.get('topic_id') topic = get_object_or_404(Topic, pk=(pk_topic)) - Post.objects.create(**validated_data) - return topic.last_message + new_post = Post.objects.create(**validated_data) + return new_post # Todo a t on besoin d'un validateur #def throw_error(self, key=None, message=None): #raise serializers.ValidationError(message) -class PostUpdateSerializer(ModelSerializer, TextValidator): +class PostUpdateSerializer(serializers.ModelSerializer, TextValidator): """ Serializer to update a post. """ - can_be_empty = True # TODO verifier l'interet de cette ligne text = serializers.CharField(required=True, allow_blank=True) permissions = DRYPermissionsField() @@ -131,7 +120,7 @@ def update(self, instance, validated_data): return instance -class AlertSerializer(ModelSerializer): +class AlertSerializer(serializers.ModelSerializer): """ Serializer to alert a post. """ @@ -144,12 +133,12 @@ class Meta: def create(self, validated_data): - # Get topic + # Get topic TODO pourquoi ces lignes? pk_post = validated_data.get('comment') post = get_object_or_404(Post, pk=(pk_post)) - Alert.objects.create(**validated_data) - return topic.last_message + alert = Alert.objects.create(**validated_data) + return alert - # Todo a t on besoin d'un validateur + # Todo a t on besoin d'un validateur ? #def throw_error(self, key=None, message=None): #raise serializers.ValidationError(message) \ No newline at end of file diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index ac9d5c77c2..3d3138c01f 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -284,13 +284,16 @@ def test_list_of_forums_private(self): self.client = APIClient() response = self.client.get(reverse('api:forum:list')) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + response = self.client_authenticated.get(reverse('api:forum:list')) - self.assertEqual(response.status_code, status.HTTP_UNAUTHORIZED) - + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + response = self.client_authenticated_staff.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 1) # TODO nombre a affiner en fonction de la realite def test_list_of_forums_with_several_pages(self): @@ -378,9 +381,14 @@ def test_details_forum(self): self.assertEqual(response.data.get('slug'), forum.slug) self.assertEqual(response.data.get('category'), forum.category.id) self.assertEqual(response.data.get('position_in_category'), forum.position_in_category) - self.assertEqual(response.data.get('group'), forum.group) - + + print('-------') + print(type(response.data.get('group'))) + print(type(list(forum.group.all()))) + print('-------') + self.assertEqual(response.data.get('group'), list(forum.group.all())) + def test_details_forum_private(self): """ Tries to get the details of a private forum with different users. @@ -423,11 +431,8 @@ def test_details_private_forum_user(self): self.assertEqual(response.status_code, status.HTTP_200_OK) # TODO -# Récupérer la liste des sujets en filtrant sur l'auteur (resulat non vide) # Récupérer la liste des sujets en filtrant sur le tag (resulat non vide) -# Récupérer la liste des sujets en filtrant sur le forum (resulat non vide) # Récupérer la liste des sujets en filtrant tag, forum, auteur (resulat non vide) -# Idem avec un tag inexistant NE MARCHE PAS def test_list_of_topics_empty(self): """ @@ -534,6 +539,17 @@ def test_list_of_topics_with_forum_filter_empty(self): self.assertEqual(response.data.get('results'), []) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) + + def test_list_of_topics_with_forum_filter(self): + """ + Gets a list of topics in a forum. + """ + self.create_multiple_forums_with_topics(1) + forum = Forum.objects.all().first() + response = self.client.get(reverse('api:forum:list-topic') + '?forum=' + forum.id) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) def test_list_of_topics_with_author_filter_empty(self): """ @@ -547,12 +563,22 @@ def test_list_of_topics_with_author_filter_empty(self): self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) + def test_list_of_topics_with_author_filter(self): + """ + Gets a list of topics created by an user. + """ + self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE,self.profile) + response = self.client.get(reverse('api:forum:list-topic') + '?author=' + str(self.profile.user.id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) + self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) + def test_list_of_topics_with_tag_filter_empty(self): """ Gets an empty list of topics with a specific tag. """ self.create_multiple_forums_with_topics(1) - response = self.client.get(reverse('api:forum:list-topic') + '?tag=ilovezozor') + response = self.client.get(reverse('api:forum:list-topic') + '?tags__title=ilovezozor') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 0) self.assertEqual(response.data.get('results'), []) @@ -574,12 +600,10 @@ def test_new_topic_with_user(self): response = self.client_authenticated.post(reverse('api:forum:list-topic'), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - topics = Topic.objects.filter(author=self.profile.user.id) - print(topics[0]) - self.assertEqual(1, len(topics)) - self.assertEqual(response.data.get('title'), topics[0].title) - self.assertEqual(response.data.get('subtitle'), topics[0].subtitle) - # Todo ne fonctionne pas self.assertEqual(data.get('text'), topics[0].last_message.text) + topic = Topic.objects.filter(author=self.profile.user.id).first() + self.assertEqual(response.data.get('title'), topic.title) + self.assertEqual(response.data.get('subtitle'), topic.subtitle) + self.assertEqual(data.get('text'), topic.last_message.text) self.assertEqual(response.data.get('author'), self.profile.user.id) self.assertIsNotNone(response.data.get('last_message')) self.assertIsNotNone(response.data.get('pubdate')) @@ -830,10 +854,8 @@ def test_new_post_unknown_topic(self): response = self.client_authenticated.post(reverse('api:forum:list-post', args=[666,]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) -# Edite un sujet sans changement # Édite un sujet qvec user en ls # Édite un sujet avec user banni -# Édite un sujet en le passant en resolu # Editer dans un forum privé ? Verifier les auths # TODO @@ -894,7 +916,7 @@ def test_update_topic_staff(self): 'title': 'Mon nouveau titre' } topic = self.create_multiple_forums_with_topics(1, 1, self.profile) - response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('title'), data.get('title')) @@ -920,9 +942,9 @@ def test_update_unknown_topic(self): response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[666]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_update_topic_forum_user(self): + def test_update_topic_forum(self): """ - Tries to move (change forum in which the topic is) with an user. + Tries to move (change forum in which the topic is) with different users. """ data = { 'forum': 5 @@ -930,28 +952,79 @@ def test_update_topic_forum_user(self): profile = ProfileFactory() self.create_multiple_forums_with_topics(5, 1, profile) topic = Topic.objects.filter(forum=1).first() + + # Anonymous + self.client = APIClient() + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # User response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_topic_forum_staff(self): + + # Staff + response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('forum'), data.get('forum')) + + def test_update_topic_lock(self): """ - Tries to move (change forum in which the topic is) with a staff member. + Tries to lock a Topic with different users. """ data = { - 'forum': 5 + 'is_locked': True } - self.create_multiple_forums_with_topics(5, 1, self.profile) - topic = Topic.objects.filter(forum=1).first() + topic = self.create_multiple_forums_with_topics(1, 1) + + self.client = APIClient() + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('forum'), data.get('forum')) + self.assertTrue(response.data.get('is_locked')) + + def test_update_topic_solve(self): + """ + Tries to solve a Topic with different users. + """ + data = { + 'is_solved': True + } + topic = self.create_multiple_forums_with_topics(1, 1, self.profile) + + self.client = APIClient() + response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # Author + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data.get('is_solved')) + + # Other user + other_profile = ProfileFactory() + client_oauth2 = create_oauth2_client(other_profile.user) + client_other_user = APIClient() + authenticate_client(client_other_user, client_oauth2, other_profile.user.username, 'hostel77') + + response = client_other_user.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Staff + response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data.get('is_solved')) def test_list_of_posts_unknown(self): """ Tries to get a list of posts in an unknown topic """ response = self.client.get(reverse('api:forum:list-post', args=[666])) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_list_of_posts(self): """ @@ -1157,8 +1230,7 @@ def test_list_of_user_topics_with_a_custom_page_size(self): self.create_multiple_forums_with_topics(REST_PAGE_SIZE * 2, 1, self.profile) page_size = 'page_size' - self.client = APIClient() - response = self.client.get(reverse('api:forum:list-usertopic') + '?{}=20'.format(page_size)) + response = self.client_authenticated.get(reverse('api:forum:list-usertopic') + '?{}=20'.format(page_size)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), 20) self.assertEqual(len(response.data.get('results')), 20) @@ -1249,7 +1321,11 @@ def test_create_post(self): response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - post = Post.objects.filter(topic=topic.id) + post = topic.get_last_answer() + print('test_create_post') + print(response) + print(response.data) + print('end test create post') self.assertEqual(response.data.get('text'), data.get('text')) self.assertEqual(response.data.get('text'), post.text) self.assertEqual(response.data.get('is_userful'), post.is_userful) @@ -1267,7 +1343,7 @@ def test_failure_post_in_a_forum_we_cannot_read(self): data = { 'text': 'Welcome to this post!' } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) + response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_post_in_a_private_forum(self): @@ -1283,7 +1359,7 @@ def test_post_in_a_private_forum(self): response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - post = Post.objects.filter(topic=topic.id) + post = topic.get_last_answer() self.assertEqual(response.data.get('text'), data.get('text')) self.assertEqual(response.data.get('text'), post.text) self.assertEqual(response.data.get('is_userful'), post.is_userful) @@ -1307,8 +1383,8 @@ def test_detail_post(self): self.assertIsNone(response.data.get('update')) print('test_detail_post') print(response.data) - self.assertEqual(post.position, response.data.get('position_in_topic')) - self.assertEqual(topic.author, response.data.get('author')) + self.assertEqual(post.position, response.data.get('position')) + self.assertEqual(topic.author.id, response.data.get('author')) def test_detail_of_a_private_post_not_present(self): """ @@ -1372,15 +1448,20 @@ def test_list_of_staff_posts(self): category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) + # Anonymous user cannot see staff private post. self.client = APIClient() + response = self.client.get(reverse('api:forum:list-memberpost', args=[profile.id])) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + + # Same for normal user response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[profile.id])) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - response = self.client_authenticated.get(reverse('api:forum:list-memberpost'), args=[profile.id]) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 0) + response = self.client_authenticated_staff.get(reverse('api:forum:list-memberpost', args=[profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('count'), 1) def test_list_of_member_posts_with_several_pages(self): """ @@ -1428,7 +1509,8 @@ def test_list_of_member_posts_with_a_custom_page_size(self): Gets list of user's posts with a custom page size. DRF allows to specify a custom size for the pagination. """ - self.create_topic_with_post(REST_PAGE_SIZE * 2, self.profile) + # When a topic is created, a post is also created. + self.create_topic_with_post((REST_PAGE_SIZE * 2) - 1, self.profile) page_size = 'page_size' response = self.client.get(reverse('api:forum:list-memberpost', args=[self.profile.id]) + '?{}=20'.format(page_size)) @@ -1483,8 +1565,8 @@ def test_alert_post(self): alerte = Alert.objects.latest('pubdate') self.assertEqual(alerte.text, data.get('text')) - self.assertEqual(alerte.author, self.client_authenticated) - self.assertEqual(alerte.comment, post.id) + self.assertEqual(alerte.author, self.profile.user) + self.assertEqual(alerte.comment.id, post.id) def test_alert_post_in_private_forum(self): @@ -1526,22 +1608,12 @@ def test_alert_post_not_found(self): 'text': 'There is a guy flooding about Flask, con you do something about it ?' } - response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[666, post.id]), data) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[topic.id, 666]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - -# Edite un message en anonymous DONE -# Edite un message avec le bon user DONE -# Edite un message avec un autre user DONE -# Edite un message avec un staff DONE + # Edite un message d'un sujet fermé user # Edite un message d'un sujet fermé staff -# Edite un message dans un forum privé user -# Edite un message dans un forum privé anonymous DONE -# Edite un message dans un forum privé staff -# Edite un message un message qui n'exite pas DONE + # TODO @@ -1596,12 +1668,12 @@ def test_update_unknow_post(self): Tries to update post that does not exist. """ data = { - 'text': 'I am Vladimir Lupin, I do want I want.' + 'text': 'I made an error I want to edit.' } - response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[666, 42]), data) + response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[666, 42]), data) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_update_post_in_private_topic_anonymous(self): + def test_update_post_in_private_topic(self): """ Tries to update a post in a private forum (and topic) with anonymous user. """ @@ -1613,11 +1685,18 @@ def test_update_post_in_private_topic_anonymous(self): topic = add_topic_in_a_forum(forum, self.staff) post = PostFactory(topic=topic, author=self.staff.user, position=1) + # With anonymous response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - + # With user + response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # With staff (member of private forum) + response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(data.get('text'), response.get('text')) def create_oauth2_client(user): client = Application.objects.create(user=user, @@ -1639,7 +1718,9 @@ def authenticate_client(client, client_auth, username, password): client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) # TODO -# Reorganiser le code de test en differentes classes -# Renommer les serializer -# Voir ou on a besoin de read only -# Voir ou a besoin de validator \ No newline at end of file +# Reorganiser le code de test en differentes classes, reordonner les tests +# Voir où on a besoin de read only +# Voir où a besoin de validator +# Vérifier que l'on affiche pas le text hidden ou l'adresse ip +# Créer un topic avec des tags (ajouter le test) +# Tester le cas ou un gars veux vider le contenu de ses messages \ No newline at end of file diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 7977f3f0f8..730e382fce 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -2,10 +2,11 @@ from zds.member.api.permissions import CanReadTopic, CanReadPost, CanReadForum, CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsOwnerOrReadOnly, IsStaffUser from zds.utils.api.views import KarmaView -from zds.forum.models import Post, Forum, Topic +from zds.forum.models import Post, Forum, Topic, Category import datetime from django.core.cache import cache from django.db.models.signals import post_save, post_delete +from django.http import Http404 from rest_framework import filters from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveAPIView, CreateAPIView from rest_framework_extensions.key_constructor.constructors import DefaultKeyConstructor @@ -18,9 +19,10 @@ from dry_rest_permissions.generics import DRYPermissions from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit from zds.utils import slugify -from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicActionSerializer, TopicUpdateSerializer, PostSerializer, PostActionSerializer, PostUpdateSerializer, AlertSerializer +from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicCreateSerializer, TopicUpdateSerializer, PostSerializer, PostCreateSerializer, PostUpdateSerializer, AlertSerializer from zds.forum.api.permissions import IsStaffUser - +from zds.member.models import User +from itertools import chain class PostKarmaView(KarmaView): queryset = Post.objects.all() @@ -54,10 +56,8 @@ def change_api_forum_updated_at(sender=None, instance=None, *args, **kwargs): class ForumListAPI(ListCreateAPIView): """ - Profile resource to list all forum + Profile resource to list all forum. """ - - queryset = Forum.objects.all() serializer_class = ForumSerializer list_key_func = PagingSearchListKeyConstructor() @@ -82,9 +82,14 @@ def get(self, request, *args, **kwargs): message: Not Found """ return self.list(request, *args, **kwargs) - + + def get_queryset(self): + public_forums = Forum.objects.filter(group__isnull=True).order_by('position_in_category') + private_forums = Forum.objects.filter(group__in=self.request.user.groups.all()).order_by('position_in_category') + return public_forums | private_forums + def get_permissions(self): - permission_classes = [AllowAny, CanReadForum] + permission_classes = [CanReadForum] # TODO style plus joli ? return [permission() for permission in permission_classes] @@ -116,13 +121,13 @@ def get_serializer_class(self): return ForumSerializer def get_permissions(self): - permission_classes = [AllowAny, CanReadForum] + permission_classes = [CanReadForum] # TODO style plus joli ? return [permission() for permission in permission_classes] class TopicListAPI(ListCreateAPIView): """ - Profile resource to list all topic + Profile resource to list all topics (GET) or to create a topic (POST) """ queryset = Topic.objects.all() filter_backends = (filters.DjangoFilterBackend,) @@ -145,6 +150,15 @@ def get(self, request, *args, **kwargs): description: Sets the number of forum per page. required: false paramType: query + - name: forum + description: Filters by forum id. + required: false + - name: author + description: Filters by author id. + required: false + - name: tags__title + description: Filters by tag name. + required: false responseMessages: - code: 404 message: Not Found @@ -178,13 +192,19 @@ def post(self, request, *args, **kwargs): description: Content of the first post in markdown. required: true paramType: form + - name: tags + description: To add a tag, specify its taf identifier. Specify this parameter + several times to add several tags. + required: false + paramType: form TODO vérifier que les tags fonctionnent responseMessages: - code: 400 message: Bad Request - code: 401 message: Not Authenticated + - code: 403 + message: Forbidden """ - author = request.user.id serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) @@ -196,7 +216,7 @@ def get_serializer_class(self): if self.request.method == 'GET': return TopicSerializer elif self.request.method == 'POST': - return TopicActionSerializer + return TopicCreateSerializer def get_permissions(self): permission_classes = [AllowAny, ] @@ -210,7 +230,7 @@ def get_permissions(self): class UserTopicListAPI(ListAPIView): """ - Profile resource to list all topic from current user + Profile resource to list all topics from current user """ serializer_class = TopicSerializer @@ -239,6 +259,8 @@ def get(self, request, *args, **kwargs): required: false paramType: query responseMessages: + - code: 401 + message: Not Authenticated - code: 404 message: Not Found """ @@ -255,7 +277,7 @@ def get_permissions(self): class TopicDetailAPI(RetrieveUpdateAPIView): """ - Profile resource to display and updates details of a given topic + Profile resource to display and update details of a given topic """ queryset = Topic.objects.all() obj_key_func = DetailKeyConstructor() @@ -267,6 +289,10 @@ def get(self, request, *args, **kwargs): Gets a topic given by its identifier. --- responseMessages: + - code: 401 + message: Not Authenticated + - code: 403 + message: Forbidden - code: 404 message: Not Found """ @@ -284,7 +310,23 @@ def put(self, request, *args, **kwargs): description: Bearer token to make an authenticated request. required: true paramType: header - # TODO doc manquante + - name: title + description: Title of the Topic. + required: false + paramType: form + - name: subtitle + description: Subtitle of the Topic. + required: false + paramType: form + - name: text + description: Content of the first post in markdown. + required: false + paramType: form + - name: tags + description: To add a tag, specify its taf identifier. Specify this parameter + several times to add several tags. + required: false + paramType: form TODO vérifier que les tags fonctionnent responseMessages: - code: 400 message: Bad Request if you specify a bad identifier @@ -316,7 +358,7 @@ def get_permissions(self): class PostListAPI(ListCreateAPIView): """ - Profile resource to list all message in a topic + Profile resource to list all messages in a topic """ list_key_func = PagingSearchListKeyConstructor() @@ -337,6 +379,10 @@ def get(self, request, *args, **kwargs): required: false paramType: query responseMessages: + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied - code: 404 message: Not Found """ @@ -347,7 +393,6 @@ def post(self, request, *args, **kwargs): """ Creates a new post in a topic. --- - parameters: - name: Authorization description: Bearer token to make an authenticated request. @@ -357,12 +402,18 @@ def post(self, request, *args, **kwargs): description: Content of the post in markdown. required: true paramType: form - # TODO doc manquante + - name: text + description: Content of the first post in markdown. + required: true responseMessages: - code: 400 message: Bad Request - code: 401 message: Not Authenticated + - code: 403 + message: Permission Denied + - code: 404 + message: Not Found """ author = request.user.id topic = self.kwargs.get('pk') @@ -377,25 +428,23 @@ def get_serializer_class(self): if self.request.method == 'GET': return PostSerializer elif self.request.method == 'POST': - return PostActionSerializer + return PostCreateSerializer def get_queryset(self): if self.request.method == 'GET': posts = Post.objects.filter(topic=self.kwargs.get('pk')) - #if posts is None: - # raise Http404("Topic with pk {} was not found".format(self.kwargs.get('pk'))) + if posts.count() == 0: + raise Http404("Topic with pk {} was not found".format(self.kwargs.get('pk'))) return posts def get_current_user(self): return self.request.user.profile def get_permissions(self): - permission_classes = [AllowAny, CanReadPost, CanReadTopic] + permission_classes = [AllowAny, CanReadPost, CanReadTopic, CanReadForum, CanReadPost] if self.request.method == 'POST': - permission_classes.append(DRYPermissions) permission_classes.append(IsAuthenticated) permission_classes.append(CanReadAndWriteNowOrReadOnly) - permission_classes.append(CanReadPost) return [permission() for permission in permission_classes] @@ -431,7 +480,13 @@ def get(self, request, *args, **kwargs): def get_queryset(self): if self.request.method == 'GET': - posts = Post.objects.filter(author=self.kwargs.get('pk')) + try: + author = User.objects.get(pk = self.kwargs.get('pk')) + except User.DoesNotExist: + raise Http404("User with pk {} was not found".format(self.kwargs.get('pk'))) + + # Gets every post of author visible by current user + posts = Post.objects.get_all_messages_of_a_user(self.request.user, author) return posts @@ -447,9 +502,8 @@ class UserPostListAPI(ListAPIView): @cache_response(key_func=list_key_func) def get(self, request, *args, **kwargs): """ - Lists all posts from a member + Lists all posts from a current user. --- - parameters: - name: page description: Restricts output to the given page number. @@ -486,6 +540,10 @@ def get(self, request, *args, **kwargs): Gets a post given by its identifier. --- responseMessages: + - code: 401 + message: Not Authenticated + - code: 403 + message: Permission Denied - code: 404 message: Not Found """ @@ -503,7 +561,10 @@ def put(self, request, *args, **kwargs): description: Bearer token to make an authenticated request. required: true paramType: header - # TODO doc manquante + - name: text + description: Content of the post in markdown. + required: true + paramType: form responseMessages: - code: 400 message: Bad Request if you specify a bad identifier @@ -547,27 +608,33 @@ def post(self, request, *args, **kwargs): required: true paramType: header - name: text - description: Content of the post in markdown. + description: Content of the alert in markdown. required: true paramType: form - # TODO doc manquante responseMessages: - code: 400 message: Bad Request - code: 401 message: Not Authenticated + - code: 403 + message: Permission Denied + - code: 404 + message: Not Found """ author = request.user - post = Post.objects.get(id = self.kwargs.get('pk')) + try: + post = Post.objects.get(id = self.kwargs.get('pk')) + except Post.DoesNotExist: + raise Http404("Post with pk {} was not found".format(self.kwargs.get('pk'))) serializer = self.get_serializer_class()(data=request.data, context={'request': self.request}) serializer.is_valid(raise_exception=True) - serializer.save(comment=post) + serializer.save(comment=post, pubdate = datetime.datetime.now(), author = author) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def get_permissions(self): - permission_classes = [AllowAny, IsAuthenticated] + permission_classes = [AllowAny, IsAuthenticated, CanReadPost] return [permission() for permission in permission_classes] def get_serializer_class(self): @@ -575,8 +642,6 @@ def get_serializer_class(self): -# TODO global identier quand masquer les messages -# TODO gerer l'antispam -# TODO alerter un post A tester -# TODO editer un post A tester -# TODO empecher les ls et les ban de faire des alertes ? +# TODO global identier quand masquer les messages (message modéré ou masqué par son auteur) +# TODO gérer l'antispam +# TODO empecher les ls et les ban de faire des alertes diff --git a/zds/utils/validators.py b/zds/utils/validators.py new file mode 100644 index 0000000000..eded1fa5eb --- /dev/null +++ b/zds/utils/validators.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +import logging + +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext_lazy as _ +from zds.api.validators import Validator +from zds.member.models import Profile +from crispy_forms.bootstrap import StrictButton +from crispy_forms.layout import Layout, ButtonHolder, Field, Div, HTML +from django.utils.translation import ugettext_lazy as _ +from zds.utils.models import Tag +from zds.utils.misc import contains_utf8mb4 + +class TitleValidator(Validator): + """ + Validates title field of a Comment. + """ + + def validate_title(self, value): + """ + Checks about title. + + :param value: title value + :return: title value + """ + msg = None + if value: + if value.strip() == '': + msg = _(u'Le champ titre ne peut être vide.') + if msg is not None: + self.throw_error('title', msg) + return value + + +class TextValidator(Validator): + """ + Validates text field of a MP. + """ + + def validate_text(self, value): + """ + Checks about text. + + :param value: text value + :return: text value + """ + msg = None + if value: + if value.strip() == '': + msg = _(u'Le champ text ne peut être vide.') + if msg is not None: + self.throw_error('text', msg) + return value + + +class TagValidator(object): + """ + validate tags + """ + def __init__(self): + self.__errors = [] + self.logger = logging.getLogger("zds.utils.forms") + self.__clean = [] + + def validate_raw_string(self, raw_string): + """ + validate a string composed as ``tag1,tag2``. + + :param raw_string: the string to be validate. If ``None`` this is considered as a empty str + :type raw_string: basestring + :return: ``True`` if ``raw_string`` is fully valid, ``False`` if at least one error appears. See ``self.errors`` + to get all internationalized error. + """ + if raw_string is None or not isinstance(raw_string, basestring): + return self.validate_string_list([]) + return self.validate_string_list(raw_string.split(",")) + + def validate_length(self, tag): + """ + Check the length is in correct range. See ``Tag.label`` max length to have the true upper bound. + + :param tag: the tag lavel to validate + :return: ``True`` if length is valid + """ + if len(tag) > Tag._meta.get_field("title").max_length: + self.errors.append(_(u"Le tag {} est trop long (maximum {} caractères)".format( + tag, Tag._meta.get_field("title").max_length))) + self.logger.debug("%s est trop long expected=%d got=%d", tag, + Tag._meta.get_field("title").max_length, len(tag)) + return False + return True + + def validate_string_list(self, string_list): + """ + Same as ``validate_raw_string`` but with a list of tag labels. + + :param string_list: + :return: ``True`` if ``v`` is fully valid, ``False`` if at least one error appears. See ``self.errors`` + to get all internationalized error. + """ + self.__clean = list(filter(self.validate_length, string_list)) + self.__clean = list(filter(self.validate_utf8mb4, self.__clean)) + return len(string_list) == len(self.__clean) + + def validate_utf8mb4(self, tag): + """ + Checks the tag does not contain utf8mb4 chars. + + :param tag: + :return: ``True`` if no utf8mb4 string is found + """ + if contains_utf8mb4(tag): + self.errors.append(_(u"Le tag {} contient des caractères utf8mb4").format(tag)) + self.logger.warn("%s contains utf8mb4 char", tag) + return False + return True + + @property + def errors(self): + return self.__errors From 615f5d434efad064012175f13be53ab0654f9aac Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 3 Jan 2017 07:36:11 +0000 Subject: [PATCH 09/78] tests --- zds/forum/api/permissions.py | 26 ++++++- zds/forum/api/serializer.py | 27 +++++++- zds/forum/api/tests.py | 127 +++++++++++------------------------ zds/forum/api/views.py | 21 +++--- 4 files changed, 101 insertions(+), 100 deletions(-) diff --git a/zds/forum/api/permissions.py b/zds/forum/api/permissions.py index fb95de6879..9247c1e8c8 100644 --- a/zds/forum/api/permissions.py +++ b/zds/forum/api/permissions.py @@ -11,4 +11,28 @@ class IsStaffUser(permissions.BasePermission): """ def has_permission(self, request, view): - return request.user and request.user.has_perm("forum.create_forum") + return request.user and request.user.is_staff + + +class IsOwnerOrIsStaff(permissions.BasePermission): + """ + Allows access only to staff users or object owner. + """ + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + # Write permissions are only allowed to the owner of the snippet + if hasattr(obj, 'user'): + author = obj.user + elif hasattr(obj, 'author'): + author = obj.author + else: + author = AnonymousUser() + + print(request.user) + print(request.user.has_perm("forum.change_topic")) + return (author == request.user) or (request.user.is_staff) \ No newline at end of file diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index 1a8519a95e..f057187d6b 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -53,7 +53,7 @@ class TopicUpdateSerializer(serializers.ModelSerializer, TitleValidator, TextVal """ Serializer to update a topic. """ - title = serializers.CharField(required=False, allow_blank=True) + title = serializers.CharField(required=False, allow_blank=False) subtitle = serializers.CharField(required=False, allow_blank=True) permissions = DRYPermissionsField() @@ -61,7 +61,30 @@ class Meta: model = Topic fields = ('id', 'title', 'subtitle', 'permissions', 'forum', 'is_locked', 'is_solved', 'tags') read_only_fields = ('id', 'permissions', 'forum', 'is_locked') -# Todo : lors de l'update on a le droit de mettre un titre vide ? + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + + def throw_error(self, key=None, message=None): + raise serializers.ValidationError(message) + +class TopicUpdateStaffSerializer(serializers.ModelSerializer, TitleValidator, TextValidator): + """ + Serializer to update a topic by a staff member (extra rights). + """ + #title = serializers.CharField(required=True) + #subtitle = serializers.CharField(required=False, allow_blank=True) + permissions = DRYPermissions + + + class Meta: + model = Topic + fields = ('id', 'title', 'subtitle', 'permissions', 'forum', 'is_locked', 'is_solved', 'tags') + read_only_fields = ('id', 'permissions') + def update(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 3d3138c01f..3e4b920378 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -546,7 +546,7 @@ def test_list_of_topics_with_forum_filter(self): """ self.create_multiple_forums_with_topics(1) forum = Forum.objects.all().first() - response = self.client.get(reverse('api:forum:list-topic') + '?forum=' + forum.id) + response = self.client.get(reverse('api:forum:list-topic') + '?forum=' + str(forum.id)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) @@ -769,91 +769,7 @@ def test_details_topic_private(self): response = self.client_authenticated_staff.get(reverse('api:forum:detail-topic', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_new_post_anonymous(self): - """ - Try to post a new post with anonymous user. - """ - topic = self.create_multiple_forums_with_topics(1, 1) - data = { - 'text': 'I head that Flask is the best framework ever, is that true ?' - } - - self.client = APIClient() - response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_new_post_user(self): - """ - Try to post a new post with an user. - """ - topic = self.create_multiple_forums_with_topics(1, 1) - data = { - 'text': 'I head that Flask is the best framework ever, is that true ?' - } - - self.client = APIClient() - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) - topic = Topic.objects.filter(id=topic.id).first() - print(topic) - last_message = topic.get_last_post() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data.get('text'), last_message.text) - - def test_new_post_user_with_restrictions(self): - """ - Try to post a new post with an user that has some restrictions . - """ - profile = ProfileFactory() - profile.can_read = False - profile.can_write = False - profile.save() - topic = self.create_multiple_forums_with_topics(1, 1) - data = { - 'text': 'I head that Flask is the best framework ever, is that true ?' - } - - self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) - response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - profile = ProfileFactory() - profile.can_write = False - profile.save() - - self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) - response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_new_post_no_text(self): - """ - Try to post a new post without a text. - """ - topic = self.create_multiple_forums_with_topics(1, 1) - data = {} - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id,]), data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_new_post_empty_text(self): - """ - Try to post a new post with an empty text. - """ - topic = self.create_multiple_forums_with_topics(1, 1) - data = { - 'text': '' - } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id,]), data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_new_post_unknown_topic(self): - """ - Try to post a new post in a topic that does not exists. - """ - data = { - 'text': 'Where should I go now ?' - } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[666,]), data) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - + # Édite un sujet qvec user en ls # Édite un sujet avec user banni # Editer dans un forum privé ? Verifier les auths @@ -1266,13 +1182,13 @@ def test_list_of_user_topics_anonymous(self): # DONE Créer un message avec un contenu vide # DONECréer un message dans un sujet qui n'existe pas # DONE Créer un message en anonymous -# Créer un message dans un sujet qui en contient deja # DONE Créer un message dans un forum privé en user # DONE Créer un message dans un forum privé en staff # Créer un message dans un sujet fermé en user # Créer un message dans un sujet fermé en staff # Créer un message pour tester l'antiflood + def test_create_post_with_no_field(self): """ Creates a post in a topic but not with according field. @@ -1296,8 +1212,12 @@ def test_create_post_unauthenticated(self): """ Creates a post in a topic with unauthenticated client. """ + data = { + 'text': 'Welcome to this post!' + } + topic = self.create_multiple_forums_with_topics(1, 1) self.client = APIClient() - response = self.client.post(reverse('api:forum:list-post', args=[0]), {}) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_create_post_with_bad_topic_id(self): @@ -1337,6 +1257,7 @@ def test_failure_post_in_a_forum_we_cannot_read(self): """ Tries to create a post in a private topic with a normal user. """ + print('--------------------------- test rate--------------') profile = StaffProfileFactory() category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) @@ -1366,6 +1287,36 @@ def test_post_in_a_private_forum(self): self.assertEqual(response.data.get('author'), post.author) self.assertEqual(response.data.get('position'), post.position) self.assertEqual(response.data.get('pubdate'), post.pubdate) + + + + def test_new_post_user_with_restrictions(self): + """ + Try to post a new post with an user that has some restrictions . + """ + + # Banned + profile = ProfileFactory() + profile.can_read = False + profile.can_write = False + profile.save() + topic = self.create_multiple_forums_with_topics(1, 1) + data = { + 'text': 'I head that Flask is the best framework ever, is that true ?' + } + + self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Read only + profile = ProfileFactory() + profile.can_write = False + profile.save() + + self.assertTrue(self.client.login(username=profile.user.username, password='hostel77')) + response = self.client.post(reverse('api:forum:list-post', args=[topic.id,]), data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_detail_post(self): """ diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 730e382fce..4ef3e7e940 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -19,8 +19,8 @@ from dry_rest_permissions.generics import DRYPermissions from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit from zds.utils import slugify -from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicCreateSerializer, TopicUpdateSerializer, PostSerializer, PostCreateSerializer, PostUpdateSerializer, AlertSerializer -from zds.forum.api.permissions import IsStaffUser +from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicCreateSerializer, TopicUpdateSerializer, TopicUpdateStaffSerializer, PostSerializer, PostCreateSerializer, PostUpdateSerializer, AlertSerializer +from zds.forum.api.permissions import IsStaffUser, IsOwnerOrIsStaff from zds.member.models import User from itertools import chain @@ -343,15 +343,18 @@ def get_serializer_class(self): if self.request.method == 'GET': return TopicSerializer elif self.request.method == 'PUT': - return TopicUpdateSerializer + if self.request.user.has_perm("forum.change_topic"): # TODO vérifier la permission ? + return TopicUpdateStaffSerializer + else: + return TopicUpdateSerializer def get_permissions(self): permission_classes = [] if self.request.method == 'GET': permission_classes.append(CanReadTopic) elif self.request.method == 'PUT': - permission_classes.append(IsAuthenticatedOrReadOnly) - permission_classes.append(IsOwnerOrReadOnly) + print(self.request.user) + permission_classes.append(IsOwnerOrIsStaff) # TODO a remplacer par IsOwnerOrIsStaff à écrire permission_classes.append(CanReadTopic) return [permission() for permission in permission_classes] @@ -440,10 +443,11 @@ def get_queryset(self): def get_current_user(self): return self.request.user.profile + # TODO rien pour le get ici ? def get_permissions(self): - permission_classes = [AllowAny, CanReadPost, CanReadTopic, CanReadForum, CanReadPost] + print('lllllll') + permission_classes = [CanReadPost] if self.request.method == 'POST': - permission_classes.append(IsAuthenticated) permission_classes.append(CanReadAndWriteNowOrReadOnly) return [permission() for permission in permission_classes] @@ -586,8 +590,7 @@ def get_serializer_class(self): def get_permissions(self): permission_classes = [AllowAny, CanReadPost] if self.request.method == 'PUT': - permission_classes.append(IsAuthenticated) - permission_classes.append(IsNotOwnerOrReadOnly) + permission_classes.append(IsOwnerOrIsStaff) permission_classes.append(CanReadAndWriteNowOrReadOnly) return [permission() for permission in permission_classes] From fa17b7669cd3970780f2b92d7f6d28a5a9bad93c Mon Sep 17 00:00:00 2001 From: Antonin Date: Wed, 4 Jan 2017 10:51:59 +0000 Subject: [PATCH 10/78] fixing test --- zds/forum/api/permissions.py | 27 ++++++++++++++++++++++++++- zds/forum/api/serializer.py | 9 +++++---- zds/forum/api/tests.py | 32 +++++++++++++------------------- zds/forum/api/views.py | 17 ++++++++++------- zds/member/api/permissions.py | 4 +++- 5 files changed, 57 insertions(+), 32 deletions(-) diff --git a/zds/forum/api/permissions.py b/zds/forum/api/permissions.py index 9247c1e8c8..8d067d2e2b 100644 --- a/zds/forum/api/permissions.py +++ b/zds/forum/api/permissions.py @@ -2,6 +2,7 @@ from rest_framework import permissions from django.contrib.auth.models import AnonymousUser +from zds.forum.models import Forum @@ -34,5 +35,29 @@ def has_object_permission(self, request, view, obj): author = AnonymousUser() print(request.user) + print(author) print(request.user.has_perm("forum.change_topic")) - return (author == request.user) or (request.user.is_staff) \ No newline at end of file + return (author == request.user) or (request.user.has_perm("forum.change_topic")) + +class CanWriteInForum(permissions.BasePermission): + """ + Allows access only to people that can write in this forum. + """ + + def has_permission(self, request, view): + print('can write forum') + print(request.data) + forum = Forum.objects.get(id=request.data.get('forum')) # TODO tester si on met un id qui n'existe pas + return forum.can_read(request.user) + +class CanWriteInTopic(permissions.BasePermission): + """ + Allows access only to people that can write in this topic. + """ + + def has_permission(self, request, view): + print('can write topic') + #print(request.data) + #topic = Forum.objects.get(id=request.data.get('forum')) # TODO tester si on met un id qui n'existe pas + #forum.can_read(request.user) + return True \ No newline at end of file diff --git a/zds/forum/api/serializer.py b/zds/forum/api/serializer.py index f057187d6b..47e1a03810 100644 --- a/zds/forum/api/serializer.py +++ b/zds/forum/api/serializer.py @@ -75,9 +75,10 @@ class TopicUpdateStaffSerializer(serializers.ModelSerializer, TitleValidator, Te """ Serializer to update a topic by a staff member (extra rights). """ - #title = serializers.CharField(required=True) + title = serializers.CharField(required=False) + forum = serializers.PrimaryKeyRelatedField(queryset=Forum.objects.all(), required=False) #subtitle = serializers.CharField(required=False, allow_blank=True) - permissions = DRYPermissions + permissions = DRYPermissionsField() class Meta: @@ -109,8 +110,8 @@ class PostCreateSerializer(serializers.ModelSerializer, TextValidator): class Meta: model = Post - fields = ('id', 'text', 'text_html', 'permissions') - read_only_fields = ('text_html', 'permissions') + fields = ('id', 'text', 'text_html', 'permissions', 'is_useful', 'author', 'position', 'pubdate') + read_only_fields = ('text_html', 'permissions', 'is_useful', 'author', 'position', 'pubdate') # TODO a voir quel champ en read only def create(self, validated_data): diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 3e4b920378..b2b1adbd79 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -1215,8 +1215,8 @@ def test_create_post_unauthenticated(self): data = { 'text': 'Welcome to this post!' } - topic = self.create_multiple_forums_with_topics(1, 1) self.client = APIClient() + topic = self.create_multiple_forums_with_topics(1, 1) response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -1241,17 +1241,13 @@ def test_create_post(self): response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - post = topic.get_last_answer() - print('test_create_post') - print(response) - print(response.data) - print('end test create post') + post = Post.objects.filter(topic=topic.id).last() + self.assertEqual(response.data.get('text'), data.get('text')) self.assertEqual(response.data.get('text'), post.text) - self.assertEqual(response.data.get('is_userful'), post.is_userful) - self.assertEqual(response.data.get('author'), post.author) + self.assertEqual(response.data.get('is_useful'), post.is_useful) + self.assertEqual(response.data.get('author'), post.author.id) self.assertEqual(response.data.get('position'), post.position) - self.assertEqual(response.data.get('pubdate'), post.pubdate) def test_failure_post_in_a_forum_we_cannot_read(self): """ @@ -1280,16 +1276,13 @@ def test_post_in_a_private_forum(self): response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - post = topic.get_last_answer() + post = Post.objects.filter(topic=topic.id).last() self.assertEqual(response.data.get('text'), data.get('text')) self.assertEqual(response.data.get('text'), post.text) - self.assertEqual(response.data.get('is_userful'), post.is_userful) - self.assertEqual(response.data.get('author'), post.author) + self.assertEqual(response.data.get('is_useful'), post.is_useful) + self.assertEqual(response.data.get('author'), post.author.id) self.assertEqual(response.data.get('position'), post.position) - self.assertEqual(response.data.get('pubdate'), post.pubdate) - - def test_new_post_user_with_restrictions(self): """ Try to post a new post with an user that has some restrictions . @@ -1588,6 +1581,7 @@ def test_update_post_other_user(self): 'text': 'I made an error I want to edit.' } topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, self.profile) + print('test_update_post_other_user') response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -1600,7 +1594,8 @@ def test_update_post(self): } topic, posts = self.create_topic_with_post(1, self.profile) response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('text'), data.get('text')) def test_update_post_staff(self): """ @@ -1670,8 +1665,7 @@ def authenticate_client(client, client_auth, username, password): # TODO # Reorganiser le code de test en differentes classes, reordonner les tests -# Voir où on a besoin de read only -# Voir où a besoin de validator # Vérifier que l'on affiche pas le text hidden ou l'adresse ip # Créer un topic avec des tags (ajouter le test) -# Tester le cas ou un gars veux vider le contenu de ses messages \ No newline at end of file +# Tester le cas ou un gars veux vider le contenu de ses messages +# Ajouter le cas ou le staff ou un user masque son message \ No newline at end of file diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 4ef3e7e940..f4c48eaf16 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -20,7 +20,7 @@ from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit from zds.utils import slugify from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicCreateSerializer, TopicUpdateSerializer, TopicUpdateStaffSerializer, PostSerializer, PostCreateSerializer, PostUpdateSerializer, AlertSerializer -from zds.forum.api.permissions import IsStaffUser, IsOwnerOrIsStaff +from zds.forum.api.permissions import IsStaffUser, IsOwnerOrIsStaff, CanWriteInForum from zds.member.models import User from itertools import chain @@ -219,12 +219,15 @@ def get_serializer_class(self): return TopicCreateSerializer def get_permissions(self): - permission_classes = [AllowAny, ] + permission_classes = [CanReadForum] if self.request.method == 'POST': - permission_classes.append(IsAuthenticated) + print('requete post') + + #forum = Forum.objects.get(id=self.request.data.get('forum')) + #self.check_object_permissions(self.request, forum) + #permission_classes.append(CanReadAndWriteNowOrReadOnly) + permission_classes.append(CanWriteInForum) permission_classes.append(CanReadAndWriteNowOrReadOnly) - permission_classes.append(CanReadTopic) - permission_classes.append(CanReadForum) return [permission() for permission in permission_classes] @@ -354,7 +357,7 @@ def get_permissions(self): permission_classes.append(CanReadTopic) elif self.request.method == 'PUT': print(self.request.user) - permission_classes.append(IsOwnerOrIsStaff) # TODO a remplacer par IsOwnerOrIsStaff à écrire + permission_classes.append(IsOwnerOrIsStaff) permission_classes.append(CanReadTopic) return [permission() for permission in permission_classes] @@ -637,7 +640,7 @@ def post(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def get_permissions(self): - permission_classes = [AllowAny, IsAuthenticated, CanReadPost] + permission_classes = [CanReadPost, IsAuthenticated] return [permission() for permission in permission_classes] def get_serializer_class(self): diff --git a/zds/member/api/permissions.py b/zds/member/api/permissions.py index 4bd14e6656..ead9a9fca8 100644 --- a/zds/member/api/permissions.py +++ b/zds/member/api/permissions.py @@ -3,7 +3,6 @@ from rest_framework import permissions from django.contrib.auth.models import AnonymousUser - class IsOwnerOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object to edit it. @@ -86,6 +85,8 @@ class CanReadTopic(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): + print('can read topic') + print(obj.forum.can_read(request.user)) return obj.forum.can_read(request.user) @@ -95,6 +96,7 @@ class CanReadForum(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): + print('can read forum object') return obj.can_read(request.user) class CanReadPost(permissions.BasePermission): From 4ecf63cf3bedc21a1d87893a68d2b0211882a35f Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 5 Jan 2017 07:46:58 +0000 Subject: [PATCH 11/78] testing' --- zds/forum/api/permissions.py | 24 +++++++++++++++--------- zds/forum/api/tests.py | 23 ++++++++++++++--------- zds/forum/api/views.py | 8 ++++---- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/zds/forum/api/permissions.py b/zds/forum/api/permissions.py index 8d067d2e2b..e5798002d3 100644 --- a/zds/forum/api/permissions.py +++ b/zds/forum/api/permissions.py @@ -2,8 +2,8 @@ from rest_framework import permissions from django.contrib.auth.models import AnonymousUser -from zds.forum.models import Forum - +from zds.forum.models import Forum, Topic +from django.http import Http404 class IsStaffUser(permissions.BasePermission): @@ -41,23 +41,29 @@ def has_object_permission(self, request, view, obj): class CanWriteInForum(permissions.BasePermission): """ - Allows access only to people that can write in this forum. + Allows access only to people that can write in forum passed by post request. """ def has_permission(self, request, view): print('can write forum') print(request.data) - forum = Forum.objects.get(id=request.data.get('forum')) # TODO tester si on met un id qui n'existe pas + try: + forum = Forum.objects.get(id=request.data.get('forum')) # TODO tester si on met un id qui n'existe pas + except Forum.DoesNotExist: + raise Http404("Forum with pk {} was not found".format(request.data.get('forum'))) + return forum.can_read(request.user) class CanWriteInTopic(permissions.BasePermission): """ - Allows access only to people that can write in this topic. + Allows access only to people that can write in topic passed by url. """ def has_permission(self, request, view): print('can write topic') - #print(request.data) - #topic = Forum.objects.get(id=request.data.get('forum')) # TODO tester si on met un id qui n'existe pas - #forum.can_read(request.user) - return True \ No newline at end of file + topic_pk = request.resolver_match.kwargs.get('pk') + try: + topic = Topic.objects.get(id=topic_pk) # TODO tester si on met un id qui n'existe pas + except Topic.DoesNotExist: + raise Http404("Topic with pk {} was not found".format(topic_pk)) + return topic.forum.can_read(request.user) \ No newline at end of file diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index b2b1adbd79..533cf36239 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -4,6 +4,7 @@ from django.core.cache import caches from django.core.urlresolvers import reverse from django.contrib.auth.models import Group +from django.db import transaction from rest_framework import status from rest_framework.test import APIClient from rest_framework.test import APITestCase @@ -1215,9 +1216,11 @@ def test_create_post_unauthenticated(self): data = { 'text': 'Welcome to this post!' } - self.client = APIClient() + topic = self.create_multiple_forums_with_topics(1, 1) - response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) + self.client = APIClient() + with transaction.atomic(): + response = self.client.post(reverse('api:forum:list-post', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_create_post_with_bad_topic_id(self): @@ -1269,11 +1272,11 @@ def test_post_in_a_private_forum(self): """ category, forum = create_category(self.group_staff) - topic = add_topic_in_a_forum(forum, self.profile) + topic = add_topic_in_a_forum(forum, self.staff) data = { 'text': 'Welcome to this post!' } - response = self.client_authenticated.post(reverse('api:forum:list-post', args=[topic.pk,]), data) + response = self.client_authenticated_staff.post(reverse('api:forum:list-post', args=[topic.pk,]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) post = Post.objects.filter(topic=topic.id).last() @@ -1411,8 +1414,7 @@ def test_list_of_member_posts_with_several_pages(self): """ Gets list of a member topics with several pages in the database. """ - # When we create a Topic a post is also added. - self.create_multiple_forums_with_topics(REST_PAGE_SIZE, 1, self.profile) + self.create_multiple_forums_with_topics(REST_PAGE_SIZE + 1, 1, self.profile) response = self.client_authenticated.get(reverse('api:forum:list-memberpost', args=[self.profile.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1580,7 +1582,8 @@ def test_update_post_other_user(self): data = { 'text': 'I made an error I want to edit.' } - topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, self.profile) + another_profile = ProfileFactory() + topic, posts = self.create_topic_with_post(REST_PAGE_SIZE, another_profile) print('test_update_post_other_user') response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -1642,7 +1645,7 @@ def test_update_post_in_private_topic(self): # With staff (member of private forum) response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(data.get('text'), response.get('text')) + self.assertEqual(data.get('text'), response.data.get('text')) def create_oauth2_client(user): client = Application.objects.create(user=user, @@ -1668,4 +1671,6 @@ def authenticate_client(client, client_auth, username, password): # Vérifier que l'on affiche pas le text hidden ou l'adresse ip # Créer un topic avec des tags (ajouter le test) # Tester le cas ou un gars veux vider le contenu de ses messages -# Ajouter le cas ou le staff ou un user masque son message \ No newline at end of file +# Ajouter le cas ou le staff ou un user masque son message, mais un autre user ne peut pas le faire +# Poste dans un sujet fermé (3 roles) +# Gérer le champ update (date) \ No newline at end of file diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index f4c48eaf16..4475cecc11 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -20,7 +20,7 @@ from zds.api.bits import DJRF3xPaginationKeyBit, UpdatedAtKeyBit from zds.utils import slugify from zds.forum.api.serializer import ForumSerializer, TopicSerializer, TopicCreateSerializer, TopicUpdateSerializer, TopicUpdateStaffSerializer, PostSerializer, PostCreateSerializer, PostUpdateSerializer, AlertSerializer -from zds.forum.api.permissions import IsStaffUser, IsOwnerOrIsStaff, CanWriteInForum +from zds.forum.api.permissions import IsStaffUser, IsOwnerOrIsStaff, CanWriteInForum, CanWriteInTopic from zds.member.models import User from itertools import chain @@ -346,7 +346,7 @@ def get_serializer_class(self): if self.request.method == 'GET': return TopicSerializer elif self.request.method == 'PUT': - if self.request.user.has_perm("forum.change_topic"): # TODO vérifier la permission ? + if self.request.user.has_perm("forum.change_topic"): return TopicUpdateStaffSerializer else: return TopicUpdateSerializer @@ -446,12 +446,12 @@ def get_queryset(self): def get_current_user(self): return self.request.user.profile - # TODO rien pour le get ici ? def get_permissions(self): - print('lllllll') permission_classes = [CanReadPost] if self.request.method == 'POST': permission_classes.append(CanReadAndWriteNowOrReadOnly) + permission_classes.append(CanWriteInTopic) + return [permission() for permission in permission_classes] From 89eda18a9d84350725873f5205b7ce9daa189fe3 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 5 Jan 2017 20:58:49 +0000 Subject: [PATCH 12/78] Remove useless code, update todo list, style --- zds/forum/api/tests.py | 104 +++++++++++++++++++++------------------- zds/forum/api/views.py | 10 +--- zds/member/factories.py | 57 ---------------------- zds/utils/validators.py | 11 ++--- 4 files changed, 61 insertions(+), 121 deletions(-) diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 533cf36239..b400ec8779 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -221,7 +221,7 @@ def setUp(self): client_oauth2 = create_oauth2_client(self.staff.user) self.client_authenticated_staff = APIClient() authenticate_client(self.client_authenticated_staff, client_oauth2, self.staff.user.username, 'hostel77') - + self.group_staff = Group.objects.filter(name="staff").first() caches[extensions_api_settings.DEFAULT_USE_CACHE].clear() @@ -246,7 +246,7 @@ def create_topic_with_post(self, number_of_post=REST_PAGE_SIZE, profile=None): category, forum = create_category() new_topic = add_topic_in_a_forum(forum, profile) - posts=[] + posts = [] for post in xrange(0, number_of_post): posts.append(PostFactory(topic=new_topic, author=profile.user, position=2)) @@ -276,7 +276,7 @@ def test_list_of_forums(self): self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) - + def test_list_of_forums_private(self): """ Gets list of private forums not empty in the database (only for staff). @@ -286,15 +286,15 @@ def test_list_of_forums_private(self): self.client = APIClient() response = self.client.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('count'), 0) response = self.client_authenticated.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('count'), 0) + self.assertEqual(response.data.get('count'), 0) response = self.client_authenticated_staff.get(reverse('api:forum:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('count'), 1) # TODO nombre a affiner en fonction de la realite + self.assertEqual(response.data.get('count'), 1) # TODO nombre a affiner en fonction de la realite def test_list_of_forums_with_several_pages(self): @@ -382,14 +382,14 @@ def test_details_forum(self): self.assertEqual(response.data.get('slug'), forum.slug) self.assertEqual(response.data.get('category'), forum.category.id) self.assertEqual(response.data.get('position_in_category'), forum.position_in_category) - + print('-------') print(type(response.data.get('group'))) print(type(list(forum.group.all()))) print('-------') self.assertEqual(response.data.get('group'), list(forum.group.all())) - + def test_details_forum_private(self): """ Tries to get the details of a private forum with different users. @@ -399,7 +399,7 @@ def test_details_forum_private(self): self.client = APIClient() response = self.client.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + response = self.client_authenticated.get(reverse('api:forum:detail', args=[forum.id])) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -540,7 +540,7 @@ def test_list_of_topics_with_forum_filter_empty(self): self.assertEqual(response.data.get('results'), []) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) - + def test_list_of_topics_with_forum_filter(self): """ Gets a list of topics in a forum. @@ -568,12 +568,12 @@ def test_list_of_topics_with_author_filter(self): """ Gets a list of topics created by an user. """ - self.create_multiple_forums_with_topics(1,REST_PAGE_SIZE,self.profile) + self.create_multiple_forums_with_topics(1, REST_PAGE_SIZE, self.profile) response = self.client.get(reverse('api:forum:list-topic') + '?author=' + str(self.profile.user.id)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('count'), REST_PAGE_SIZE) self.assertEqual(len(response.data.get('results')), REST_PAGE_SIZE) - + def test_list_of_topics_with_tag_filter_empty(self): """ Gets an empty list of topics with a specific tag. @@ -770,7 +770,7 @@ def test_details_topic_private(self): response = self.client_authenticated_staff.get(reverse('api:forum:detail-topic', args=[topic.id])) self.assertEqual(response.status_code, status.HTTP_200_OK) - + # Édite un sujet qvec user en ls # Édite un sujet avec user banni # Editer dans un forum privé ? Verifier les auths @@ -789,7 +789,7 @@ def test_update_topic_details_title(self): print(response) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('title'), data.get('title')) - + def test_update_topic_details_title_empty(self): """ Updates title of a topic, tries to put an empty sting @@ -869,69 +869,69 @@ def test_update_topic_forum(self): profile = ProfileFactory() self.create_multiple_forums_with_topics(5, 1, profile) topic = Topic.objects.filter(forum=1).first() - + # Anonymous self.client = APIClient() response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + # User response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # Staff + + # Staff response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('forum'), data.get('forum')) - + def test_update_topic_lock(self): """ Tries to lock a Topic with different users. """ data = { - 'is_locked': True + 'is_locked': True } topic = self.create_multiple_forums_with_topics(1, 1) - + self.client = APIClient() response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data.get('is_locked')) - + def test_update_topic_solve(self): """ Tries to solve a Topic with different users. """ data = { - 'is_solved': True + 'is_solved': True } topic = self.create_multiple_forums_with_topics(1, 1, self.profile) - + self.client = APIClient() response = self.client.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - # Author + + # Author response = self.client_authenticated.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data.get('is_solved')) - + # Other user other_profile = ProfileFactory() client_oauth2 = create_oauth2_client(other_profile.user) client_other_user = APIClient() authenticate_client(client_other_user, client_oauth2, other_profile.user.username, 'hostel77') - + response = client_other_user.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # Staff + + # Staff response = self.client_authenticated_staff.put(reverse('api:forum:detail-topic', args=[topic.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data.get('is_solved')) @@ -1060,7 +1060,7 @@ def test_list_of_posts_in_topic_with_a_wrong_custom_page_size(self): response = self.client.get(reverse('api:forum:list-post', args=[topic.id]) + '?page_size={}'.format(page_size_value)) self.assertEqual(response.status_code, status.HTTP_200_OK) - + # We will have page_size_value + 1 because a post is added at topic creation. self.assertEqual(response.data.get('count'), page_size_value + 1) self.assertIsNotNone(response.data.get('next')) @@ -1285,12 +1285,12 @@ def test_post_in_a_private_forum(self): self.assertEqual(response.data.get('is_useful'), post.is_useful) self.assertEqual(response.data.get('author'), post.author.id) self.assertEqual(response.data.get('position'), post.position) - + def test_new_post_user_with_restrictions(self): """ Try to post a new post with an user that has some restrictions . """ - + # Banned profile = ProfileFactory() profile.can_read = False @@ -1385,7 +1385,7 @@ def test_list_of_member_posts(self): self.assertEqual(len(response.data.get('results')), 10) self.assertIsNone(response.data.get('next')) self.assertIsNone(response.data.get('previous')) - + def test_list_of_staff_posts(self): """ Gets list of a staff posts. @@ -1394,7 +1394,7 @@ def test_list_of_staff_posts(self): profile = StaffProfileFactory() category, forum = create_category(self.group_staff) topic = add_topic_in_a_forum(forum, profile) - + # Anonymous user cannot see staff private post. self.client = APIClient() response = self.client.get(reverse('api:forum:list-memberpost', args=[profile.id])) @@ -1508,7 +1508,7 @@ def test_alert_post(self): response = self.client_authenticated.post(reverse('api:forum:alert-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - + alerte = Alert.objects.latest('pubdate') self.assertEqual(alerte.text, data.get('text')) self.assertEqual(alerte.author, self.profile.user) @@ -1562,7 +1562,7 @@ def test_alert_post_not_found(self): # TODO - + def test_update_post_anonymous(self): """ Tries to update a post with anonymous user. @@ -1574,7 +1574,7 @@ def test_update_post_anonymous(self): topic, posts = self.create_topic_with_post() response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + def test_update_post_other_user(self): """ Tries to update a post with another user that the one who posted on the first time. @@ -1599,7 +1599,7 @@ def test_update_post(self): response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('text'), data.get('text')) - + def test_update_post_staff(self): """ Update a post with a staff user. @@ -1611,7 +1611,7 @@ def test_update_post_staff(self): response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, posts[0].id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('text'), data.get('text')) - + def test_update_unknow_post(self): """ Tries to update post that does not exist. @@ -1637,11 +1637,11 @@ def test_update_post_in_private_topic(self): # With anonymous response = self.client.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - + # With user response = self.client_authenticated.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + # With staff (member of private forum) response = self.client_authenticated_staff.put(reverse('api:forum:detail-post', args=[topic.id, post.id]), data) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1666,11 +1666,19 @@ def authenticate_client(client, client_auth, username, password): access_token = AccessToken.objects.get(user__username=username) client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) -# TODO +# TODO # Reorganiser le code de test en differentes classes, reordonner les tests +# Gérer le champ update (date) lors de l'edit +# Gérer l'antispam +# identifer quand masquer les messages (message modéré ou masqué par son auteur) +# empecher les ls et les ban de faire des alertes +# Gestion de tags (post/edit/details) +# Tests qui ne passent pas +# Style / PEP8 + +# TESTS MANQUANTS # Vérifier que l'on affiche pas le text hidden ou l'adresse ip -# Créer un topic avec des tags (ajouter le test) -# Tester le cas ou un gars veux vider le contenu de ses messages +# Créer un topic avec des tags (ajouter le test), éditer les tags +# Tester le cas ou un user veux vider le contenu de son message # Ajouter le cas ou le staff ou un user masque son message, mais un autre user ne peut pas le faire # Poste dans un sujet fermé (3 roles) -# Gérer le champ update (date) \ No newline at end of file diff --git a/zds/forum/api/views.py b/zds/forum/api/views.py index 4475cecc11..378dbe256b 100644 --- a/zds/forum/api/views.py +++ b/zds/forum/api/views.py @@ -196,7 +196,7 @@ def post(self, request, *args, **kwargs): description: To add a tag, specify its taf identifier. Specify this parameter several times to add several tags. required: false - paramType: form TODO vérifier que les tags fonctionnent + paramType: form responseMessages: - code: 400 message: Bad Request @@ -329,7 +329,7 @@ def put(self, request, *args, **kwargs): description: To add a tag, specify its taf identifier. Specify this parameter several times to add several tags. required: false - paramType: form TODO vérifier que les tags fonctionnent + paramType: form responseMessages: - code: 400 message: Bad Request if you specify a bad identifier @@ -645,9 +645,3 @@ def get_permissions(self): def get_serializer_class(self): return AlertSerializer - - - -# TODO global identier quand masquer les messages (message modéré ou masqué par son auteur) -# TODO gérer l'antispam -# TODO empecher les ls et les ban de faire des alertes diff --git a/zds/member/factories.py b/zds/member/factories.py index 4cc4222db9..179063c3d9 100644 --- a/zds/member/factories.py +++ b/zds/member/factories.py @@ -68,44 +68,6 @@ def _prepare(cls, create, **kwargs): return user -class AdminFactory(factory.DjangoModelFactory): - """ - This factory creates admin User. - WARNING: Don't try to directly use `AdminFactory`, this didn't create associated Profile then don't work! - Use `AdminProfileFactory` instead. - """ - class Meta: - model = User - - username = factory.Sequence('admin{0}'.format) - email = factory.Sequence('firmstaff{0}@zestedesavoir.com'.format) - password = 'hostel77' - - is_superuser = True - is_staff = True - is_active = True - - @classmethod - def _prepare(cls, create, **kwargs): - password = kwargs.pop('password', None) - user = super(AdminFactory, cls)._prepare(create, **kwargs) - if password: - user.set_password(password) - if create: - user.save() - group_staff = Group.objects.filter(name="staff").first() - if group_staff is None: - group_staff = Group(name="staff") - group_staff.save() - - perms = Permission.objects.all() - group_staff.permissions = perms - user.groups.add(group_staff) - - user.save() - return user - - class ProfileFactory(factory.DjangoModelFactory): """ Use this factory when you need a complete Profile for a standard user. @@ -151,25 +113,6 @@ def biography(self): sign = 'Please look my flavour' -class AdminProfileFactory(factory.DjangoModelFactory): - """ - Use this factory when you need a complete admin Profile for a user. - """ - class Meta: - model = Profile - - user = factory.SubFactory(AdminFactory) - - last_ip_address = '192.168.2.1' - site = 'www.zestedesavoir.com' - - @factory.lazy_attribute - def biography(self): - return u'My name is {0} and I i\'m the guy who control the guy that kill the bad guys '.format(self.user.username.lower()) - - sign = 'Please look my flavour' - - class NonAsciiUserFactory(UserFactory): """ This factory creates standard user with non-ASCII characters in its username. diff --git a/zds/utils/validators.py b/zds/utils/validators.py index eded1fa5eb..540aaae472 100644 --- a/zds/utils/validators.py +++ b/zds/utils/validators.py @@ -1,17 +1,12 @@ # -*- coding: utf-8 -*- import logging -from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from zds.api.validators import Validator -from zds.member.models import Profile -from crispy_forms.bootstrap import StrictButton -from crispy_forms.layout import Layout, ButtonHolder, Field, Div, HTML -from django.utils.translation import ugettext_lazy as _ from zds.utils.models import Tag from zds.utils.misc import contains_utf8mb4 + class TitleValidator(Validator): """ Validates title field of a Comment. @@ -31,8 +26,8 @@ def validate_title(self, value): if msg is not None: self.throw_error('title', msg) return value - - + + class TextValidator(Validator): """ Validates text field of a MP. From d209ad7d4dc8e72e1b8f91c9484dcaec7f52f096 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Thu, 5 Jan 2017 22:21:37 +0100 Subject: [PATCH 13/78] Update settings.py --- zds/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zds/settings.py b/zds/settings.py index af63907337..73b5bb720b 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -31,7 +31,6 @@ }, } -ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. From 405f17c93808d837d74f572ee4de35daeecaddd0 Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 10 Jan 2017 15:52:41 +0000 Subject: [PATCH 14/78] First POC --- Gulpfile.js | 3 ++ assets/js/auto-merge.js | 54 ++++++++++++++++++++++++++ assets/scss/main.scss | 3 ++ templates/tutorialv2/edit/content.html | 11 ++++++ zds/settings.py | 1 + zds/tutorialv2/forms.py | 24 ++++++++++-- zds/tutorialv2/views/views_contents.py | 8 +++- 7 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 assets/js/auto-merge.js diff --git a/Gulpfile.js b/Gulpfile.js index a699cd1fc8..d7bd4e9d8c 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -45,6 +45,8 @@ gulp.task('js', () => gulp.src([ require.resolve('jquery'), require.resolve('cookies-eu-banner'), + require.resolve('codemirror'), + require.resolve('mergely'), 'assets/js/_custom.modernizr.js', // Used by other scripts, must be first @@ -55,6 +57,7 @@ gulp.task('js', () => 'assets/js/accordeon.js', 'assets/js/ajax-actions.js', 'assets/js/autocompletion.js', + 'assets/js/auto-merge.js', 'assets/js/close-alert-box.js', 'assets/js/compare-commits.js', 'assets/js/data-click.js', diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js new file mode 100644 index 0000000000..b9528e6af7 --- /dev/null +++ b/assets/js/auto-merge.js @@ -0,0 +1,54 @@ + + +$(document).ready(function () { + console.log('oooooo'); + + $('#compare').mergely({ + width: 'auto', + height: 400, + sidebar: false, + cmsettings: { readOnly: false, lineNumbers: true }, + lhs: function(setValue) { + setValue($("#your_introduction").html()); + }, + rhs: function(setValue) { + setValue($("#id_introduction").text()); + } + }); + + $("#compare-editor-lhs").append('Votre Version'); + $("#compare-editor-rhs").append('La version courante'); + + + + /** + * Merge introduction + */ + $(".merge-btn").on("click", function(e){ + console.log('click'); + e.stopPropagation(); + e.preventDefault(); + // var $form = $(this).parents("form:first"); + var button = $(this); + + console.log(button); + + var classList = button.attr('class').split(/\s+/); + console.log(classList); + for (var i = 0; i < classList.length; i++) { + // Pour un POC on pourrait faire plus simple, mais je m'intéresse au cas ou il y aura plusieurs + // boutons par page, voila un moyen de les différencier. + // on pourrait probablement automatiser ça avec une regex ou autre + if (classList[i] === 'need-to-merge-introduction') { + $intro = $("#id_introduction"); + $to_merge = $("#compare").mergely('get','rhs'); + $intro.val($to_merge); + + console.log($intro.val()); + // TODO : un bon petit alert des familles + alert('Contenu bien mergé'); + } + } + }); + +}); diff --git a/assets/scss/main.scss b/assets/scss/main.scss index e6ea2b9311..00add6f198 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -58,13 +58,16 @@ @import "components/alert-box"; @import "components/authors"; @import "components/autocomplete"; +@import "components/auto-merge"; @import "components/breadcrumb"; +@import "components/codemirror"; @import "components/content-item"; @import "components/editor"; @import "components/featured-item"; @import "components/home-search-box"; @import "components/markdown-help"; @import "components/mobile-menu"; +@import "components/mergely"; @import "components/modals"; @import "components/pagination"; @import "components/pygments"; diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html index 74f55f0ba0..1ef4be84de 100644 --- a/templates/tutorialv2/edit/content.html +++ b/templates/tutorialv2/edit/content.html @@ -3,6 +3,7 @@ {% load thumbnail %} {% load i18n %} {% load feminize %} +{% load htmldiff %} {% block title %} {% trans "Éditer " %}{{ content.textual_type }} @@ -32,7 +33,17 @@

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

+ {% endif %} + + {% if messages %} + +
{{form.data.your_introduction}}
+ + +
+ {% endif %} + {% crispy form %} {% endblock %} diff --git a/zds/settings.py b/zds/settings.py index b72a9a3808..27f828c262 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -57,6 +57,7 @@ ('en', _('Anglais')), ) +ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: '/home/media/media.lawrence.com/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 0c49dbdb19..b5dac159b2 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -233,8 +233,26 @@ def __init__(self, *args, **kwargs): Field('description'), Field('tags'), Field('type'), - Field('image'), - Field('introduction', css_class='md-editor'), + Field('image')) + + print('----') + + if kwargs.get('data') is not None: + old_intro = kwargs.get('data').get('introduction') + # TODO on peut imagine retirer l'internationalisation + # TODO retirer le br et passer en style + self.helper.layout.append(Layout(Field('introduction', css_class='md-editor hidden'))) + # TODO cacher aussi la toolbar de l'introduction ... + self.helper.layout.append(Layout(HTML(_(u'



')))) + # TODO passer couleur du bouton en vert avec un nouveau style + # TODO lui passer un id pour le js ? + self.helper.layout.append(Layout( + ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-grey merge-btn need-to-merge-introduction')))) + + else : + self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) + + self.helper.layout.append( Layout( Field('conclusion', css_class='md-editor'), Field('last_hash'), Field('licence'), @@ -250,7 +268,7 @@ def __init__(self, *args, **kwargs): ButtonHolder( StrictButton('Valider', type='submit'), ), - ) + )) if 'type' in self.initial: self.helper['type'].wrap( diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index d0552d37ff..19fd94948d 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -237,14 +237,14 @@ def get_initial(self): initial['helps'] = self.object.helps.all() initial['last_hash'] = versioned.compute_hash() - + print('ici') return initial def get_context_data(self, **kwargs): context = super(EditContent, self).get_context_data(**kwargs) context['gallery'] = self.object.gallery - + print('la') return context def form_valid(self, form): @@ -255,10 +255,14 @@ def form_valid(self, form): current_hash = versioned.compute_hash() if current_hash != form.cleaned_data['last_hash']: data = form.data.copy() + your_introduction = data.get('introduction') + print(your_introduction) data['last_hash'] = current_hash data['introduction'] = versioned.get_introduction() data['conclusion'] = versioned.get_conclusion() + data['your_introduction'] = your_introduction form.data = data + messages.error(self.request, _(u'Une nouvelle version a été postée avant que vous ne validiez.')) return self.form_invalid(form) From 6b780281f251347d90c8b2c2f705a9b92df7bd15 Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 10 Jan 2017 16:08:38 +0000 Subject: [PATCH 15/78] clean, remove useless file --- assets/scss/main.scss | 1 - zds/tutorialv2/views/views_contents.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/assets/scss/main.scss b/assets/scss/main.scss index 00add6f198..f694c0ff9e 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -58,7 +58,6 @@ @import "components/alert-box"; @import "components/authors"; @import "components/autocomplete"; -@import "components/auto-merge"; @import "components/breadcrumb"; @import "components/codemirror"; @import "components/content-item"; diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 19fd94948d..d49d0699d2 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -237,14 +237,12 @@ def get_initial(self): initial['helps'] = self.object.helps.all() initial['last_hash'] = versioned.compute_hash() - print('ici') return initial def get_context_data(self, **kwargs): context = super(EditContent, self).get_context_data(**kwargs) context['gallery'] = self.object.gallery - print('la') return context def form_valid(self, form): @@ -256,7 +254,6 @@ def form_valid(self, form): if current_hash != form.cleaned_data['last_hash']: data = form.data.copy() your_introduction = data.get('introduction') - print(your_introduction) data['last_hash'] = current_hash data['introduction'] = versioned.get_introduction() data['conclusion'] = versioned.get_conclusion() From 2b19e0d117f1e5b84b67b36dc027e8ec02bcecca Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 10 Jan 2017 17:57:37 +0000 Subject: [PATCH 16/78] =?UTF-8?q?Clean,=20industrialisation=20du=20process?= =?UTF-8?q?us,=20divers=20bugs=20r=C3=A9gl=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/auto-merge.js | 13 ++++++++----- templates/tutorialv2/edit/content.html | 9 --------- zds/tutorialv2/forms.py | 12 +++++------- zds/tutorialv2/views/views_contents.py | 2 -- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index b9528e6af7..ac001df56b 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -36,11 +36,14 @@ $(document).ready(function () { var classList = button.attr('class').split(/\s+/); console.log(classList); for (var i = 0; i < classList.length; i++) { - // Pour un POC on pourrait faire plus simple, mais je m'intéresse au cas ou il y aura plusieurs - // boutons par page, voila un moyen de les différencier. - // on pourrait probablement automatiser ça avec une regex ou autre - if (classList[i] === 'need-to-merge-introduction') { - $intro = $("#id_introduction"); + if (classList[i].indexOf('need-to-merge-') >= 0) { + + // Cut the string to get the ending part + console.log(classList[i]) + var substring = classList[i].substring(14) + + // TODO : problème des retours à la ligne qui s'en vont ? + $intro = $("#id_" + substring); $to_merge = $("#compare").mergely('get','rhs'); $intro.val($to_merge); diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html index 1ef4be84de..4303880492 100644 --- a/templates/tutorialv2/edit/content.html +++ b/templates/tutorialv2/edit/content.html @@ -33,15 +33,6 @@

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

- - {% endif %} - - {% if messages %} - -
{{form.data.your_introduction}}
- - -
{% endif %} {% crispy form %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index b5dac159b2..97440d2de1 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -239,15 +239,13 @@ def __init__(self, *args, **kwargs): if kwargs.get('data') is not None: old_intro = kwargs.get('data').get('introduction') - # TODO on peut imagine retirer l'internationalisation # TODO retirer le br et passer en style - self.helper.layout.append(Layout(Field('introduction', css_class='md-editor hidden'))) - # TODO cacher aussi la toolbar de l'introduction ... - self.helper.layout.append(Layout(HTML(_(u'



')))) - # TODO passer couleur du bouton en vert avec un nouveau style - # TODO lui passer un id pour le js ? + self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('



'))) + self.helper.layout.append(Layout( - ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-grey merge-btn need-to-merge-introduction')))) + ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-introduction')))) else : self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index d49d0699d2..1768d81687 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -253,11 +253,9 @@ def form_valid(self, form): current_hash = versioned.compute_hash() if current_hash != form.cleaned_data['last_hash']: data = form.data.copy() - your_introduction = data.get('introduction') data['last_hash'] = current_hash data['introduction'] = versioned.get_introduction() data['conclusion'] = versioned.get_conclusion() - data['your_introduction'] = your_introduction form.data = data messages.error(self.request, _(u'Une nouvelle version a été postée avant que vous ne validiez.')) From 68ecbb19e1fcbc0865cc942bbcb415f0674af165 Mon Sep 17 00:00:00 2001 From: Antonin Date: Wed, 11 Jan 2017 11:11:34 +0000 Subject: [PATCH 17/78] ajout de la conclusion --- assets/js/auto-merge.js | 21 ++++++++++++++++++--- zds/tutorialv2/forms.py | 21 ++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index ac001df56b..3415cb648a 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -3,7 +3,7 @@ $(document).ready(function () { console.log('oooooo'); - $('#compare').mergely({ + $('.compare-introduction').mergely({ width: 'auto', height: 400, sidebar: false, @@ -16,6 +16,21 @@ $(document).ready(function () { } }); + $('.compare-conclusion').mergely({ + width: 'auto', + height: 400, + sidebar: false, + cmsettings: { readOnly: false, lineNumbers: true }, + lhs: function(setValue) { + setValue($("#your_conclusion").html()); + }, + rhs: function(setValue) { + setValue($("#id_conclusion").text()); + } + }); + + // TODO a voir comment on peut factoriser ce code. + $("#compare-editor-lhs").append('Votre Version'); $("#compare-editor-rhs").append('La version courante'); @@ -42,9 +57,9 @@ $(document).ready(function () { console.log(classList[i]) var substring = classList[i].substring(14) - // TODO : problème des retours à la ligne qui s'en vont ? + // TODO : problème des retours à la ligne qui s'en vont (ou normal car markdown ?) ? $intro = $("#id_" + substring); - $to_merge = $("#compare").mergely('get','rhs'); + $to_merge = $(".compare-" + substring).mergely('get','rhs'); $intro.val($to_merge); console.log($intro.val()); diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 97440d2de1..5629800ab7 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -235,23 +235,34 @@ def __init__(self, *args, **kwargs): Field('type'), Field('image')) - print('----') - if kwargs.get('data') is not None: old_intro = kwargs.get('data').get('introduction') # TODO retirer le br et passer en style self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) self.helper.layout.append(Layout(HTML(''))) - self.helper.layout.append(Layout(HTML('



'))) + self.helper.layout.append(Layout(HTML('



'))) self.helper.layout.append(Layout( ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-introduction')))) else : self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) - + + if kwargs.get('data') is not None: + old_conclusion = kwargs.get('data').get('conclusion') + # TODO retirer le br et passer en style + self.helper.layout.append(Layout(Field('conclusion', css_class='hidden'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('



'))) + + self.helper.layout.append(Layout( + ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-conclusion')))) + + else : + self.helper.layout.append(Layout(Field('conclusion', css_class='md-editor'))) + self.helper.layout.append( Layout( - Field('conclusion', css_class='md-editor'), + # Field('conclusion', css_class='md-editor'), Field('last_hash'), Field('licence'), Field('subcategory', template='crispy/checkboxselectmultiple.html'), From 14e987a2fd8322cacf5b133c680d0fe0807e7157 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 11:46:37 +0000 Subject: [PATCH 18/78] change alert to msg --- assets/js/auto-merge.js | 7 +++++++ zds/settings.py | 1 + 2 files changed, 8 insertions(+) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 3415cb648a..f9cad6897f 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -64,6 +64,13 @@ $(document).ready(function () { console.log($intro.val()); // TODO : un bon petit alert des familles + var msg = '
\ + Le merge a bien été effectué \ + {% trans "Masquer l\'alerte" %} \ +
'; + + $(".compare-" + substring).append(msg) + alert('Contenu bien mergé'); } } diff --git a/zds/settings.py b/zds/settings.py index 27f828c262..e122421d44 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -31,6 +31,7 @@ }, } +ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. From 8be724e6b1532dfaf510ce2129babb6706aff266 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 17:25:45 +0000 Subject: [PATCH 19/78] =?UTF-8?q?Nettoyage=20et=20style,=20diverses=20am?= =?UTF-8?q?=C3=A9liorations=20visuelles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/auto-merge.js | 137 ++++++++++++++++++---------------------- zds/settings.py | 1 - zds/tutorialv2/forms.py | 10 ++- 3 files changed, 66 insertions(+), 82 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index f9cad6897f..066eaa0210 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -1,79 +1,66 @@ - - -$(document).ready(function () { - console.log('oooooo'); - - $('.compare-introduction').mergely({ - width: 'auto', - height: 400, - sidebar: false, - cmsettings: { readOnly: false, lineNumbers: true }, - lhs: function(setValue) { - setValue($("#your_introduction").html()); - }, - rhs: function(setValue) { - setValue($("#id_introduction").text()); - } - }); +(function($, undefined) { + "use strict"; - $('.compare-conclusion').mergely({ - width: 'auto', - height: 400, - sidebar: false, - cmsettings: { readOnly: false, lineNumbers: true }, - lhs: function(setValue) { - setValue($("#your_conclusion").html()); - }, - rhs: function(setValue) { - setValue($("#id_conclusion").text()); + $(document).ready(function () { + + /** + * Sets up the merge interface (using mergely). + */ + function mergelySetUp(div, left, right) + { + div.mergely({ + width: "auto", + height: 400, + sidebar: false, + cmsettings: { readOnly: false, lineNumbers: true, lineWrapping: true }, + lhs: function(setValue) { + setValue(left.html()); + }, + rhs: function(setValue) { + setValue(right.text()); + } + }); } - }); - - // TODO a voir comment on peut factoriser ce code. - - $("#compare-editor-lhs").append('Votre Version'); - $("#compare-editor-rhs").append('La version courante'); - - - - /** - * Merge introduction - */ - $(".merge-btn").on("click", function(e){ - console.log('click'); - e.stopPropagation(); - e.preventDefault(); - // var $form = $(this).parents("form:first"); - var button = $(this); - - console.log(button); - - var classList = button.attr('class').split(/\s+/); - console.log(classList); - for (var i = 0; i < classList.length; i++) { - if (classList[i].indexOf('need-to-merge-') >= 0) { - // Cut the string to get the ending part - console.log(classList[i]) - var substring = classList[i].substring(14) - - // TODO : problème des retours à la ligne qui s'en vont (ou normal car markdown ?) ? - $intro = $("#id_" + substring); - $to_merge = $(".compare-" + substring).mergely('get','rhs'); - $intro.val($to_merge); - - console.log($intro.val()); - // TODO : un bon petit alert des familles - var msg = '
\ - Le merge a bien été effectué \ - {% trans "Masquer l\'alerte" %} \ -
'; - - $(".compare-" + substring).append(msg) - - alert('Contenu bien mergé'); - } - } - }); + mergelySetUp($(".compare-introduction"),$("#your_introduction"),$("#id_introduction")); + mergelySetUp($(".compare-conclusion"),$("#your_conclusion"),$("#id_conclusion")); + + $("#compare-editor-lhs").append("Votre Version"); + $("#compare-editor-rhs").append("La version courante"); + + + /** + * Merge content + */ + $(".merge-btn").on("click", function(e){ + + e.stopPropagation(); + e.preventDefault(); + + var button = $(this); + var classList = button.attr("class").split(/\s+/); + + for (var i = 0; i < classList.length; i++) { + if (classList[i].indexOf("need-to-merge-") >= 0) { + + // Cut the string to get the ending part + var substring = classList[i].substring(14); + + var $intro = $("#id_" + substring); + var $toMerge = $(".compare-" + substring).mergely("get","rhs"); + $intro.val($toMerge); + -}); + // Display confirmation message + var msg = "
" + + "Le merge a bien été effectué" + + "Masquer l'alerte" + + "
"; + + button.before(msg); + } + } + }); + + }); +})(jQuery); \ No newline at end of file diff --git a/zds/settings.py b/zds/settings.py index e122421d44..27f828c262 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -31,7 +31,6 @@ }, } -ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 5629800ab7..a78a3d5145 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -237,27 +237,25 @@ def __init__(self, *args, **kwargs): if kwargs.get('data') is not None: old_intro = kwargs.get('data').get('introduction') - # TODO retirer le br et passer en style + self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) self.helper.layout.append(Layout(HTML(''))) - self.helper.layout.append(Layout(HTML('



'))) + self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-introduction')))) - else : self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) if kwargs.get('data') is not None: old_conclusion = kwargs.get('data').get('conclusion') - # TODO retirer le br et passer en style + self.helper.layout.append(Layout(Field('conclusion', css_class='hidden'))) self.helper.layout.append(Layout(HTML(''))) - self.helper.layout.append(Layout(HTML('



'))) + self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-conclusion')))) - else : self.helper.layout.append(Layout(Field('conclusion', css_class='md-editor'))) From 0977079951e4d76dde45ffdfebcea6dc7533018e Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 17:52:11 +0000 Subject: [PATCH 20/78] style --- assets/js/auto-merge.js | 2 +- templates/tutorialv2/edit/content.html | 2 -- zds/tutorialv2/forms.py | 27 ++++++++++++++------------ zds/tutorialv2/views/views_contents.py | 3 ++- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 066eaa0210..995feac13a 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -63,4 +63,4 @@ }); }); -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html index 4303880492..74f55f0ba0 100644 --- a/templates/tutorialv2/edit/content.html +++ b/templates/tutorialv2/edit/content.html @@ -3,7 +3,6 @@ {% load thumbnail %} {% load i18n %} {% load feminize %} -{% load htmldiff %} {% block title %} {% trans "Éditer " %}{{ content.textual_type }} @@ -34,7 +33,6 @@

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

{% endif %} - {% crispy form %} {% endblock %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index a78a3d5145..4c6f14b290 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -234,33 +234,36 @@ def __init__(self, *args, **kwargs): Field('tags'), Field('type'), Field('image')) - + if kwargs.get('data') is not None: old_intro = kwargs.get('data').get('introduction') self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) - self.helper.layout.append(Layout(HTML(''))) - self.helper.layout.append(Layout(HTML('
'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( - ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-introduction')))) - else : + ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ + merge-btn need-to-merge-introduction')))) + else: self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) if kwargs.get('data') is not None: old_conclusion = kwargs.get('data').get('conclusion') self.helper.layout.append(Layout(Field('conclusion', css_class='hidden'))) - self.helper.layout.append(Layout(HTML(''))) - self.helper.layout.append(Layout(HTML('
'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( - ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit merge-btn need-to-merge-conclusion')))) - else : + ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ + merge-btn need-to-merge-conclusion')))) + else: self.helper.layout.append(Layout(Field('conclusion', css_class='md-editor'))) - - self.helper.layout.append( Layout( - # Field('conclusion', css_class='md-editor'), + + self.helper.layout.append(Layout( Field('last_hash'), Field('licence'), Field('subcategory', template='crispy/checkboxselectmultiple.html'), diff --git a/zds/tutorialv2/views/views_contents.py b/zds/tutorialv2/views/views_contents.py index 1768d81687..d0552d37ff 100644 --- a/zds/tutorialv2/views/views_contents.py +++ b/zds/tutorialv2/views/views_contents.py @@ -237,12 +237,14 @@ def get_initial(self): initial['helps'] = self.object.helps.all() initial['last_hash'] = versioned.compute_hash() + return initial def get_context_data(self, **kwargs): context = super(EditContent, self).get_context_data(**kwargs) context['gallery'] = self.object.gallery + return context def form_valid(self, form): @@ -257,7 +259,6 @@ def form_valid(self, form): data['introduction'] = versioned.get_introduction() data['conclusion'] = versioned.get_conclusion() form.data = data - messages.error(self.request, _(u'Une nouvelle version a été postée avant que vous ne validiez.')) return self.form_invalid(form) From c465f32bc769ce4d2fa2b86b4127a0f57cb1a9f6 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 17:54:50 +0000 Subject: [PATCH 21/78] style --- zds/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zds/settings.py b/zds/settings.py index 27f828c262..b72a9a3808 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -57,7 +57,6 @@ ('en', _('Anglais')), ) -ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: '/home/media/media.lawrence.com/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') From cbfe255ad6a4b383fda0bfbf0762c2805db09c41 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 20:26:12 +0000 Subject: [PATCH 22/78] ajout du package, simplifie le formulaire --- package.json | 1 + zds/tutorialv2/forms.py | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 124480a25d..e967f220f3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "gulp-uglify": "2.0.0", "gulp.spritesmith": "6.2.1", "jquery": "3.1.1", + "mergely": "3.4.0", "normalize.css": "4.2.0" }, "devDependencies": { diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 4c6f14b290..fc59d69d0c 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -235,7 +235,7 @@ def __init__(self, *args, **kwargs): Field('type'), Field('image')) - if kwargs.get('data') is not None: + if kwargs.get('data', None) is not None: old_intro = kwargs.get('data').get('introduction') self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) @@ -246,10 +246,7 @@ def __init__(self, *args, **kwargs): self.helper.layout.append(Layout( ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ merge-btn need-to-merge-introduction')))) - else: - self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) - - if kwargs.get('data') is not None: + old_conclusion = kwargs.get('data').get('conclusion') self.helper.layout.append(Layout(Field('conclusion', css_class='hidden'))) @@ -261,6 +258,7 @@ def __init__(self, *args, **kwargs): ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ merge-btn need-to-merge-conclusion')))) else: + self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) self.helper.layout.append(Layout(Field('conclusion', css_class='md-editor'))) self.helper.layout.append(Layout( From 1daa48ac3860d31810fd111acf3c11716d48b6f5 Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 12 Jan 2017 20:41:30 +0000 Subject: [PATCH 23/78] js plus logique --- assets/js/auto-merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 995feac13a..9128f5dc02 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -17,7 +17,7 @@ setValue(left.html()); }, rhs: function(setValue) { - setValue(right.text()); + setValue(right.html()); } }); } From 25276f09c657f25d12c69621b4d2461bf9708e5d Mon Sep 17 00:00:00 2001 From: Antonin Date: Fri, 13 Jan 2017 07:58:32 +0000 Subject: [PATCH 24/78] Documentation JS, Refacto JS, ajout d'un TODO --- assets/js/auto-merge.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 9128f5dc02..a0d90bfcfc 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -4,20 +4,24 @@ $(document).ready(function () { /** - * Sets up the merge interface (using mergely). + * Sets up the merge interface (using mergely) in the $div Object. Data is generally retrieved from a form field + * or an aditionnal div exposing the old data, also generated in the form. + * @param {Object} $div - The base object used to set up the interface. Generally created in forms files. + * @param {Object} $left - The object from which we will pick the content to put in the left hand side (lhs) of the editor. + * @param {Object} $right - The object from which we will pick the content to put in the right hand side (rhs) of the editor. */ - function mergelySetUp(div, left, right) + function mergelySetUp($div, $left, $right) { - div.mergely({ + $div.mergely({ width: "auto", height: 400, sidebar: false, cmsettings: { readOnly: false, lineNumbers: true, lineWrapping: true }, lhs: function(setValue) { - setValue(left.html()); + setValue($left.html()); }, rhs: function(setValue) { - setValue(right.html()); + setValue($right.html()); } }); } @@ -38,19 +42,19 @@ e.preventDefault(); var button = $(this); - var classList = button.attr("class").split(/\s+/); - - for (var i = 0; i < classList.length; i++) { - if (classList[i].indexOf("need-to-merge-") >= 0) { + + Array.from(this.classList).forEach(function(element){ + if (element.indexOf("need-to-merge-") >= 0) { // Cut the string to get the ending part - var substring = classList[i].substring(14); + var substring = element.substring(14); var $intro = $("#id_" + substring); var $toMerge = $(".compare-" + substring).mergely("get","rhs"); $intro.val($toMerge); - + // TODO : cacher le message avant de l'afficher + // TODO ainsi si l'utilisateur clique plusieurs fois les alertes ne se cumulent pas. // Display confirmation message var msg = "
" + "Le merge a bien été effectué" + @@ -59,7 +63,9 @@ button.before(msg); } - } + + }); + }); }); From 1ec13cc674839ffffd06f54178c9d547a083c84c Mon Sep 17 00:00:00 2001 From: Antonin Date: Fri, 13 Jan 2017 07:58:55 +0000 Subject: [PATCH 25/78] Style --- assets/js/auto-merge.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index a0d90bfcfc..bf72aeab0f 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -1,8 +1,8 @@ (function($, undefined) { "use strict"; - + $(document).ready(function () { - + /** * Sets up the merge interface (using mergely) in the $div Object. Data is generally retrieved from a form field * or an aditionnal div exposing the old data, also generated in the form. @@ -31,8 +31,8 @@ $("#compare-editor-lhs").append("Votre Version"); $("#compare-editor-rhs").append("La version courante"); - - + + /** * Merge content */ @@ -40,33 +40,33 @@ e.stopPropagation(); e.preventDefault(); - + var button = $(this); Array.from(this.classList).forEach(function(element){ if (element.indexOf("need-to-merge-") >= 0) { - + // Cut the string to get the ending part var substring = element.substring(14); - + var $intro = $("#id_" + substring); var $toMerge = $(".compare-" + substring).mergely("get","rhs"); $intro.val($toMerge); - + // TODO : cacher le message avant de l'afficher // TODO ainsi si l'utilisateur clique plusieurs fois les alertes ne se cumulent pas. // Display confirmation message - var msg = "
" + - "Le merge a bien été effectué" + + var msg = "
" + + "Le merge a bien été effectué" + "Masquer l'alerte" + "
"; - + button.before(msg); } - + }); }); - + }); })(jQuery); From 9335ccc015e7607e3a1512d1accf22a8ab832b80 Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 17 Jan 2017 12:52:44 +0000 Subject: [PATCH 26/78] better UX, remove todo --- assets/js/auto-merge.js | 16 ++++++++++------ assets/scss/main.scss | 1 + zds/tutorialv2/forms.py | 21 +++++++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index bf72aeab0f..25df007da3 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -53,15 +53,19 @@ var $toMerge = $(".compare-" + substring).mergely("get","rhs"); $intro.val($toMerge); - // TODO : cacher le message avant de l'afficher - // TODO ainsi si l'utilisateur clique plusieurs fois les alertes ne se cumulent pas. - // Display confirmation message - var msg = "
" + - "Le merge a bien été effectué" + + + // Confirmation message + var msg = "
" + + "Le contenu a bien été validé." + "Masquer l'alerte" + "
"; - button.before(msg); + button.before(msg); + + setTimeout(function() { + $('.alert-merge').fadeOut('fast'); + }, 2000); + } }); diff --git a/assets/scss/main.scss b/assets/scss/main.scss index f694c0ff9e..00add6f198 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -58,6 +58,7 @@ @import "components/alert-box"; @import "components/authors"; @import "components/autocomplete"; +@import "components/auto-merge"; @import "components/breadcrumb"; @import "components/codemirror"; @import "components/content-item"; diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index fc59d69d0c..acd755b748 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -244,8 +244,8 @@ def __init__(self, *args, **kwargs): self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( - ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ - merge-btn need-to-merge-introduction')))) + ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-introduction')))) old_conclusion = kwargs.get('data').get('conclusion') @@ -255,11 +255,20 @@ def __init__(self, *args, **kwargs): self.helper.layout.append(Layout(HTML('
'))) self.helper.layout.append(Layout( - ButtonHolder(StrictButton(_(u'Merger'), type='merge', name='merge', css_class='btn btn-submit \ - merge-btn need-to-merge-conclusion')))) + ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-conclusion')))) else: - self.helper.layout.append(Layout(Field('introduction', css_class='md-editor'))) - self.helper.layout.append(Layout(Field('conclusion', css_class='md-editor'))) + + self.helper.layout.append(Layout( + Field('introduction', css_class='md-editor preview-source'), + ButtonHolder(StrictButton(_(u'Aperçu'), type='preview', name='preview', + css_class='btn btn-grey preview-btn'),), + HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \ + with text=form.introduction.value %}{% endif %}'), + Field('conclusion', css_class='md-editor preview-source'), + ButtonHolder(StrictButton(_(u'Aperçu'), type='preview', name='preview', + css_class='btn btn-grey preview-btn'),))) + self.helper.layout.append(Layout( Field('last_hash'), From 1e404653f8ee1136089b7013ce55427123b60f24 Mon Sep 17 00:00:00 2001 From: Antonin Date: Mon, 23 Jan 2017 09:29:33 +0000 Subject: [PATCH 27/78] ajout de l'interface de merge aux autres formulaires --- assets/js/auto-merge.js | 2 +- zds/tutorialv2/forms.py | 43 ++++++++++++++++++++++++++++++++++++----- zds/utils/forms.py | 19 ++++++++++++++++-- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 25df007da3..0ab492919e 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -53,7 +53,7 @@ var $toMerge = $(".compare-" + substring).mergely("get","rhs"); $intro.val($toMerge); - + // La version courante / votre version n'est pas affiche sur le deuxieme textarea // Confirmation message var msg = "
" + "Le contenu a bien été validé." + diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index acd755b748..e81a17b4c2 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -149,10 +149,43 @@ def __init__(self, *args, **kwargs): self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' - self.helper.layout = Layout( - Field('title'), - Field('introduction', css_class='md-editor'), - Field('conclusion', css_class='md-editor'), + self.helper.layout = Layout(Field('title')) + + if kwargs.get('data', None) is not None: + old_intro = kwargs.get('data').get('introduction') + + self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('
'))) + + self.helper.layout.append(Layout( + ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-introduction')))) + + old_conclusion = kwargs.get('data').get('conclusion') + + self.helper.layout.append(Layout(Field('conclusion', css_class='hidden'))) + self.helper.layout.append(Layout(HTML(''))) + self.helper.layout.append(Layout(HTML('
'))) + + self.helper.layout.append(Layout( + ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-conclusion')))) + else: + + self.helper.layout.append(Layout( + Field('introduction', css_class='md-editor preview-source'), + ButtonHolder(StrictButton(_(u'Aperçu'), type='preview', name='preview', + css_class='btn btn-grey preview-btn'),), + HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \ + with text=form.introduction.value %}{% endif %}'), + Field('conclusion', css_class='md-editor preview-source'), + ButtonHolder(StrictButton(_(u'Aperçu'), type='preview', name='preview', + css_class='btn btn-grey preview-btn'),))) + + self.helper.layout.append(Layout( Field('msg_commit'), Field('last_hash'), ButtonHolder( @@ -160,7 +193,7 @@ def __init__(self, *args, **kwargs): _(u'Valider'), type='submit'), ) - ) + )) class ContentForm(ContainerForm): diff --git a/zds/utils/forms.py b/zds/utils/forms.py index ea711701bd..52d839479b 100644 --- a/zds/utils/forms.py +++ b/zds/utils/forms.py @@ -39,9 +39,24 @@ def __init__(self, *args, **kwargs): class CommonLayoutVersionEditor(Layout): def __init__(self, *args, **kwargs): + + print(kwargs.get('data', None)) + if kwargs.get('data', None) is not None: + old_text = kwargs.get('data').get('text') + + text_field = Field('text', css_class='hidden') + text_field += HTML('') + text_field += HTML('
') + + text_field+= ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-text')) + + else: + text_field = Field('text', css_class='md-editor') + super(CommonLayoutVersionEditor, self).__init__( Div( - Field('text', css_class='md-editor'), + text_field, Field('msg_commit'), ButtonHolder( StrictButton( @@ -52,7 +67,7 @@ def __init__(self, *args, **kwargs): _(u'Aperçu'), type='submit', name='preview', - css_class='btn-grey'), + css_class='btn-grey preview-btn'), ), ), *args, **kwargs From f64b3192a3f0ca9dd13b3758021e715ed423d0f3 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Mon, 23 Jan 2017 19:37:29 +0100 Subject: [PATCH 28/78] Update forms.py --- zds/utils/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zds/utils/forms.py b/zds/utils/forms.py index 52d839479b..1d2dd4b7aa 100644 --- a/zds/utils/forms.py +++ b/zds/utils/forms.py @@ -39,8 +39,7 @@ def __init__(self, *args, **kwargs): class CommonLayoutVersionEditor(Layout): def __init__(self, *args, **kwargs): - - print(kwargs.get('data', None)) + if kwargs.get('data', None) is not None: old_text = kwargs.get('data').get('text') From e81dc711033964cb8d9dfdb965535f8d701d0887 Mon Sep 17 00:00:00 2001 From: Antonin Date: Tue, 24 Jan 2017 08:04:50 +0000 Subject: [PATCH 29/78] ajout aux autres forms --- assets/js/auto-merge.js | 2 +- zds/settings.py | 1 + zds/tutorialv2/forms.py | 4 ++-- zds/utils/forms.py | 18 ++++++++---------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 0ab492919e..3dbaa762d0 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -28,6 +28,7 @@ mergelySetUp($(".compare-introduction"),$("#your_introduction"),$("#id_introduction")); mergelySetUp($(".compare-conclusion"),$("#your_conclusion"),$("#id_conclusion")); + mergelySetUp($(".compare-text"),$("#your_text"),$("#id_text")); $("#compare-editor-lhs").append("Votre Version"); $("#compare-editor-rhs").append("La version courante"); @@ -53,7 +54,6 @@ var $toMerge = $(".compare-" + substring).mergely("get","rhs"); $intro.val($toMerge); - // La version courante / votre version n'est pas affiche sur le deuxieme textarea // Confirmation message var msg = "
" + "Le contenu a bien été validé." + diff --git a/zds/settings.py b/zds/settings.py index b72a9a3808..27f828c262 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -57,6 +57,7 @@ ('en', _('Anglais')), ) +ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: '/home/media/media.lawrence.com/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index e81a17b4c2..ffb6a6e1b1 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -365,7 +365,7 @@ class ExtractForm(FormWithTitle): last_hash = forms.CharField(widget=forms.HiddenInput, required=False) def __init__(self, *args, **kwargs): - super(ExtractForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' @@ -373,7 +373,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Field('title'), Field('last_hash'), - CommonLayoutVersionEditor(), + CommonLayoutVersionEditor(*args, **kwargs), ) diff --git a/zds/utils/forms.py b/zds/utils/forms.py index 52d839479b..b3ab01d49b 100644 --- a/zds/utils/forms.py +++ b/zds/utils/forms.py @@ -40,17 +40,15 @@ class CommonLayoutVersionEditor(Layout): def __init__(self, *args, **kwargs): - print(kwargs.get('data', None)) if kwargs.get('data', None) is not None: old_text = kwargs.get('data').get('text') - text_field = Field('text', css_class='hidden') - text_field += HTML('') - text_field += HTML('
') - - text_field+= ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ - css_class='btn btn-submit merge-btn need-to-merge-text')) - + text_field = Div( Field('text', css_class='hidden'), + HTML(''), + HTML('
'), + ButtonHolder(StrictButton(_(u'Valider cette version'), type='merge', name='merge', \ + css_class='btn btn-submit merge-btn need-to-merge-text'))) + else: text_field = Field('text', css_class='md-editor') @@ -67,11 +65,11 @@ def __init__(self, *args, **kwargs): _(u'Aperçu'), type='submit', name='preview', - css_class='btn-grey preview-btn'), + css_class='btn-grey preview-btn' ), ), *args, **kwargs - ) + )) class CommonLayoutModalText(Layout): From 0541a3e8eafba77ffd7b2ec0c44d5b7624f3ce0b Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Thu, 26 Jan 2017 07:50:18 +0100 Subject: [PATCH 30/78] Update forms.py --- zds/tutorialv2/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index da44423b74..3839064038 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -149,7 +149,7 @@ def __init__(self, *args, **kwargs): self.helper = FormHelper() self.helper.form_class = 'content-wrapper' self.helper.form_method = 'post' - + self.helper.layout = Layout(Field('title')) if kwargs.get('data', None) is not None: From 20bf69963c12535a663e00cfaa8b229a52b9c1ff Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 26 Jan 2017 06:57:03 +0000 Subject: [PATCH 31/78] fix style --- zds/tutorialv2/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 3839064038..20fb003aee 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -188,7 +188,7 @@ def __init__(self, *args, **kwargs): HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \ with text=form.conclusion.value %}{% endif %}'), - self.helper.layout.append(Layout( + self.helper.layout.append(Layout( Field('msg_commit'), Field('last_hash'), ButtonHolder( @@ -307,7 +307,7 @@ def __init__(self, *args, **kwargs): HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \ with text=form.conclusion.value %}{% endif %}'), - self.helper.layout.append(Layout( + self.helper.layout.append(Layout( Field('last_hash'), Field('licence'), Field('subcategory', template='crispy/checkboxselectmultiple.html'), From a199b20e6f6f929d0b4513de2c478e1070f6104c Mon Sep 17 00:00:00 2001 From: Antonin Date: Thu, 26 Jan 2017 06:58:09 +0000 Subject: [PATCH 32/78] fix style --- zds/tutorialv2/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 20fb003aee..69804b5382 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): self.helper.form_method = 'post' self.helper.layout = Layout(Field('title')) - + if kwargs.get('data', None) is not None: old_intro = kwargs.get('data').get('introduction') From f32e2baa1503d05d5af7af75489d659480105824 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Thu, 2 Feb 2017 03:48:41 -0400 Subject: [PATCH 33/78] Update forms.py --- zds/tutorialv2/forms.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index be2b32f7c9..57bb4e96fa 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -305,13 +305,9 @@ def __init__(self, *args, **kwargs): ButtonHolder(StrictButton(_(u'Aperçu'), type='preview', name='preview', css_class='btn btn-grey preview-btn'),))) HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \ - with text=form.conclusion.value %}{% endif %}'), + with text=form.conclusion.value %}{% endif %}') -<<<<<<< HEAD self.helper.layout.append(Layout( -======= - self.helper.layout.append(Layout( ->>>>>>> 0541a3e8eafba77ffd7b2ec0c44d5b7624f3ce0b Field('last_hash'), Field('licence'), Field('subcategory', template='crispy/checkboxselectmultiple.html'), From 185cc00c734ad801242372f733279bd7d5f0c927 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Tue, 21 Feb 2017 03:59:20 -0400 Subject: [PATCH 34/78] Update settings.py --- zds/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zds/settings.py b/zds/settings.py index 27f828c262..b72a9a3808 100644 --- a/zds/settings.py +++ b/zds/settings.py @@ -57,7 +57,6 @@ ('en', _('Anglais')), ) -ALLOWED_HOSTS = ['zds-anto59290.c9users.io'] # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: '/home/media/media.lawrence.com/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') From 4e2dbfbfd460e181608b5c6e92e43090e6521f48 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Tue, 21 Feb 2017 04:03:23 -0400 Subject: [PATCH 35/78] Update auto-merge.js --- assets/js/auto-merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js index 3dbaa762d0..bb909c693e 100644 --- a/assets/js/auto-merge.js +++ b/assets/js/auto-merge.js @@ -63,7 +63,7 @@ button.before(msg); setTimeout(function() { - $('.alert-merge').fadeOut('fast'); + $(".alert-merge").fadeOut("fast"); }, 2000); } From 7f2c913e81915a94ee0e8d4c7af2e3b0b6fb9cb3 Mon Sep 17 00:00:00 2001 From: Anto59290 Date: Tue, 28 Feb 2017 03:44:59 -0400 Subject: [PATCH 36/78] Update forms.py --- zds/tutorialv2/forms.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 57bb4e96fa..6fc3c55876 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -154,6 +154,8 @@ def __init__(self, *args, **kwargs): if kwargs.get('data', None) is not None: old_intro = kwargs.get('data').get('introduction') + if old_intro is None: + old_intro = '' self.helper.layout.append(Layout(Field('introduction', css_class='hidden'))) self.helper.layout.append(Layout(HTML('