From e51c37a3fff8697ab8dae8fc96af0f116c69cf8c Mon Sep 17 00:00:00 2001 From: fdambrine Date: Thu, 11 Jan 2018 13:35:47 +0100 Subject: [PATCH] Add end-to-end test refactor factories to use python3 syntax change js to properly react --- .travis.yml | 10 +-- assets/js/content-publication-readiness.js | 19 ++--- doc/source/back-end/contents_manifest.rst | 6 +- requirements-dev.txt | 2 +- templates/tutorialv2/view/container.html | 18 ++-- zds/member/api/permissions.py | 19 +++-- zds/settings/abstract_base/django.py | 2 +- zds/tutorialv2/api/views.py | 7 +- zds/tutorialv2/factories.py | 19 +---- zds/tutorialv2/models/versioned.py | 8 ++ zds/tutorialv2/publication_utils.py | 1 - zds/tutorialv2/publish_container.py | 13 +-- zds/tutorialv2/tests/__init__.py | 75 +++++++++++++++++ zds/tutorialv2/tests/tests_front.py | 98 ++++++++++++++++++++++ zds/tutorialv2/tests/tests_utils.py | 1 - zds/tutorialv2/utils.py | 2 +- 16 files changed, 239 insertions(+), 61 deletions(-) create mode 100644 zds/tutorialv2/tests/tests_front.py diff --git a/.travis.yml b/.travis.yml index c304cdef19..fffad52095 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,9 +91,7 @@ install: && mkdir geckodriver \ && tar -xzf geckodriver-v0.19.1-linux64.tar.gz -C geckodriver \ && export PATH=$PATH:$PWD/geckodriver \ - && export DISPLAY=:99.0 \ - && sh -e /etc/init.d/xvfb start \ - && sleep 3 # give xvfb some time to start + && export DISPLAY=:99.0 fi - | @@ -163,11 +161,11 @@ script: # selenium tests if [[ "$ZDS_TEST_JOB" == *"selenium"* ]]; then yarn build - python manage.py \ + xvfb-run --server-args="-screen 0 1280x720x8" python manage.py \ test \ - --keepdb \ --settings zds.settings.ci_test \ - --tag=front + --tag=front \ + --keepdb fi - | diff --git a/assets/js/content-publication-readiness.js b/assets/js/content-publication-readiness.js index d6702f6995..6efa1eca36 100644 --- a/assets/js/content-publication-readiness.js +++ b/assets/js/content-publication-readiness.js @@ -1,19 +1,18 @@ -(function ($, undefined) { - $("li").on("click", ".readiness", function (e) { +(function ($) { + $(".readiness").on("click", function (e) { var url = $(e.target).data("url"); - var readiness = $(e.target).data("is-ready") === "true"; + var readiness = $(e.target).data("is-ready").toString() === "true"; var csrf = $("input[name=csrfmiddlewaretoken]").val(); - + var toggledReadiness = !readiness; $.ajax(url, {method: "PUT", data: { - "ready_to_publish": readiness, + "ready_to_publish": toggledReadiness, "container_slug": $(e.target).data("container-slug"), "parent_container_slug": $(e.target).data("parent-container-slug") || "" }, success: function () { - $(e.target).data("is-ready", readiness?"false":"true") - .children("span") - .removeClass("glyphicon-remove-sign") - .removeClass("glyphicon-ok-sign") - .addClass(readiness?"glyphicon-remove-sign":"glyphicon-ok-sign"); + var readinessAsString = String(toggledReadiness); + var newDisplayedText = $(e.target).data("is-ready-" + readinessAsString); + $(e.target).attr("data-is-ready", readinessAsString) + .text(newDisplayedText); }, headers: { "X-CSRFToken": csrf }}); diff --git a/doc/source/back-end/contents_manifest.rst b/doc/source/back-end/contents_manifest.rst index 09f72f3d20..c640c64026 100644 --- a/doc/source/back-end/contents_manifest.rst +++ b/doc/source/back-end/contents_manifest.rst @@ -38,10 +38,10 @@ Les 0 non significatifs sont optionnels ainsi ``{version: "1"}`` est strictement Version 2.1 ----------- -La version 2.1 est la version actuelement utilisée. -Le manifest voit l'arrivée d'un nouvel élément non obligatoire ``ready_to_publish`` qui sera utilisé sur tous les éléments de type `Container`. +La version 2.1 est la version actuellement utilisée. +Le manifest voit l'arrivée d'un nouvel élément non obligatoire ``ready_to_publish`` qui sera utilisé sur tous les éléments de type ``Container``. Cet élément permet de marquer qu'une partie ou un chapitre est prêt à être publié. Lorsque la valeur est à ``False``, la partie ou le chapitre -sont simplement ignoré du processus de publication. +sont simplement ignorés du processus de publication. Lorsque l'attribut n'est pas renseigné, il est supposé *truthy*. diff --git a/requirements-dev.txt b/requirements-dev.txt index 8d9d6bc019..9cca6eed7e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ flake8==3.4.1 flake8_quotes==0.11.0 autopep8==1.3.2 sphinx==1.6.3 -selenium==3.6.0 +selenium==3.8.1 sphinx_rtd_theme==0.2.4 faker==0.8.3 mock==2.0.0 diff --git a/templates/tutorialv2/view/container.html b/templates/tutorialv2/view/container.html index cb3b56da6a..cdf7d07c55 100644 --- a/templates/tutorialv2/view/container.html +++ b/templates/tutorialv2/view/container.html @@ -243,17 +243,17 @@

{% endif %} - {% if container.ready_to_publish %}
  • - - {% trans "Marquer comme à ne pas valider." %} + + {% if container.ready_to_publish %} + {% trans "Marquer comme à ne pas valider." %} + {% else %} + {% trans "Marquer comme prêt à valider." %} + {% endif %}
  • - {% else %} -
  • - {% trans "Marquer comme prêt à valider." %}
  • - {% endif %} {% endif %} {% endblock %} diff --git a/zds/member/api/permissions.py b/zds/member/api/permissions.py index 85f6958f61..920dc1ff68 100644 --- a/zds/member/api/permissions.py +++ b/zds/member/api/permissions.py @@ -25,16 +25,25 @@ def has_object_permission(self, request, view, obj): class IsOwner(permissions.BasePermission): + + def has_permission(self, request, view): + return request.user and self.has_object_permission(request, view, view.get_object()) + def has_object_permission(self, request, view, obj): - # Write permissions are not allowed to the owner of the snippet if hasattr(obj, 'user'): - owners = [obj.user] + owners = [obj.user.pk] elif hasattr(obj, 'author'): - owners = [obj.author] + owners = [obj.author.pk] elif hasattr(obj, 'authors'): - owners = list(obj.authors) + owners = list(obj.authors.values_list('pk', flat=True)) - return request.user in owners + return request.user.pk in owners + + +class IsAuthorOrStaff(permissions.BasePermission): + def has_permission(self, request, view): + return IsStaffUser().has_permission(request, view) or IsOwner().has_object_permission(request, view, + view.get_object()) class IsNotOwnerOrReadOnly(permissions.BasePermission): diff --git a/zds/settings/abstract_base/django.py b/zds/settings/abstract_base/django.py index 967f2fcebe..9b339ee9ce 100644 --- a/zds/settings/abstract_base/django.py +++ b/zds/settings/abstract_base/django.py @@ -145,8 +145,8 @@ CRISPY_TEMPLATE_PACK = 'bootstrap' INSTALLED_APPS = ( - 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', diff --git a/zds/tutorialv2/api/views.py b/zds/tutorialv2/api/views.py index 40450df80d..97b068c1e0 100644 --- a/zds/tutorialv2/api/views.py +++ b/zds/tutorialv2/api/views.py @@ -4,14 +4,14 @@ from rest_framework.generics import UpdateAPIView from rest_framework.serializers import Serializer, CharField, BooleanField from rest_framework.permissions import IsAuthenticatedOrReadOnly -from zds.member.api.permissions import CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsStaffUser, IsOwner +from zds.member.api.permissions import CanReadAndWriteNowOrReadOnly, IsNotOwnerOrReadOnly, IsAuthorOrStaff from zds.tutorialv2.utils import search_container_or_404 from zds.utils.api.views import KarmaView from zds.tutorialv2.models.database import ContentReaction, PublishableContent class ContainerReadinessSerializer(Serializer): - parent_container_slug = CharField() + parent_container_slug = CharField(allow_blank=True, allow_null=True, required=False) container_slug = CharField(required=True) ready_to_publish = BooleanField(required=True) @@ -45,7 +45,7 @@ class ContentReactionKarmaView(KarmaView): class ContainerPublicationReadinessView(UpdateAPIView): - permission_classes = (IsStaffUser, IsOwner) + permission_classes = (IsAuthorOrStaff, ) serializer_class = ContainerReadinessSerializer def get_object(self): @@ -54,4 +54,5 @@ def get_object(self): .first() if not content: raise Http404() + self.check_object_permissions(self.request, object) return content diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 062e97ca56..3dce25263b 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -41,7 +41,7 @@ class Meta: pubdate = datetime.now() @classmethod - def _prepare(cls, create, **kwargs): + def _prepare(cls, create, *, light=True, **kwargs): auths = [] if 'author_list' in kwargs: auths = kwargs.pop('author_list') @@ -52,9 +52,6 @@ def _prepare(cls, create, **kwargs): given_licence = Licence.objects.filter(title=given_licence).first() or Licence.objects.first() licence = given_licence or LicenceFactory() - light = True - if 'light' in kwargs: - light = kwargs.pop('light') text = text_content if not light: text = tricky_text_content @@ -82,13 +79,9 @@ class Meta: title = factory.Sequence(lambda n: 'Mon container No{0}'.format(n + 1)) @classmethod - def _prepare(cls, create, **kwargs): - db_object = kwargs.pop('db_object', None) + def _prepare(cls, create, *, db_object=None, light=True, **kwargs): parent = kwargs.pop('parent', None) - light = True - if 'light' in kwargs: - light = kwargs.pop('light') text = text_content if not light: text = tricky_text_content @@ -109,13 +102,9 @@ class Meta: title = factory.Sequence(lambda n: 'Mon extrait No{0}'.format(n + 1)) @classmethod - def _prepare(cls, create, **kwargs): + def _prepare(cls, create, *, light=True, container=None, **kwargs): db_object = kwargs.pop('db_object', None) - parent = kwargs.pop('container', None) - - light = True - if 'light' in kwargs: - light = kwargs.pop('light') + parent = container text = text_content if not light: text = tricky_text_content diff --git a/zds/tutorialv2/models/versioned.py b/zds/tutorialv2/models/versioned.py index ff2ebf7b30..b3ceee1820 100644 --- a/zds/tutorialv2/models/versioned.py +++ b/zds/tutorialv2/models/versioned.py @@ -794,6 +794,14 @@ def requires_validation(self): """ return self.type in CONTENT_TYPES_REQUIRING_VALIDATION + def remove_children(self, children_slugs): + for slug in children_slugs: + if slug not in self.children_dict: + continue + to_be_remove = self.children_dict[slug] + self.children.remove(to_be_remove) + del self.children_dict[slug] + def _dump_html(self, file_path, content, db_object): try: with file_path.open('w', encoding='utf-8') as f: diff --git a/zds/tutorialv2/publication_utils.py b/zds/tutorialv2/publication_utils.py index f5e5fbbcc2..56b3bd82b7 100644 --- a/zds/tutorialv2/publication_utils.py +++ b/zds/tutorialv2/publication_utils.py @@ -490,7 +490,6 @@ def publish(self, md_file_path, base_name, silently_pass=True, **kwargs): class FailureDuringPublication(Exception): """Exception raised if something goes wrong during publication process """ - def __init__(self, *args, **kwargs): super(FailureDuringPublication, self).__init__(*args, **kwargs) diff --git a/zds/tutorialv2/publish_container.py b/zds/tutorialv2/publish_container.py index 295fbc1968..74ab1d45b7 100644 --- a/zds/tutorialv2/publish_container.py +++ b/zds/tutorialv2/publish_container.py @@ -69,13 +69,16 @@ def publish_container(db_object, base_dir, container, template='tutorialv2/expor parsed = emarkdown(container.get_introduction(), db_object.js_support) container.introduction = str(part_path) write_chapter_file(base_dir, container, part_path, parsed, path_to_title_dict, image_callback) - - for i, child in enumerate(copy.copy(container.children)): + children = copy.copy(container.children) + container.children = [] + container.children_dict = {} + for child in filter(lambda c: c.ready_to_publish, children): altered_version = copy.copy(child) - container.children[i] = altered_version + container.children.append(altered_version) container.children_dict[altered_version.slug] = altered_version - path_to_title_dict.update(publish_container(db_object, base_dir, altered_version, file_ext=file_ext, - image_callback=image_callback)) + result = publish_container(db_object, base_dir, altered_version, file_ext=file_ext, + image_callback=image_callback) + path_to_title_dict.update(result) if container.conclusion and container.get_conclusion(): part_path = Path(container.get_prod_path(relative=True), 'conclusion.' + file_ext) parsed = emarkdown(container.get_conclusion(), db_object.js_support) diff --git a/zds/tutorialv2/tests/__init__.py b/zds/tutorialv2/tests/__init__.py index 19ef2e9462..cd17ac450a 100644 --- a/zds/tutorialv2/tests/__init__.py +++ b/zds/tutorialv2/tests/__init__.py @@ -1,6 +1,11 @@ from django.conf import settings import shutil +from django.urls import reverse +from selenium.webdriver.support.wait import WebDriverWait + +from zds.tutorialv2.models.database import Validation + class TutorialTestMixin: def clean_media_dir(self): @@ -11,3 +16,73 @@ def clean_media_dir(self): def tearDown(self): self.clean_media_dir() + + +class TutorialFrontMixin: + def login(self, profile): + """ + TODO: This is definitely way too slow. Fasten this. + """ + selenium = self.selenium + find_element = selenium.find_element_by_css_selector + + selenium.get(self.live_server_url + reverse('member-login')) + + username = find_element('.content-container input#id_username') + password = find_element('.content-container input#id_password') + username.send_keys(profile.user.username) + password.send_keys('hostel77') + + find_element('.content-container button[type=submit]').click() + + # Wait until the user is logged in (this raises if the element + # is not found). + + find_element('.header-container .logbox .my-account .username') + + def login_author(self): + self.login(self.user_author.profile) + + def login_staff(self): + self.login(self.user_staff.profile) + + def logout(self): + find_element = self.selenium.find_element_by_css_selector + find_element('#my-account').click() + find_element('form[action="/membres/deconnexion/"] button').click() + + def ask_validation(self): + find_element = self.selenium.find_element_by_css_selector + self.selenium.get(self.live_server_url + self.content.get_absolute_url()) + find_element('a[href="#ask-validation"]').click() + find_element('#id_text').send_keys('Coucou.') + find_element('#ask-validation button[type="submit"]').click() + + def take_reservation(self): + find_element = self.selenium.find_element_by_css_selector + self.selenium.get(self.live_server_url + self.content.get_absolute_url()) + validation = Validation.objects.filter(content=self.content).first() + find_element('form[action="/validations/reserver/{}/"] button'.format(validation.pk)).click() + + def validate(self): + find_element = self.selenium.find_element_by_css_selector + self.selenium.get(self.live_server_url + self.content.get_absolute_url()) + validation = Validation.objects.filter(content=self.content).first() + find_element('a[href="#valid-publish"]').click() + find_element('form#valid-publish #id_text').send_keys('Coucou.') + find_element('form[action="/validations/accepter/{}/"] button'.format(validation.pk)).click() + + def wait_element_attribute_change(self, locator, attribute, initial_value, time): + return WebDriverWait(self.selenium, time) \ + .until(AttributeHasChanged(locator, attribute, initial_value)) + + +class AttributeHasChanged: + def __init__(self, locator, attribute_name, initial_value): + self.locator = locator + self.attribute_name = attribute_name + self.initial_value = initial_value + + def __call__(self, driver): + element = driver.find_element(*self.locator) + return element.get_attribute(self.attribute_name) != self.initial_value diff --git a/zds/tutorialv2/tests/tests_front.py b/zds/tutorialv2/tests/tests_front.py new file mode 100644 index 0000000000..dda39d8337 --- /dev/null +++ b/zds/tutorialv2/tests/tests_front.py @@ -0,0 +1,98 @@ +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.test import override_settings +from django.urls import reverse +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.webdriver import WebDriver +from django.test import tag +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from zds.member.factories import StaffProfileFactory, ProfileFactory +from zds.tutorialv2.factories import LicenceFactory, SubCategoryFactory, PublishableContentFactory, ContainerFactory, \ + ExtractFactory +from zds.tutorialv2.models.database import PublishedContent, PublishableContent +from zds.tutorialv2.tests import TutorialTestMixin, TutorialFrontMixin +from copy import deepcopy +from django.conf import settings +import os + +overridden_zds_app = deepcopy(settings.ZDS_APP) +overridden_zds_app['content']['repo_private_path'] = os.path.join(settings.BASE_DIR, 'contents-private-test') +overridden_zds_app['content']['repo_public_path'] = os.path.join(settings.BASE_DIR, 'contents-public-test') + + +@override_settings(MEDIA_ROOT=os.path.join(settings.BASE_DIR, 'media-test')) +@override_settings(ZDS_APP=overridden_zds_app) +@override_settings(ES_ENABLED=False) +@tag('front') +class PublicationFronttest(StaticLiveServerTestCase, TutorialTestMixin, TutorialFrontMixin): + @classmethod + def setUpClass(cls): + super(PublicationFronttest, cls).setUpClass() + cls.selenium = WebDriver() + cls.selenium.implicitly_wait(10) + + @classmethod + def tearDownClass(cls): + cls.selenium.quit() + super(PublicationFronttest, cls).tearDownClass() + + def tearDown(self): + super().tearDown() + self.clean_media_dir() + + def setUp(self): + self.overridden_zds_app = overridden_zds_app + # don't build PDF to speed up the tests + overridden_zds_app['content']['build_pdf_when_published'] = False + + self.staff = StaffProfileFactory().user + + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + self.mas = ProfileFactory().user + overridden_zds_app['member']['bot_account'] = self.mas.username + + self.licence = LicenceFactory() + self.subcategory = SubCategoryFactory() + + self.user_author = ProfileFactory().user + self.user_staff = StaffProfileFactory().user + self.user_guest = ProfileFactory().user + self.content = PublishableContentFactory(author_list=[self.user_author], light=False) + self.part_published = ContainerFactory(db_object=self.content, light=False, parent=self.content.load_version()) + self.ignored_part = ContainerFactory(db_object=self.content, light=False, parent=self.content.load_version()) + ExtractFactory(db_object=self.content, container=self.part_published, light=False) + ExtractFactory(db_object=self.content, container=self.ignored_part, light=False) + + def test_partial_publication(self): + self.login_author() + self.selenium.get(self.live_server_url + self.ignored_part.get_absolute_url()) + find_element = self.selenium.find_element_by_css_selector + button = WebDriverWait(self.selenium, 20)\ + .until(expected_conditions.element_to_be_clickable((By.CSS_SELECTOR, '.readiness'))) + readiness = button.get_attribute('data-is-ready') + button.click() + self.wait_element_attribute_change((By.CSS_SELECTOR, '.readiness'), 'data-is-ready', readiness, 20) + self.content = PublishableContent.objects.get(pk=self.content.pk) + self.ignored_part = self.content.load_version().children[1] + self.assertFalse(self.ignored_part.ready_to_publish, 'part should be marked as not ready to publish') + self.selenium.get(self.live_server_url + self.content.get_absolute_url()) + self.selenium.get(self.live_server_url + self.ignored_part.get_absolute_url()) + button = find_element('.readiness') + self.assertNotEqual(readiness, button.get_attribute('data-is-ready'), + 'part should be marked as not ready to publish') + self.selenium.get(self.live_server_url + self.content.get_absolute_url()) + self.ask_validation() + self.logout() + self.login_staff() + self.take_reservation() + self.validate() + url = PublishedContent.objects.get(content__pk=self.content.pk).get_absolute_url_online() + self.selenium.get(self.live_server_url + url) + self.assertRaises(WebDriverException, find_element, 'a[href="{}"]'.format( + reverse('tutorial:view-container', kwargs={ + 'slug': self.content.slug, + 'pk': self.content.pk, + 'container_slug': self.ignored_part.slug + }))) diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index c7b9d41ea7..0d811aa449 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -7,7 +7,6 @@ from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse -from pathlib import Path from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, LicenceFactory, ExtractFactory, \ diff --git a/zds/tutorialv2/utils.py b/zds/tutorialv2/utils.py index 42613bdae6..95a17a06f3 100644 --- a/zds/tutorialv2/utils.py +++ b/zds/tutorialv2/utils.py @@ -789,7 +789,7 @@ def __init__(self, reason): class FailureDuringPublication(Exception): - """Exception raised if something goes wrong during publication process + """Exception raised if something goes wrong during the publication process """ def __init__(self, *args, **kwargs):