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 @@
@@ -87,7 +90,7 @@
ПРОДОЛЖИТЕЛЬНОСТЬ ДОСТУПА
+ @input="$v.course.access_duration.$touch()" name="course-access-duration">
@@ -96,14 +99,16 @@
@@ -111,13 +116,13 @@
@@ -138,13 +145,15 @@
@@ -156,12 +165,12 @@