diff --git a/apps/auth/__init__.py b/apps/auth/__init__.py index e69de29b..193dde43 100644 --- a/apps/auth/__init__.py +++ b/apps/auth/__init__.py @@ -0,0 +1 @@ +default_app_config = 'apps.auth.apps.AuthConfig' diff --git a/apps/auth/migrations/0001_initial.py b/apps/auth/migrations/0001_initial.py new file mode 100644 index 00000000..04bbcee2 --- /dev/null +++ b/apps/auth/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.7 on 2019-02-19 18:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='TempToken', + fields=[ + ('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='auth_temp_token', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + ), + ] diff --git a/apps/auth/models.py b/apps/auth/models.py index d95d3f20..8fbac91a 100644 --- a/apps/auth/models.py +++ b/apps/auth/models.py @@ -1,8 +1,29 @@ +import binascii +import os + from django.db import models +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ -from rest_framework.authtoken.models import Token +class TempToken(models.Model): + key = models.CharField(_("Key"), max_length=40, primary_key=True) + user = models.OneToOneField( + settings.AUTH_USER_MODEL, related_name='auth_temp_token', + on_delete=models.CASCADE, verbose_name=_("User") + ) + created = models.DateTimeField(_("Created"), auto_now_add=True) -class TempToken(Token): class Meta: - app_label = 'auth' + app_label = 'lilcity_auth' + + def save(self, *args, **kwargs): + if not self.key: + self.key = self.generate_key() + return super().save(*args, **kwargs) + + def generate_key(self): + return binascii.hexlify(os.urandom(20)).decode() + + def __str__(self): + return self.key diff --git a/apps/content/tests.py b/apps/content/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/content/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/content/tests/__init__.py b/apps/content/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/content/tests/mixins.py b/apps/content/tests/mixins.py new file mode 100644 index 00000000..4e9dcc31 --- /dev/null +++ b/apps/content/tests/mixins.py @@ -0,0 +1,144 @@ +import time + +from factory.faker import Faker +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.common.by import By +from django.utils.html import strip_tags + + +class TestContentMixin: + content_data = [] + + def check_saving(self): + if not self.autosave: + return + raise NotImplementedError() + + def check_content_saving(self, obj): + print('Check content saving',) + self.assertEqual(obj.content.all().count(), len(self.content_data)) + for i, item in enumerate(obj.content.all().order_by('position', '-created_at',)): + item_data = self.content_data[i].get('data') + for key, value in item_data.items(): + if key == 'txt': + self.assertEqual(strip_tags(getattr(item, key)), value) + else: + self.assertEqual(getattr(item, key), value) + print('OK') + + def check_content(self, inside_el=None): + print('Check content block') + self.object_data['content'] = self.content_data + block_add_el = self.wait_elem_name('block-add', inside_el) + open_el = self.wait_elem_name('block-add-open', block_add_el) + self.assertRaises(TimeoutException, lambda: self.wait_elem_name('block-add-close', block_add_el)) + open_el.click() + time.sleep(0.5) + close_el = self.wait_elem_name('block-add-close', block_add_el) + + block_text_el = self.wait_elem_name('block-add-block-text', block_add_el) + block_text_el.click() + time.sleep(0.5) + self.check_block_text(inside_el) + + open_el.click() + time.sleep(0.5) + block_image_el = self.wait_elem_name('block-add-block-image', block_add_el) + block_image_el.click() + time.sleep(0.5) + self.check_block_image(inside_el) + + open_el.click() + time.sleep(0.5) + block_image_text_el = self.wait_elem_name('block-add-block-image-text', block_add_el) + block_image_text_el.click() + time.sleep(0.5) + self.check_block_image_text(inside_el) + + open_el.click() + time.sleep(0.5) + block_images_el = self.wait_elem_name('block-add-block-images', block_add_el) + block_images_el.click() + time.sleep(0.5) + self.check_block_images(inside_el) + + open_el.click() + time.sleep(0.5) + block_video_el = self.wait_elem_name('block-add-block-video', block_add_el) + block_video_el.click() + time.sleep(0.5) + self.check_block_video(inside_el) + + def check_block_text(self, inside_el=None): + print('Check block text') + time.sleep(1) + block_obj = {'type': 'text', 'data': {}} + block_el = self.wait_elem_name('block-text', inside_el) + title_el = self.wait_elem_name('block-text-title', block_el) + text_el = self.wait_elem_name('block-text-text-wrap', block_el).find_element( + By.XPATH, './/div[contains(@class, "redactor-layer")][@contenteditable]') + self.content_data.append(block_obj) + + title = Faker('sentence', nb_words=6).generate({}) + title_el.send_keys(title) + block_obj['data']['title'] = title + text = Faker('sentence', nb_words=50).generate({}) + text_el.click() + text_el.send_keys(text) + block_obj['data']['txt'] = text + self.check_saving() + + def check_block_image(self, inside_el=None): + return + print('Check block image') + time.sleep(1) + block_obj = {'type': 'image', 'data': {}} + block_el = self.wait_elem_name('block-image', inside_el) + title_el = self.wait_elem_name('block-image-title', block_el) + image_el = self.wait_elem_name('block-image-image', block_el) + self.content_data.append(block_obj) + + title = Faker('sentence', nb_words=6).generate({}) + title_el.send_keys(title) + block_obj['data']['title'] = title + # TODO: check image upload + self.check_saving() + + def check_block_image_text(self, inside_el=None): + return + print('Check block image-text') + time.sleep(1) + block_obj = {'type': 'image-text', 'data': {}} + block_el = self.wait_elem_name('block-image-text', inside_el) + title_el = self.wait_elem_name('block-image-text-title', block_el) + text_el = self.wait_elem_name('block-image-text-text-wrap', block_el).find_element( + By.XPATH, './/div[contains(@class, "redactor-layer")][@contenteditable]') + image_el = self.wait_elem_name('block-image-text-image', block_el) + self.content_data.append(block_obj) + + title = Faker('sentence', nb_words=6).generate({}) + title_el.send_keys(title) + block_obj['data']['title'] = title + text = Faker('sentence', nb_words=50).generate({}) + text_el.click() + text_el.send_keys(text) + block_obj['data']['txt'] = text + # TODO: check image upload + self.check_saving() + + def check_block_images(self, inside_el=None): + print('Check block images') + time.sleep(1) + block_obj = {'type': 'images', 'data': {}} + self.content_data.append(block_obj) + # block_el = self.wait_elem_name('block-images', inside_el) + # title_el = self.wait_elem_name('block-images-title', block_el) + # title = Faker('sentence', nb_words=6).generate({}) + # title_el.send_keys(title) + # block_obj['data']['title'] = title + + self.check_saving() + + def check_block_video(self, inside_el=None): + return + print('Check block images') diff --git a/apps/course/migrations/0022_auto_20180208_0647.py b/apps/course/migrations/0022_auto_20180208_0647.py index c95cd3fc..54fc0991 100644 --- a/apps/course/migrations/0022_auto_20180208_0647.py +++ b/apps/course/migrations/0022_auto_20180208_0647.py @@ -8,6 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('course', '0021_auto_20180206_0632'), + ('content', '0005_auto_20180208_0520'), ] operations = [ diff --git a/apps/course/templates/course/course_edit.html b/apps/course/templates/course/course_edit.html index 83990c00..5fc6666a 100644 --- a/apps/course/templates/course/course_edit.html +++ b/apps/course/templates/course/course_edit.html @@ -17,7 +17,8 @@ {% endblock header_buttons %} {% block content %} - diff --git a/apps/course/tests.py b/apps/course/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/course/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/course/tests/test_views.py b/apps/course/tests/test_views.py new file mode 100644 index 00000000..b770ae47 --- /dev/null +++ b/apps/course/tests/test_views.py @@ -0,0 +1,196 @@ +from datetime import timedelta +from random import randint +import time +import re + +from selenium.common.exceptions import TimeoutException +from factory.faker import Faker +from django.utils import timezone +from django.test import TestCase +from django.shortcuts import reverse +from django.utils.text import slugify +from unidecode import unidecode +from selenium.webdriver.common.by import By +from django.utils.html import strip_tags + +from project.tests.factories import create_admin, create_batch_unique, User, UserFactory, Course, CourseFactory, login_admin +from project.tests import SeleniumTestCase +from apps.content.tests.mixins import TestContentMixin + + +class CoursesTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + create_admin() + create_batch_unique(CourseFactory, status=Course.STATUS_CHOICES[:3], price=[0, 1000], + age=Course.AGE_CHOICES[:2], deferred_start_at=[None, timezone.now() + timedelta(days=randint(5, 15))]) + + def test_courses_url_accessible(self): + print('test_courses_url_accessible') + print('get ', reverse('courses')) + resp = self.client.get(reverse('courses')) + self.assertEqual(resp.status_code, 200) + + def test_course_url_accessible(self): + print('test_course_url_accessible') + for course in Course.objects.filter(status=Course.PUBLISHED): + print('get ', course.url) + resp = self.client.get(course.url) + self.assertEqual(resp.status_code, 200) + + @login_admin + def test_course_edit_url_accessible(self): + print('test_course_edit_url_accessible') + for course in Course.objects.all(): + print('get ', reverse('course_edit', args=[course.id])) + resp = self.client.get(reverse('course_edit', args=[course.id])) + self.assertEqual(resp.status_code, 200) + + +class CourseEditTestCase(TestContentMixin, SeleniumTestCase): + model = Course + object_id = None + object_data = {} + + @classmethod + def setUpClass(cls): + super().setUpClass() + create_admin() + UserFactory.create_batch(5, role=User.AUTHOR_ROLE) + + def check_saving(self, url=None, response_code=200): + print('Check saving') + time.sleep(1) + request = self.wait_for_request(url or f'/api/v1/courses/{self.object_id}/') + self.assertEqual(request[2].get('status'), response_code) + self.assertNotEqual(request[2].get('response'), None) + obj = self.model.objects.get(id=self.object_id) + for key, value in self.object_data.items(): + if key not in ['content', 'lessons']: + self.assertEqual(getattr(obj, key), value) + self.check_content_saving(obj) + self.check_lessons_saving(obj) + + def check_lessons_saving(self, obj): + print('Check lessons saving', ) + lessons = self.object_data.get('lessons', []) + self.assertEqual(obj.lessons.all().count(), len(lessons)) + for i, lesson in enumerate(obj.lessons.all().order_by('title',)): + self.assertEqual(strip_tags(lesson.short_description), lessons[i]['short_description']) + self.assertEqual(lesson.title, lessons[i]['title']) + print('OK') + + def test_course_edit(self): + print('test_course_edit') + user = User.objects.filter(role=User.AUTHOR_ROLE).first() + self.login(user) + url = self.get_url(reverse('course_create')) + print('url is', url) + self.driver.get(url) + print('page opened', self.driver.current_url) + self.add_requests_log() + + course_redactor = self.wait_elem_name('course-redactor') + # visible always elements + title_el = self.wait_elem_name('course-title') + slug_el = self.wait_elem_name('course-slug') + category_el = self.wait_elem_name('course-category') + is_paid_no_el = self.wait_elem_name('course-is-paid-no') + is_paid_yes_el = self.wait_elem_name('course-is-paid-yes') + is_paid_input_el = self.wait_elem_css('[name=course-is-paid-input]:checked') + age_el = self.wait_elem_name('course-age') + # Only for ADMIN + # is_featured_el = self.wait_elem_name('course-is-featured') + is_deferred_no_el = self.wait_elem_name('course-is-deferred-no') + is_deferred_yes_el = self.wait_elem_name('course-is-deferred-yes') + is_deferred_input_el = self.wait_elem_name('course-is-deferred-input') + content_btn_el = self.wait_elem_name('course-content-btn') + lessons_btn_el = self.wait_elem_name('course-lessons-btn') + content_el = self.wait_elem_name('course-content') + + title = Faker('sentence', nb_words=6).generate({}) + title_el.send_keys(title) + slug = slugify(unidecode(title[:90])) + slug = re.sub(r'[^-\w]+$', '', slug) + slug = re.sub(r'[^-\w]', '-', slug) + self.object_data['title'] = title + self.object_data['slug'] = slug + time.sleep(1) + self.assertEqual(slug_el.get_attribute('value'), slug) + + # check save + obj = self.model.objects.get(title=title, slug=slug) + self.assertEqual(bool(obj), True) + self.object_id = obj.id + + self.assertEqual(is_paid_input_el.get_attribute('value'), 'false') + self.assertRaises(TimeoutException, callable=lambda: self.driver.find_element_by_name('course-price')) + self.assertRaises(TimeoutException, callable=lambda: self.driver.find_element_by_name('course-old-price')) + self.assertRaises(TimeoutException, callable=lambda: self.driver.find_element_by_name('course-access-duration')) + is_paid_yes_el.click() + time.sleep(1) + is_paid_input_el = self.wait_elem_css('[name=course-is-paid-input]:checked') + self.assertEqual(is_paid_input_el.get_attribute('value'), 'true') + try: + price_el = self.wait_elem_name('course-price') + old_price_el = self.wait_elem_name('course-old-price') + access_duration_el = self.wait_elem_name('course-access-duration') + except: + self.fail('Price, old price and access_duration elements not shown') + + self.assertEqual(is_deferred_input_el.get_attribute('checked'), 'true') + self.assertRaises(TimeoutException, callable=lambda: self.driver.find_element_by_name('course-date')) + self.assertRaises(TimeoutException, callable=lambda: self.driver.find_element_by_name('course-time')) + is_deferred_yes_el.click() + try: + date_el = self.wait_elem_name('course-date') + time_el = self.wait_elem_name('course-time') + except: + self.fail('Date and time elements not shown') + + price_el.send_keys(1000) + self.object_data['price'] = 1000 + old_price_el.send_keys(1500) + self.object_data['old_price'] = 1500 + access_duration_el.send_keys(15) + self.object_data['access_duration'] = 15 + self.check_saving() + + self.check_content(content_el) + + lessons_btn_el.click() + time.sleep(0.5) + # lessons_el = self.wait_elem_name('course-lessons') + # lesson_edit_el = self.wait_elem_name('course-lesson-edit') + # stream_el = self.wait_elem_name('course-stream') + add_lesson_el = self.wait_elem_name('course-add-lesson') + + add_lesson_el.click() + time.sleep(0.5) + + lesson_title_el = self.wait_elem_name('course-lesson-title') + lesson_text_el = self.wait_elem_name('course-lesson-text-wrap').find_element( + By.XPATH, './/div[contains(@class, "redactor-layer")][@contenteditable]') + lesson_save_btn_el = self.wait_elem_name('course-lesson-save') + + lesson = {} + title = Faker('sentence', nb_words=6).generate({}) + lesson_title_el.send_keys(title) + lesson['title'] = title + + text = Faker('sentence', nb_words=50).generate({}) + lesson_text_el.click() + lesson_text_el.send_keys(text) + time.sleep(0.5) + + lesson_save_btn_el.click() + self.object_data['lessons'] = [{ + 'title': title, + 'short_description': text + }] + self.check_saving('/api/v1/lessons/', 201) + + + + diff --git a/project/settings.py b/project/settings.py index 9f835203..edfcdf90 100644 --- a/project/settings.py +++ b/project/settings.py @@ -59,7 +59,7 @@ INSTALLED_APPS = [ 'django_user_agents', 'imagekit', ] + [ - 'apps.auth.apps', + 'apps.auth', 'apps.user', 'apps.notification', 'apps.payment', diff --git a/project/tests/__init__.py b/project/tests/__init__.py new file mode 100644 index 00000000..0bf7f1ed --- /dev/null +++ b/project/tests/__init__.py @@ -0,0 +1,65 @@ +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.common.by import By +from pyvirtualdisplay import Display +from django.conf import settings +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from project.utils.selenium_utils import SeleniumExtensions as SE +from apps.auth.models import TempToken + + +class SeleniumTestCase(LiveServerTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.display = Display(visible=0, size=(1280, 1024)) + cls.display.start() + cls.driver = webdriver.Firefox() + + @classmethod + def tearDownClass(cls): + cls.driver.quit() + cls.display.stop() + super().tearDownClass() + + def get_url(self, url=''): + return '%s%s' % (self.live_server_url, url) + + def wait_elem_xpath(self, xpath, inside_el=None, wait_time=10): + return SE.wait_elem(self.driver, (By.XPATH, xpath), inside_el, wait_time) + + def wait_elems_xpath(self, xpath, inside_el=None, wait_time=10): + return SE.wait_elems(self.driver, (By.XPATH, xpath), inside_el, wait_time) + + def wait_elem_css(self, selector, inside_el=None, wait_time=10): + return SE.wait_elem(self.driver, (By.CSS_SELECTOR, selector), inside_el, wait_time) + + def wait_elems_css(self, selector, inside_el=None, wait_time=10): + return SE.wait_elems(self.driver, (By.CSS_SELECTOR, selector), inside_el, wait_time) + + def wait_elem_name(self, name, inside_el=None, wait_time=10): + return SE.wait_elem(self.driver, (By.NAME, name), inside_el, wait_time) + + def wait_elem_id(self, id, inside_el=None, wait_time=10): + return SE.wait_elem(self.driver, (By.ID, id), inside_el, wait_time) + + def wait_elems_name(self, name, inside_el=None, wait_time=10): + return SE.wait_elems(self.driver, (By.NAME, name), inside_el, wait_time) + + def login(self, user): + TempToken.objects.all().delete() + tt = TempToken.objects.create(user=user) + self.driver.get('%s?temp-token=%s' % (self.get_url(), tt.key)) + + def add_requests_log(self): + SE.add_requests_log(self.driver) + + def get_requests(self): + return SE.get_requests(self.driver) + + def wait_for_request(self, path, wait_time=10): + return SE.wait_for_request(self.driver, path, wait_time) + diff --git a/project/tests/factories.py b/project/tests/factories.py new file mode 100644 index 00000000..a56fd68b --- /dev/null +++ b/project/tests/factories.py @@ -0,0 +1,96 @@ +from itertools import combinations + +from factory.django import DjangoModelFactory, ImageField +import factory +import factory.fuzzy +from unidecode import unidecode +from django.utils.text import slugify + + +from apps.course.models import * +from apps.content.models import * +from apps.user.models import * + + +ADMIN_EMAIL = 'admin@mail.com' + + +def create_admin(): + admin = UserFactory(username=ADMIN_EMAIL, email=ADMIN_EMAIL, role=User.ADMIN_ROLE, + is_staff=True, is_superuser=True) + admin.set_password('admin') + admin.save() + return admin + + +def create_users(count_multiplier=1): + create_admin() + UserFactory.create_batch(10 * count_multiplier, role=User.USER_ROLE) + UserFactory.create_batch(5 * count_multiplier, role=User.AUTHOR_ROLE) + UserFactory.create_batch(5 * count_multiplier, role=User.TEACHER_ROLE) + UserFactory.create_batch(5, role=User.ADMIN_ROLE, is_staff=True) + + +def login_admin(fn): + def wrap(self, *args, **kwargs): + admin = User.objects.get(username=ADMIN_EMAIL) + self.client.force_login(admin) + try: + fn(self, *args, **kwargs) + finally: + self.client.logout() + return wrap + + +def create_batch_unique(factory_class, **kwargs): + model = factory_class._meta.model + values = [] + for k, v in kwargs.items(): + try: + f = model._meta.get_field(k) + except: + del kwargs[k] + if getattr(f, 'choices', None): + v = [c[0] for c in v] + values += ((k, val) for val in v) + for params in combinations(values, len(kwargs)): + data = dict(params) + if len(data) == len(kwargs): + factory_class(**data) + + +class ImageObjectFactory(DjangoModelFactory): + image = ImageField() + image_thumbnail = ImageField() + + +class UserFactory(DjangoModelFactory): + class Meta: + model = User + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + email = factory.Sequence(lambda n: "test_user%d@mail.com" % n) + username = factory.LazyAttribute(lambda o: o.email) + gallery = None + photo = None #factory.SubFactory(ImageObjectFactory) + is_active = True + auth_token = None + + +class CategoryFactory(DjangoModelFactory): + class Meta: + model = Category + + +class CourseFactory(DjangoModelFactory): + class Meta: + model = Course + + author = factory.SubFactory(UserFactory, role=User.AUTHOR_ROLE) + title = factory.Faker('sentence', nb_words=6) + slug = factory.LazyAttribute(lambda o: slugify(unidecode(o.title[:90]))) + category = factory.SubFactory(CategoryFactory) + cover = None #factory.SubFactory(ImageObjectFactory) + gallery = None + status = factory.Iterator([s[0] for s in Course.STATUS_CHOICES]) diff --git a/project/utils/selenium_utils.py b/project/utils/selenium_utils.py new file mode 100644 index 00000000..3ad4ab17 --- /dev/null +++ b/project/utils/selenium_utils.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class ElementIn(object): + + default_find_fn = 'find_element' + + def __init__(self, element, locator, find_fn=None): + self.element = element + self.locator = locator + self.find_fn = self.default_find_fn if find_fn is None else find_fn + + def __call__(self, driver): + try: + return getattr(self.element, self.find_fn)(*self.locator) + except: + return False + + +class ElementsIn(ElementIn): + + default_find_fn = 'find_elements' + + +class SeleniumExtensions(object): + + @classmethod + def wait_elem(cls, driver, locator, inside_el=None, wait_time=10): + if inside_el: + return WebDriverWait(driver, wait_time).until(ElementIn(inside_el, locator)) + return WebDriverWait(driver, wait_time).until( + EC.presence_of_element_located(locator) + ) + + @classmethod + def wait_elems(cls, driver, locator, inside_el=None, wait_time=10): + if inside_el: + return WebDriverWait(driver, wait_time).until(ElementsIn(inside_el, locator)) + return WebDriverWait(driver, wait_time).until( + EC.presence_of_all_elements_located(locator) + ) + + @classmethod + def wait_for_js_load(cls, driver, wait_time=10): + WebDriverWait(driver, wait_time).until( + lambda driver: driver.execute_script('return document.readyState') == 'complete') + + @classmethod + def add_requests_log(cls, driver): + js = ''' + (function() { + var open = XMLHttpRequest.prototype.open; + var send = XMLHttpRequest.prototype.send; + window._requestsLog = []; + window._findRequest = function(pathOrXHR){ + for(var i=window._requestsLog.length - 1; i >= 0; i--){ + var request = window._requestsLog[i]; + if(typeof(pathOrXHR) == 'string' && request[1].indexOf(pathOrXHR) > -1 + || request[2] === pathOrXHR){ + return request; + } + } + } + XMLHttpRequest.prototype.open = function(method, url){ + window._requestsLog.push([method, url, this]); + return open.apply(this, arguments); + } + XMLHttpRequest.prototype.send = function(body){ + var r = window._findRequest(this); + if(r){ + r.push(body); + } + return send.apply(this, arguments); + } + })(); + ''' + return driver.execute_script(js) + + @classmethod + def get_requests(cls, driver): + return driver.execute_script('return window._requestsLog;') + + @classmethod + def wait_for_request(cls, driver, path, wait_time=10): + WebDriverWait(driver, wait_time).until( + lambda driver: driver.execute_script(''' + var r = window._findRequest(arguments[0]); + return !!r && r[2].readyState == 4; + ''', path) is True) + return driver.execute_script('return window._findRequest(arguments[0]);', path) diff --git a/requirements.txt b/requirements.txt index 78736651..c5d8b002 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,3 +37,17 @@ sendgrid drf_dynamic_fields flower==0.9.2 unidecode +factory-boy==2.11.1 +pyvirtualdisplay==0.2.1 +selenium + # sudo apt-get install xvfb + # sudo apt-get install libappindicator3-1 fonts-liberation + # wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + + # sudo apt-get install unzip + # wget -N https://chromedriver.storage.googleapis.com/73.0.3683.20/chromedriver_linux64.zip + # unzip chromedriver_linux64.zip + # chmod +x chromedriver + # sudo mv -f chromedriver /usr/local/share/chromedriver + # sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver + # sudo ln -s /usr/local/share/chromedriver /usr/bin/chromedriver diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 76ab9d93..54d61412 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -6,12 +6,13 @@
@@ -71,12 +72,14 @@
ДОСТУП
@@ -87,7 +90,7 @@
ПРОДОЛЖИТЕЛЬНОСТЬ ДОСТУПА
+ @input="$v.course.access_duration.$touch()" name="course-access-duration">
@@ -96,14 +99,16 @@
СТОИМОСТЬ
- +
СТОИМОСТЬ БЕЗ СКИДКИ
- +
@@ -111,13 +116,13 @@
ВОЗРАСТ
-
@@ -125,12 +130,14 @@
ЗАПУСК
@@ -138,13 +145,15 @@
ДАТА
- +
ВРЕМЯ
- +
@@ -156,12 +165,12 @@
- -
- +