├── README.md ├── accounts ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── admin.cpython-38.pyc │ ├── forms.cpython-38.pyc │ ├── models.cpython-38.pyc │ ├── urls.cpython-38.pyc │ └── views.cpython-38.pyc ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200923_0856.py │ ├── __init__.py │ └── __pycache__ │ │ ├── 0001_initial.cpython-38.pyc │ │ ├── 0002_auto_20200923_0856.cpython-38.pyc │ │ └── __init__.cpython-38.pyc ├── models.py ├── signals.py ├── tests.py ├── urls.py └── views.py ├── assets ├── css │ └── layout.css └── ss │ ├── a.png │ ├── b.png │ ├── c.png │ ├── d.png │ ├── e.png │ └── f.png ├── courses ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── admin.cpython-38.pyc │ ├── forms.cpython-38.pyc │ ├── models.cpython-38.pyc │ ├── tests.cpython-38.pyc │ ├── urls.cpython-38.pyc │ └── views.cpython-38.pyc ├── admin.py ├── apps.py ├── forms.py ├── js │ ├── order.js │ └── vendor │ │ └── jquery.fn.sortable.min.js ├── migrations │ ├── 0001_initial.py │ ├── 0002_step.py │ ├── 0003_auto_20200423_0950.py │ ├── 0004_auto_20200722_0841.py │ ├── 0005_quiz.py │ ├── 0006_auto_20200722_1016.py │ ├── 0007_answer.py │ ├── 0008_multiplechoicequestion.py │ ├── 0009_truefalsequestion.py │ ├── 0010_auto_20200807_1011.py │ ├── 0011_auto_20200922_0831.py │ ├── __init__.py │ └── __pycache__ │ │ ├── 0001_initial.cpython-38.pyc │ │ ├── 0002_step.cpython-38.pyc │ │ ├── 0003_auto_20200423_0950.cpython-38.pyc │ │ ├── 0004_auto_20200722_0841.cpython-38.pyc │ │ ├── 0005_quiz.cpython-38.pyc │ │ ├── 0006_auto_20200722_1016.cpython-38.pyc │ │ ├── 0007_answer.cpython-38.pyc │ │ ├── 0008_multiplechoicequestion.cpython-38.pyc │ │ ├── 0009_truefalsequestion.cpython-38.pyc │ │ ├── 0010_auto_20200807_1011.cpython-38.pyc │ │ ├── 0011_auto_20200922_0831.cpython-38.pyc │ │ └── __init__.cpython-38.pyc ├── models.py ├── static │ └── courses │ │ └── css │ │ ├── courses.css │ │ └── order.css ├── templates │ └── courses │ │ ├── answer_form.html │ │ ├── course_detail.html │ │ ├── course_list.html │ │ ├── course_nav.html │ │ ├── question_form.html │ │ ├── quiz_detail.html │ │ ├── quiz_form.html │ │ ├── step_detail.html │ │ └── text_detail.html ├── templatetags │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ └── course_extras.cpython-38.pyc │ └── course_extras.py ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── learning_site ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── forms.cpython-38.pyc │ ├── settings.cpython-38.pyc │ ├── urls.cpython-38.pyc │ ├── views.cpython-38.pyc │ └── wsgi.cpython-38.pyc ├── asgi.py ├── forms.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── media ├── default.jpg └── profile_pics │ └── FB_IMG_1563211510479.jpg ├── requirements.txt └── templates ├── home.html ├── layout.html ├── suggestion_form.html └── users ├── login.html ├── profile.html └── register.html /README.md: -------------------------------------------------------------------------------- 1 | # eLearning-Website-Django 2 | *A project on eLearning Site using Django and SQLite. Due to this pandemic situation, the activities of all educational institutions are being done online ![Django](https://img.shields.io/badge/-Django-%23092E20?style=flat-square&logo=Django&logoColor=white) ![SQLite](https://img.shields.io/badge/-SQLite-%23003B57?style=flat-square&logo=SQLite) ![Bootstrap](https://img.shields.io/badge/-Bootstrap-%23563D7C?style=flat-square&logo=Bootstrap)* 3 | 4 | # Features 5 | ``` 6 | Student Login System. 7 | Student Registration w/ email verification 8 | Discuss Comment Every Lesson make it more interactive ! 9 | Email Verification System. 10 | Student Data Management. 11 | Teacher Data Management. 12 | Lesson Data Management. 13 | Administrator Page for Data Management ( Students, Lesson and Teacher Data ) 14 | Student Page for learning. 15 | Teacher Page for uploading a lesson. 16 | Fancy Animations! 17 | Student can attempt a quiz 18 | User-friendly 19 | ``` 20 | 21 | # Future Work: 22 | ``` 23 | I will use React for Front-end in future. 24 | ``` 25 | 26 | # Tools 27 | ## Front-end Part 28 | ``` 29 | HTML 30 | CSS 31 | Bootstrap 32 | JavaScript 33 | ``` 34 | ## Back-end 35 | ``` 36 | Django 37 | SQLite 3 38 | ``` 39 | # Screenshots of the Project 40 |

41 | 42 | 43 | 44 | 45 | 46 | 47 |

48 | 49 | **Copyright (c)** 2020-3020 Md. Omar Faruk 50 | 51 | ## Go Through This Site Then You Will Know About This Site Properly. 52 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/__pycache__/admin.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/admin.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/__pycache__/forms.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/forms.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/__pycache__/models.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/models.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Profile 3 | 4 | # Register your models here. 5 | admin.site.register(Profile) -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | 7 | def ready(self): 8 | import accounts.signals 9 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth.forms import UserCreationForm 4 | from .models import Profile 5 | 6 | 7 | class SignUpForm(UserCreationForm): 8 | first_name = forms.CharField(max_length=30, required=False, help_text='Optional') 9 | last_name = forms.CharField(max_length=30, required=False, help_text='Optional') 10 | email = forms.EmailField(max_length=254, help_text='Enter a valid email address') 11 | 12 | class Meta: 13 | model = User 14 | fields = [ 15 | 'username', 16 | 'first_name', 17 | 'last_name', 18 | 'email', 19 | 'password1', 20 | 'password2', 21 | ] 22 | 23 | 24 | class UserUpdateForm(forms.ModelForm): 25 | email = forms.EmailField() # This For Additional Field 26 | 27 | class Meta: 28 | model = User 29 | fields = ['username', 'email'] 30 | 31 | 32 | class ProfileUpdateForm(forms.ModelForm): 33 | class Meta: 34 | model = Profile 35 | fields = ['image'] -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-09-22 02:31 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('image', models.ImageField(default='default.jpg', upload_to='profile_pics')), 22 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /accounts/migrations/0002_auto_20200923_0856.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-09-23 02:56 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('accounts', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='profile', 18 | name='image', 19 | field=models.ImageField(blank=True, default='default.jpg', null=True, upload_to='profile_pics'), 20 | ), 21 | migrations.AlterField( 22 | model_name='profile', 23 | name='user', 24 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/migrations/__pycache__/0001_initial.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/migrations/__pycache__/0001_initial.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/migrations/__pycache__/0002_auto_20200923_0856.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/migrations/__pycache__/0002_auto_20200923_0856.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/migrations/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/accounts/migrations/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | from django.contrib.auth.models import User 5 | 6 | class Profile(models.Model): 7 | user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True) 8 | image = models.ImageField(default='default.jpg', upload_to='profile_pics', null=True, blank=True) 9 | 10 | 11 | def __str__(self): 12 | return f'{self.user.username} Profile' 13 | -------------------------------------------------------------------------------- /accounts/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.contrib.auth.models import User 3 | from django.dispatch import receiver 4 | from .models import Profile 5 | 6 | @receiver(post_save, sender=User) 7 | def create_profile(sender, instance, created, **kwargs): 8 | if created: 9 | Profile.objects.create(user=instance) 10 | 11 | 12 | @receiver(post_save, sender=User) 13 | def save_profile(sender, instance, **kwargs): 14 | instance.profile.save() -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import SignUpView 3 | from django.contrib.auth import views as auth_views 4 | from . import views as user_views 5 | 6 | urlpatterns = [ 7 | path('register/', SignUpView.as_view(), name='register'), 8 | path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'), 9 | path('logout/', auth_views.LogoutView.as_view(next_page='home'), name='logout'), 10 | path('profile/', user_views.profile, name='profile'), 11 | ] -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib.auth.decorators import login_required 3 | from .forms import UserUpdateForm, ProfileUpdateForm 4 | 5 | # Create your views here. 6 | 7 | from django.contrib import messages 8 | 9 | 10 | 11 | from django.views.generic import CreateView 12 | from .forms import SignUpForm 13 | from django.urls import reverse_lazy 14 | 15 | class SignUpView(CreateView): 16 | form_class = SignUpForm 17 | success_url = reverse_lazy('login') # After Creating Login Form Then Put 'login' Here. 18 | template_name = 'users/register.html' 19 | 20 | 21 | @login_required 22 | def profile(request): 23 | if request.method == 'POST': # This Will Be Run When I Submit My Form. And Possibly Pass New Data. 24 | u_form = UserUpdateForm(request.POST, instance=request.user) # request.POST To Pass The POST Data 25 | p_form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user.profile) # File Data (images) Users Try To Upload. 26 | 27 | if u_form.is_valid() and p_form.is_valid(): 28 | u_form.save() 29 | p_form.save() 30 | messages.success(request, f'Your profile has been updated!') 31 | return redirect('profile') 32 | 33 | else: 34 | u_form = UserUpdateForm(instance=request.user) 35 | p_form = ProfileUpdateForm(instance=request.user.profile) 36 | 37 | context = { # To Pass This Into Template We Used context. 38 | 'u_form': u_form, 39 | 'p_form': p_form 40 | } 41 | 42 | return render(request, 'users/profile.html', context) 43 | -------------------------------------------------------------------------------- /assets/css/layout.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden; 6 | background-color: #333; 7 | } 8 | 9 | li { 10 | float: left; 11 | } 12 | 13 | li a { 14 | display: inline-block; 15 | color: white; 16 | text-align: center; 17 | padding: 14px 16px; 18 | text-decoration: none; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/ss/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/a.png -------------------------------------------------------------------------------- /assets/ss/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/b.png -------------------------------------------------------------------------------- /assets/ss/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/c.png -------------------------------------------------------------------------------- /assets/ss/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/d.png -------------------------------------------------------------------------------- /assets/ss/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/e.png -------------------------------------------------------------------------------- /assets/ss/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/assets/ss/f.png -------------------------------------------------------------------------------- /courses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__init__.py -------------------------------------------------------------------------------- /courses/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/admin.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/admin.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/forms.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/forms.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/models.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/models.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/tests.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/tests.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /courses/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /courses/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from . import models 4 | 5 | admin.site.register(models.Course) 6 | admin.site.register(models.Text) 7 | admin.site.register(models.Quiz) 8 | admin.site.register(models.MultipleChoiceQuestion) 9 | admin.site.register(models.TrueFalseQuestion) 10 | admin.site.register(models.Answer) 11 | -------------------------------------------------------------------------------- /courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoursesConfig(AppConfig): 5 | name = 'courses' 6 | -------------------------------------------------------------------------------- /courses/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from . import models 4 | 5 | class QuizForm(forms.ModelForm): 6 | class Meta: 7 | model = models.Quiz 8 | fields = [ 9 | 'title', 10 | 'description', 11 | 'order', 12 | 'total_questions', 13 | ] 14 | 15 | 16 | class QuestionForm(forms.ModelForm): 17 | class Media: 18 | css = {'all':('courses/css/order.css',)} 19 | js = ( 20 | 'courses/js/vendor/jquery.fn.sortable.min.js', 21 | 'courses/js/order.js' 22 | ) 23 | 24 | 25 | class TrueFalseQuestionForm(QuestionForm): 26 | class Meta: 27 | model = models.TrueFalseQuestion 28 | fields = [ 29 | 'order', 30 | 'prompt', 31 | ] 32 | 33 | 34 | class MultipleChoiceQuestionForm(QuestionForm): 35 | class Meta: 36 | model = models.MultipleChoiceQuestion 37 | fields = [ 38 | 'order', 39 | 'prompt', 40 | 'shuffle_answers', 41 | ] 42 | 43 | 44 | 45 | class AnswerForm(forms.ModelForm): 46 | class Meta: 47 | model = models.Answer 48 | fields = [ 49 | 'order', 50 | 'text', 51 | 'correct', 52 | ] 53 | 54 | AnswerFormSet = forms.modelformset_factory( 55 | models.Answer, 56 | form = AnswerForm, 57 | extra = 2, 58 | ) 59 | 60 | 61 | AnswerInlineFormSet = forms.inlineformset_factory( 62 | models.Question, 63 | models.Answer, 64 | extra = 2, 65 | fields = ('order', 'text', 'correct'), 66 | formset = AnswerFormSet, 67 | min_num = 1, 68 | ) 69 | -------------------------------------------------------------------------------- /courses/js/order.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * Sortable 3 | * @author RubaXa 4 | * @license MIT 5 | */ 6 | 7 | (function sortableModule(factory) { 8 | "use strict"; 9 | 10 | if (typeof define === "function" && define.amd) { 11 | define(factory); 12 | } 13 | else if (typeof module != "undefined" && typeof module.exports != "undefined") { 14 | module.exports = factory(); 15 | } 16 | else { 17 | /* jshint sub:true */ 18 | window["Sortable"] = factory(); 19 | } 20 | })(function sortableFactory() { 21 | "use strict"; 22 | 23 | if (typeof window === "undefined" || !window.document) { 24 | return function sortableError() { 25 | throw new Error("Sortable.js requires a window with a document"); 26 | }; 27 | } 28 | 29 | var dragEl, 30 | parentEl, 31 | ghostEl, 32 | cloneEl, 33 | rootEl, 34 | nextEl, 35 | lastDownEl, 36 | 37 | scrollEl, 38 | scrollParentEl, 39 | scrollCustomFn, 40 | 41 | lastEl, 42 | lastCSS, 43 | lastParentCSS, 44 | 45 | oldIndex, 46 | newIndex, 47 | 48 | activeGroup, 49 | putSortable, 50 | 51 | autoScroll = {}, 52 | 53 | tapEvt, 54 | touchEvt, 55 | 56 | moved, 57 | 58 | /** @const */ 59 | R_SPACE = /\s+/g, 60 | R_FLOAT = /left|right|inline/, 61 | 62 | expando = 'Sortable' + (new Date).getTime(), 63 | 64 | win = window, 65 | document = win.document, 66 | parseInt = win.parseInt, 67 | setTimeout = win.setTimeout, 68 | 69 | $ = win.jQuery || win.Zepto, 70 | Polymer = win.Polymer, 71 | 72 | captureMode = false, 73 | 74 | supportDraggable = ('draggable' in document.createElement('div')), 75 | supportCssPointerEvents = (function (el) { 76 | // false when IE11 77 | if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { 78 | return false; 79 | } 80 | el = document.createElement('x'); 81 | el.style.cssText = 'pointer-events:auto'; 82 | return el.style.pointerEvents === 'auto'; 83 | })(), 84 | 85 | _silent = false, 86 | 87 | abs = Math.abs, 88 | min = Math.min, 89 | 90 | savedInputChecked = [], 91 | touchDragOverListeners = [], 92 | 93 | _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { 94 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 95 | if (rootEl && options.scroll) { 96 | var _this = rootEl[expando], 97 | el, 98 | rect, 99 | sens = options.scrollSensitivity, 100 | speed = options.scrollSpeed, 101 | 102 | x = evt.clientX, 103 | y = evt.clientY, 104 | 105 | winWidth = window.innerWidth, 106 | winHeight = window.innerHeight, 107 | 108 | vx, 109 | vy, 110 | 111 | scrollOffsetX, 112 | scrollOffsetY 113 | ; 114 | 115 | // Delect scrollEl 116 | if (scrollParentEl !== rootEl) { 117 | scrollEl = options.scroll; 118 | scrollParentEl = rootEl; 119 | scrollCustomFn = options.scrollFn; 120 | 121 | if (scrollEl === true) { 122 | scrollEl = rootEl; 123 | 124 | do { 125 | if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || 126 | (scrollEl.offsetHeight < scrollEl.scrollHeight) 127 | ) { 128 | break; 129 | } 130 | /* jshint boss:true */ 131 | } while (scrollEl = scrollEl.parentNode); 132 | } 133 | } 134 | 135 | if (scrollEl) { 136 | el = scrollEl; 137 | rect = scrollEl.getBoundingClientRect(); 138 | vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); 139 | vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); 140 | } 141 | 142 | 143 | if (!(vx || vy)) { 144 | vx = (winWidth - x <= sens) - (x <= sens); 145 | vy = (winHeight - y <= sens) - (y <= sens); 146 | 147 | /* jshint expr:true */ 148 | (vx || vy) && (el = win); 149 | } 150 | 151 | 152 | if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { 153 | autoScroll.el = el; 154 | autoScroll.vx = vx; 155 | autoScroll.vy = vy; 156 | 157 | clearInterval(autoScroll.pid); 158 | 159 | if (el) { 160 | autoScroll.pid = setInterval(function () { 161 | scrollOffsetY = vy ? vy * speed : 0; 162 | scrollOffsetX = vx ? vx * speed : 0; 163 | 164 | if ('function' === typeof(scrollCustomFn)) { 165 | return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt); 166 | } 167 | 168 | if (el === win) { 169 | win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); 170 | } else { 171 | el.scrollTop += scrollOffsetY; 172 | el.scrollLeft += scrollOffsetX; 173 | } 174 | }, 24); 175 | } 176 | } 177 | } 178 | }, 30), 179 | 180 | _prepareGroup = function (options) { 181 | function toFn(value, pull) { 182 | if (value === void 0 || value === true) { 183 | value = group.name; 184 | } 185 | 186 | if (typeof value === 'function') { 187 | return value; 188 | } else { 189 | return function (to, from) { 190 | var fromGroup = from.options.group.name; 191 | 192 | return pull 193 | ? value 194 | : value && (value.join 195 | ? value.indexOf(fromGroup) > -1 196 | : (fromGroup == value) 197 | ); 198 | }; 199 | } 200 | } 201 | 202 | var group = {}; 203 | var originalGroup = options.group; 204 | 205 | if (!originalGroup || typeof originalGroup != 'object') { 206 | originalGroup = {name: originalGroup}; 207 | } 208 | 209 | group.name = originalGroup.name; 210 | group.checkPull = toFn(originalGroup.pull, true); 211 | group.checkPut = toFn(originalGroup.put); 212 | group.revertClone = originalGroup.revertClone; 213 | 214 | options.group = group; 215 | } 216 | ; 217 | 218 | 219 | /** 220 | * @class Sortable 221 | * @param {HTMLElement} el 222 | * @param {Object} [options] 223 | */ 224 | function Sortable(el, options) { 225 | if (!(el && el.nodeType && el.nodeType === 1)) { 226 | throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); 227 | } 228 | 229 | this.el = el; // root element 230 | this.options = options = _extend({}, options); 231 | 232 | 233 | // Export instance 234 | el[expando] = this; 235 | 236 | // Default options 237 | var defaults = { 238 | group: Math.random(), 239 | sort: true, 240 | disabled: false, 241 | store: null, 242 | handle: null, 243 | scroll: true, 244 | scrollSensitivity: 30, 245 | scrollSpeed: 10, 246 | draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', 247 | ghostClass: 'sortable-ghost', 248 | chosenClass: 'sortable-chosen', 249 | dragClass: 'sortable-drag', 250 | ignore: 'a, img', 251 | filter: null, 252 | preventOnFilter: true, 253 | animation: 0, 254 | setData: function (dataTransfer, dragEl) { 255 | dataTransfer.setData('Text', dragEl.textContent); 256 | }, 257 | dropBubble: false, 258 | dragoverBubble: false, 259 | dataIdAttr: 'data-id', 260 | delay: 0, 261 | forceFallback: false, 262 | fallbackClass: 'sortable-fallback', 263 | fallbackOnBody: false, 264 | fallbackTolerance: 0, 265 | fallbackOffset: {x: 0, y: 0} 266 | }; 267 | 268 | 269 | // Set default options 270 | for (var name in defaults) { 271 | !(name in options) && (options[name] = defaults[name]); 272 | } 273 | 274 | _prepareGroup(options); 275 | 276 | // Bind all private methods 277 | for (var fn in this) { 278 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 279 | this[fn] = this[fn].bind(this); 280 | } 281 | } 282 | 283 | // Setup drag mode 284 | this.nativeDraggable = options.forceFallback ? false : supportDraggable; 285 | 286 | // Bind events 287 | _on(el, 'mousedown', this._onTapStart); 288 | _on(el, 'touchstart', this._onTapStart); 289 | _on(el, 'pointerdown', this._onTapStart); 290 | 291 | if (this.nativeDraggable) { 292 | _on(el, 'dragover', this); 293 | _on(el, 'dragenter', this); 294 | } 295 | 296 | touchDragOverListeners.push(this._onDragOver); 297 | 298 | // Restore sorting 299 | options.store && this.sort(options.store.get(this)); 300 | } 301 | 302 | 303 | Sortable.prototype = /** @lends Sortable.prototype */ { 304 | constructor: Sortable, 305 | 306 | _onTapStart: function (/** Event|TouchEvent */evt) { 307 | var _this = this, 308 | el = this.el, 309 | options = this.options, 310 | preventOnFilter = options.preventOnFilter, 311 | type = evt.type, 312 | touch = evt.touches && evt.touches[0], 313 | target = (touch || evt).target, 314 | originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0]) || target, 315 | filter = options.filter, 316 | startIndex; 317 | 318 | _saveInputCheckedState(el); 319 | 320 | 321 | // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. 322 | if (dragEl) { 323 | return; 324 | } 325 | 326 | if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { 327 | return; // only left button or enabled 328 | } 329 | 330 | // cancel dnd if original target is content editable 331 | if (originalTarget.isContentEditable) { 332 | return; 333 | } 334 | 335 | target = _closest(target, options.draggable, el); 336 | 337 | if (!target) { 338 | return; 339 | } 340 | 341 | if (lastDownEl === target) { 342 | // Ignoring duplicate `down` 343 | return; 344 | } 345 | 346 | // Get the index of the dragged element within its parent 347 | startIndex = _index(target, options.draggable); 348 | 349 | // Check filter 350 | if (typeof filter === 'function') { 351 | if (filter.call(this, evt, target, this)) { 352 | _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); 353 | preventOnFilter && evt.preventDefault(); 354 | return; // cancel dnd 355 | } 356 | } 357 | else if (filter) { 358 | filter = filter.split(',').some(function (criteria) { 359 | criteria = _closest(originalTarget, criteria.trim(), el); 360 | 361 | if (criteria) { 362 | _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); 363 | return true; 364 | } 365 | }); 366 | 367 | if (filter) { 368 | preventOnFilter && evt.preventDefault(); 369 | return; // cancel dnd 370 | } 371 | } 372 | 373 | if (options.handle && !_closest(originalTarget, options.handle, el)) { 374 | return; 375 | } 376 | 377 | // Prepare `dragstart` 378 | this._prepareDragStart(evt, touch, target, startIndex); 379 | }, 380 | 381 | _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { 382 | var _this = this, 383 | el = _this.el, 384 | options = _this.options, 385 | ownerDocument = el.ownerDocument, 386 | dragStartFn; 387 | 388 | if (target && !dragEl && (target.parentNode === el)) { 389 | tapEvt = evt; 390 | 391 | rootEl = el; 392 | dragEl = target; 393 | parentEl = dragEl.parentNode; 394 | nextEl = dragEl.nextSibling; 395 | lastDownEl = target; 396 | activeGroup = options.group; 397 | oldIndex = startIndex; 398 | 399 | this._lastX = (touch || evt).clientX; 400 | this._lastY = (touch || evt).clientY; 401 | 402 | dragEl.style['will-change'] = 'all'; 403 | 404 | dragStartFn = function () { 405 | // Delayed drag has been triggered 406 | // we can re-enable the events: touchmove/mousemove 407 | _this._disableDelayedDrag(); 408 | 409 | // Make the element draggable 410 | dragEl.draggable = _this.nativeDraggable; 411 | 412 | // Chosen item 413 | _toggleClass(dragEl, options.chosenClass, true); 414 | 415 | // Bind the events: dragstart/dragend 416 | _this._triggerDragStart(evt, touch); 417 | 418 | // Drag start event 419 | _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); 420 | }; 421 | 422 | // Disable "draggable" 423 | options.ignore.split(',').forEach(function (criteria) { 424 | _find(dragEl, criteria.trim(), _disableDraggable); 425 | }); 426 | 427 | _on(ownerDocument, 'mouseup', _this._onDrop); 428 | _on(ownerDocument, 'touchend', _this._onDrop); 429 | _on(ownerDocument, 'touchcancel', _this._onDrop); 430 | _on(ownerDocument, 'pointercancel', _this._onDrop); 431 | _on(ownerDocument, 'selectstart', _this); 432 | 433 | if (options.delay) { 434 | // If the user moves the pointer or let go the click or touch 435 | // before the delay has been reached: 436 | // disable the delayed drag 437 | _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); 438 | _on(ownerDocument, 'touchend', _this._disableDelayedDrag); 439 | _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); 440 | _on(ownerDocument, 'mousemove', _this._disableDelayedDrag); 441 | _on(ownerDocument, 'touchmove', _this._disableDelayedDrag); 442 | _on(ownerDocument, 'pointermove', _this._disableDelayedDrag); 443 | 444 | _this._dragStartTimer = setTimeout(dragStartFn, options.delay); 445 | } else { 446 | dragStartFn(); 447 | } 448 | 449 | 450 | } 451 | }, 452 | 453 | _disableDelayedDrag: function () { 454 | var ownerDocument = this.el.ownerDocument; 455 | 456 | clearTimeout(this._dragStartTimer); 457 | _off(ownerDocument, 'mouseup', this._disableDelayedDrag); 458 | _off(ownerDocument, 'touchend', this._disableDelayedDrag); 459 | _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); 460 | _off(ownerDocument, 'mousemove', this._disableDelayedDrag); 461 | _off(ownerDocument, 'touchmove', this._disableDelayedDrag); 462 | _off(ownerDocument, 'pointermove', this._disableDelayedDrag); 463 | }, 464 | 465 | _triggerDragStart: function (/** Event */evt, /** Touch */touch) { 466 | touch = touch || (evt.pointerType == 'touch' ? evt : null); 467 | 468 | if (touch) { 469 | // Touch device support 470 | tapEvt = { 471 | target: dragEl, 472 | clientX: touch.clientX, 473 | clientY: touch.clientY 474 | }; 475 | 476 | this._onDragStart(tapEvt, 'touch'); 477 | } 478 | else if (!this.nativeDraggable) { 479 | this._onDragStart(tapEvt, true); 480 | } 481 | else { 482 | _on(dragEl, 'dragend', this); 483 | _on(rootEl, 'dragstart', this._onDragStart); 484 | } 485 | 486 | try { 487 | if (document.selection) { 488 | // Timeout neccessary for IE9 489 | _nextTick(function () { 490 | document.selection.empty(); 491 | }); 492 | } else { 493 | window.getSelection().removeAllRanges(); 494 | } 495 | } catch (err) { 496 | } 497 | }, 498 | 499 | _dragStarted: function () { 500 | if (rootEl && dragEl) { 501 | var options = this.options; 502 | 503 | // Apply effect 504 | _toggleClass(dragEl, options.ghostClass, true); 505 | _toggleClass(dragEl, options.dragClass, false); 506 | 507 | Sortable.active = this; 508 | 509 | // Drag start event 510 | _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); 511 | } else { 512 | this._nulling(); 513 | } 514 | }, 515 | 516 | _emulateDragOver: function () { 517 | if (touchEvt) { 518 | if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { 519 | return; 520 | } 521 | 522 | this._lastX = touchEvt.clientX; 523 | this._lastY = touchEvt.clientY; 524 | 525 | if (!supportCssPointerEvents) { 526 | _css(ghostEl, 'display', 'none'); 527 | } 528 | 529 | var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 530 | var parent = target; 531 | var i = touchDragOverListeners.length; 532 | 533 | if (target && target.shadowRoot) { 534 | target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 535 | parent = target; 536 | } 537 | 538 | if (parent) { 539 | do { 540 | if (parent[expando]) { 541 | while (i--) { 542 | touchDragOverListeners[i]({ 543 | clientX: touchEvt.clientX, 544 | clientY: touchEvt.clientY, 545 | target: target, 546 | rootEl: parent 547 | }); 548 | } 549 | 550 | break; 551 | } 552 | 553 | target = parent; // store last element 554 | } 555 | /* jshint boss:true */ 556 | while (parent = parent.parentNode); 557 | } 558 | 559 | if (!supportCssPointerEvents) { 560 | _css(ghostEl, 'display', ''); 561 | } 562 | } 563 | }, 564 | 565 | 566 | _onTouchMove: function (/**TouchEvent*/evt) { 567 | if (tapEvt) { 568 | var options = this.options, 569 | fallbackTolerance = options.fallbackTolerance, 570 | fallbackOffset = options.fallbackOffset, 571 | touch = evt.touches ? evt.touches[0] : evt, 572 | dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, 573 | dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, 574 | translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; 575 | 576 | // only set the status to dragging, when we are actually dragging 577 | if (!Sortable.active) { 578 | if (fallbackTolerance && 579 | min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance 580 | ) { 581 | return; 582 | } 583 | 584 | this._dragStarted(); 585 | } 586 | 587 | // as well as creating the ghost element on the document body 588 | this._appendGhost(); 589 | 590 | moved = true; 591 | touchEvt = touch; 592 | 593 | _css(ghostEl, 'webkitTransform', translate3d); 594 | _css(ghostEl, 'mozTransform', translate3d); 595 | _css(ghostEl, 'msTransform', translate3d); 596 | _css(ghostEl, 'transform', translate3d); 597 | 598 | evt.preventDefault(); 599 | } 600 | }, 601 | 602 | _appendGhost: function () { 603 | if (!ghostEl) { 604 | var rect = dragEl.getBoundingClientRect(), 605 | css = _css(dragEl), 606 | options = this.options, 607 | ghostRect; 608 | 609 | ghostEl = dragEl.cloneNode(true); 610 | 611 | _toggleClass(ghostEl, options.ghostClass, false); 612 | _toggleClass(ghostEl, options.fallbackClass, true); 613 | _toggleClass(ghostEl, options.dragClass, true); 614 | 615 | _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); 616 | _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); 617 | _css(ghostEl, 'width', rect.width); 618 | _css(ghostEl, 'height', rect.height); 619 | _css(ghostEl, 'opacity', '0.8'); 620 | _css(ghostEl, 'position', 'fixed'); 621 | _css(ghostEl, 'zIndex', '100000'); 622 | _css(ghostEl, 'pointerEvents', 'none'); 623 | 624 | options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); 625 | 626 | // Fixing dimensions. 627 | ghostRect = ghostEl.getBoundingClientRect(); 628 | _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); 629 | _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); 630 | } 631 | }, 632 | 633 | _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { 634 | var _this = this; 635 | var dataTransfer = evt.dataTransfer; 636 | var options = _this.options; 637 | 638 | _this._offUpEvents(); 639 | 640 | if (activeGroup.checkPull(_this, _this, dragEl, evt)) { 641 | cloneEl = _clone(dragEl); 642 | 643 | cloneEl.draggable = false; 644 | cloneEl.style['will-change'] = ''; 645 | 646 | _css(cloneEl, 'display', 'none'); 647 | _toggleClass(cloneEl, _this.options.chosenClass, false); 648 | 649 | // #1143: IFrame support workaround 650 | _this._cloneId = _nextTick(function () { 651 | rootEl.insertBefore(cloneEl, dragEl); 652 | _dispatchEvent(_this, rootEl, 'clone', dragEl); 653 | }); 654 | } 655 | 656 | _toggleClass(dragEl, options.dragClass, true); 657 | 658 | if (useFallback) { 659 | if (useFallback === 'touch') { 660 | // Bind touch events 661 | _on(document, 'touchmove', _this._onTouchMove); 662 | _on(document, 'touchend', _this._onDrop); 663 | _on(document, 'touchcancel', _this._onDrop); 664 | _on(document, 'pointermove', _this._onTouchMove); 665 | _on(document, 'pointerup', _this._onDrop); 666 | } else { 667 | // Old brwoser 668 | _on(document, 'mousemove', _this._onTouchMove); 669 | _on(document, 'mouseup', _this._onDrop); 670 | } 671 | 672 | _this._loopId = setInterval(_this._emulateDragOver, 50); 673 | } 674 | else { 675 | if (dataTransfer) { 676 | dataTransfer.effectAllowed = 'move'; 677 | options.setData && options.setData.call(_this, dataTransfer, dragEl); 678 | } 679 | 680 | _on(document, 'drop', _this); 681 | 682 | // #1143: Бывает элемент с IFrame внутри блокирует `drop`, 683 | // поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd. 684 | _on(document, 'mouseover', _this); 685 | 686 | _this._dragStartId = _nextTick(_this._dragStarted); 687 | } 688 | }, 689 | 690 | _onDragOver: function (/**Event*/evt) { 691 | var el = this.el, 692 | target, 693 | dragRect, 694 | targetRect, 695 | revert, 696 | options = this.options, 697 | group = options.group, 698 | activeSortable = Sortable.active, 699 | isOwner = (activeGroup === group), 700 | isMovingBetweenSortable = false, 701 | canSort = options.sort; 702 | 703 | if (evt.preventDefault !== void 0) { 704 | evt.preventDefault(); 705 | !options.dragoverBubble && evt.stopPropagation(); 706 | } 707 | 708 | if (dragEl.animated) { 709 | return; 710 | } 711 | 712 | moved = true; 713 | 714 | if (activeSortable && !options.disabled && 715 | (isOwner 716 | ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list 717 | : ( 718 | putSortable === this || 719 | ( 720 | (activeSortable.lastPullMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && 721 | group.checkPut(this, activeSortable, dragEl, evt) 722 | ) 723 | ) 724 | ) && 725 | (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback 726 | ) { 727 | // Smart auto-scrolling 728 | _autoScroll(evt, options, this.el); 729 | 730 | if (_silent) { 731 | return; 732 | } 733 | 734 | target = _closest(evt.target, options.draggable, el); 735 | dragRect = dragEl.getBoundingClientRect(); 736 | 737 | if (putSortable !== this) { 738 | putSortable = this; 739 | isMovingBetweenSortable = true; 740 | } 741 | 742 | if (revert) { 743 | _cloneHide(activeSortable, true); 744 | parentEl = rootEl; // actualization 745 | 746 | if (cloneEl || nextEl) { 747 | rootEl.insertBefore(dragEl, cloneEl || nextEl); 748 | } 749 | else if (!canSort) { 750 | rootEl.appendChild(dragEl); 751 | } 752 | 753 | return; 754 | } 755 | 756 | 757 | if ((el.children.length === 0) || (el.children[0] === ghostEl) || 758 | (el === evt.target) && (_ghostIsLast(el, evt)) 759 | ) { 760 | //assign target only if condition is true 761 | if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { 762 | target = el.lastElementChild; 763 | } 764 | 765 | if (target) { 766 | if (target.animated) { 767 | return; 768 | } 769 | 770 | targetRect = target.getBoundingClientRect(); 771 | } 772 | 773 | _cloneHide(activeSortable, isOwner); 774 | 775 | if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { 776 | if (!dragEl.contains(el)) { 777 | el.appendChild(dragEl); 778 | parentEl = el; // actualization 779 | } 780 | 781 | this._animate(dragRect, dragEl); 782 | target && this._animate(targetRect, target); 783 | } 784 | } 785 | else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { 786 | if (lastEl !== target) { 787 | lastEl = target; 788 | lastCSS = _css(target); 789 | lastParentCSS = _css(target.parentNode); 790 | } 791 | 792 | targetRect = target.getBoundingClientRect(); 793 | 794 | var width = targetRect.right - targetRect.left, 795 | height = targetRect.bottom - targetRect.top, 796 | floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display) 797 | || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), 798 | isWide = (target.offsetWidth > dragEl.offsetWidth), 799 | isLong = (target.offsetHeight > dragEl.offsetHeight), 800 | halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, 801 | nextSibling = target.nextElementSibling, 802 | after = false 803 | ; 804 | 805 | if (floating) { 806 | var elTop = dragEl.offsetTop, 807 | tgTop = target.offsetTop; 808 | 809 | if (elTop === tgTop) { 810 | after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; 811 | } 812 | else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) { 813 | after = (evt.clientY - targetRect.top) / height > 0.5; 814 | } else { 815 | after = tgTop > elTop; 816 | } 817 | } else if (!isMovingBetweenSortable) { 818 | after = (nextSibling !== dragEl) && !isLong || halfway && isLong; 819 | } 820 | 821 | var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); 822 | 823 | if (moveVector !== false) { 824 | if (moveVector === 1 || moveVector === -1) { 825 | after = (moveVector === 1); 826 | } 827 | 828 | _silent = true; 829 | setTimeout(_unsilent, 30); 830 | 831 | _cloneHide(activeSortable, isOwner); 832 | 833 | if (!dragEl.contains(el)) { 834 | if (after && !nextSibling) { 835 | el.appendChild(dragEl); 836 | } else { 837 | target.parentNode.insertBefore(dragEl, after ? nextSibling : target); 838 | } 839 | } 840 | 841 | parentEl = dragEl.parentNode; // actualization 842 | 843 | this._animate(dragRect, dragEl); 844 | this._animate(targetRect, target); 845 | } 846 | } 847 | } 848 | }, 849 | 850 | _animate: function (prevRect, target) { 851 | var ms = this.options.animation; 852 | 853 | if (ms) { 854 | var currentRect = target.getBoundingClientRect(); 855 | 856 | if (prevRect.nodeType === 1) { 857 | prevRect = prevRect.getBoundingClientRect(); 858 | } 859 | 860 | _css(target, 'transition', 'none'); 861 | _css(target, 'transform', 'translate3d(' 862 | + (prevRect.left - currentRect.left) + 'px,' 863 | + (prevRect.top - currentRect.top) + 'px,0)' 864 | ); 865 | 866 | target.offsetWidth; // repaint 867 | 868 | _css(target, 'transition', 'all ' + ms + 'ms'); 869 | _css(target, 'transform', 'translate3d(0,0,0)'); 870 | 871 | clearTimeout(target.animated); 872 | target.animated = setTimeout(function () { 873 | _css(target, 'transition', ''); 874 | _css(target, 'transform', ''); 875 | target.animated = false; 876 | }, ms); 877 | } 878 | }, 879 | 880 | _offUpEvents: function () { 881 | var ownerDocument = this.el.ownerDocument; 882 | 883 | _off(document, 'touchmove', this._onTouchMove); 884 | _off(document, 'pointermove', this._onTouchMove); 885 | _off(ownerDocument, 'mouseup', this._onDrop); 886 | _off(ownerDocument, 'touchend', this._onDrop); 887 | _off(ownerDocument, 'pointerup', this._onDrop); 888 | _off(ownerDocument, 'touchcancel', this._onDrop); 889 | _off(ownerDocument, 'pointercancel', this._onDrop); 890 | _off(ownerDocument, 'selectstart', this); 891 | }, 892 | 893 | _onDrop: function (/**Event*/evt) { 894 | var el = this.el, 895 | options = this.options; 896 | 897 | clearInterval(this._loopId); 898 | clearInterval(autoScroll.pid); 899 | clearTimeout(this._dragStartTimer); 900 | 901 | _cancelNextTick(this._cloneId); 902 | _cancelNextTick(this._dragStartId); 903 | 904 | // Unbind events 905 | _off(document, 'mouseover', this); 906 | _off(document, 'mousemove', this._onTouchMove); 907 | 908 | if (this.nativeDraggable) { 909 | _off(document, 'drop', this); 910 | _off(el, 'dragstart', this._onDragStart); 911 | } 912 | 913 | this._offUpEvents(); 914 | 915 | if (evt) { 916 | if (moved) { 917 | evt.preventDefault(); 918 | !options.dropBubble && evt.stopPropagation(); 919 | } 920 | 921 | ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); 922 | 923 | if (rootEl === parentEl || Sortable.active.lastPullMode !== 'clone') { 924 | // Remove clone 925 | cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); 926 | } 927 | 928 | if (dragEl) { 929 | if (this.nativeDraggable) { 930 | _off(dragEl, 'dragend', this); 931 | } 932 | 933 | _disableDraggable(dragEl); 934 | dragEl.style['will-change'] = ''; 935 | 936 | // Remove class's 937 | _toggleClass(dragEl, this.options.ghostClass, false); 938 | _toggleClass(dragEl, this.options.chosenClass, false); 939 | 940 | // Drag stop event 941 | _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex); 942 | 943 | if (rootEl !== parentEl) { 944 | newIndex = _index(dragEl, options.draggable); 945 | 946 | if (newIndex >= 0) { 947 | // Add event 948 | _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex); 949 | 950 | // Remove event 951 | _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex); 952 | 953 | // drag from one list and drop into another 954 | _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 955 | _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 956 | } 957 | } 958 | else { 959 | if (dragEl.nextSibling !== nextEl) { 960 | // Get the index of the dragged element within its parent 961 | newIndex = _index(dragEl, options.draggable); 962 | 963 | if (newIndex >= 0) { 964 | // drag & drop within the same list 965 | _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex); 966 | _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 967 | } 968 | } 969 | } 970 | 971 | if (Sortable.active) { 972 | /* jshint eqnull:true */ 973 | if (newIndex == null || newIndex === -1) { 974 | newIndex = oldIndex; 975 | } 976 | 977 | _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex); 978 | 979 | // Save sorting 980 | this.save(); 981 | } 982 | } 983 | 984 | } 985 | 986 | this._nulling(); 987 | }, 988 | 989 | _nulling: function() { 990 | rootEl = 991 | dragEl = 992 | parentEl = 993 | ghostEl = 994 | nextEl = 995 | cloneEl = 996 | lastDownEl = 997 | 998 | scrollEl = 999 | scrollParentEl = 1000 | 1001 | tapEvt = 1002 | touchEvt = 1003 | 1004 | moved = 1005 | newIndex = 1006 | 1007 | lastEl = 1008 | lastCSS = 1009 | 1010 | putSortable = 1011 | activeGroup = 1012 | Sortable.active = null; 1013 | 1014 | savedInputChecked.forEach(function (el) { 1015 | el.checked = true; 1016 | }); 1017 | savedInputChecked.length = 0; 1018 | }, 1019 | 1020 | handleEvent: function (/**Event*/evt) { 1021 | switch (evt.type) { 1022 | case 'drop': 1023 | case 'dragend': 1024 | this._onDrop(evt); 1025 | break; 1026 | 1027 | case 'dragover': 1028 | case 'dragenter': 1029 | if (dragEl) { 1030 | this._onDragOver(evt); 1031 | _globalDragOver(evt); 1032 | } 1033 | break; 1034 | 1035 | case 'mouseover': 1036 | this._onDrop(evt); 1037 | break; 1038 | 1039 | case 'selectstart': 1040 | evt.preventDefault(); 1041 | break; 1042 | } 1043 | }, 1044 | 1045 | 1046 | /** 1047 | * Serializes the item into an array of string. 1048 | * @returns {String[]} 1049 | */ 1050 | toArray: function () { 1051 | var order = [], 1052 | el, 1053 | children = this.el.children, 1054 | i = 0, 1055 | n = children.length, 1056 | options = this.options; 1057 | 1058 | for (; i < n; i++) { 1059 | el = children[i]; 1060 | if (_closest(el, options.draggable, this.el)) { 1061 | order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); 1062 | } 1063 | } 1064 | 1065 | return order; 1066 | }, 1067 | 1068 | 1069 | /** 1070 | * Sorts the elements according to the array. 1071 | * @param {String[]} order order of the items 1072 | */ 1073 | sort: function (order) { 1074 | var items = {}, rootEl = this.el; 1075 | 1076 | this.toArray().forEach(function (id, i) { 1077 | var el = rootEl.children[i]; 1078 | 1079 | if (_closest(el, this.options.draggable, rootEl)) { 1080 | items[id] = el; 1081 | } 1082 | }, this); 1083 | 1084 | order.forEach(function (id) { 1085 | if (items[id]) { 1086 | rootEl.removeChild(items[id]); 1087 | rootEl.appendChild(items[id]); 1088 | } 1089 | }); 1090 | }, 1091 | 1092 | 1093 | /** 1094 | * Save the current sorting 1095 | */ 1096 | save: function () { 1097 | var store = this.options.store; 1098 | store && store.set(this); 1099 | }, 1100 | 1101 | 1102 | /** 1103 | * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. 1104 | * @param {HTMLElement} el 1105 | * @param {String} [selector] default: `options.draggable` 1106 | * @returns {HTMLElement|null} 1107 | */ 1108 | closest: function (el, selector) { 1109 | return _closest(el, selector || this.options.draggable, this.el); 1110 | }, 1111 | 1112 | 1113 | /** 1114 | * Set/get option 1115 | * @param {string} name 1116 | * @param {*} [value] 1117 | * @returns {*} 1118 | */ 1119 | option: function (name, value) { 1120 | var options = this.options; 1121 | 1122 | if (value === void 0) { 1123 | return options[name]; 1124 | } else { 1125 | options[name] = value; 1126 | 1127 | if (name === 'group') { 1128 | _prepareGroup(options); 1129 | } 1130 | } 1131 | }, 1132 | 1133 | 1134 | /** 1135 | * Destroy 1136 | */ 1137 | destroy: function () { 1138 | var el = this.el; 1139 | 1140 | el[expando] = null; 1141 | 1142 | _off(el, 'mousedown', this._onTapStart); 1143 | _off(el, 'touchstart', this._onTapStart); 1144 | _off(el, 'pointerdown', this._onTapStart); 1145 | 1146 | if (this.nativeDraggable) { 1147 | _off(el, 'dragover', this); 1148 | _off(el, 'dragenter', this); 1149 | } 1150 | 1151 | // Remove draggable attributes 1152 | Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { 1153 | el.removeAttribute('draggable'); 1154 | }); 1155 | 1156 | touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); 1157 | 1158 | this._onDrop(); 1159 | 1160 | this.el = el = null; 1161 | } 1162 | }; 1163 | 1164 | 1165 | function _cloneHide(sortable, state) { 1166 | if (sortable.lastPullMode !== 'clone') { 1167 | state = true; 1168 | } 1169 | 1170 | if (cloneEl && (cloneEl.state !== state)) { 1171 | _css(cloneEl, 'display', state ? 'none' : ''); 1172 | 1173 | if (!state) { 1174 | if (cloneEl.state) { 1175 | if (sortable.options.group.revertClone) { 1176 | rootEl.insertBefore(cloneEl, nextEl); 1177 | sortable._animate(dragEl, cloneEl); 1178 | } else { 1179 | rootEl.insertBefore(cloneEl, dragEl); 1180 | } 1181 | } 1182 | } 1183 | 1184 | cloneEl.state = state; 1185 | } 1186 | } 1187 | 1188 | 1189 | function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { 1190 | if (el) { 1191 | ctx = ctx || document; 1192 | 1193 | do { 1194 | if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { 1195 | return el; 1196 | } 1197 | /* jshint boss:true */ 1198 | } while (el = _getParentOrHost(el)); 1199 | } 1200 | 1201 | return null; 1202 | } 1203 | 1204 | 1205 | function _getParentOrHost(el) { 1206 | var parent = el.host; 1207 | 1208 | return (parent && parent.nodeType) ? parent : el.parentNode; 1209 | } 1210 | 1211 | 1212 | function _globalDragOver(/**Event*/evt) { 1213 | if (evt.dataTransfer) { 1214 | evt.dataTransfer.dropEffect = 'move'; 1215 | } 1216 | evt.preventDefault(); 1217 | } 1218 | 1219 | 1220 | function _on(el, event, fn) { 1221 | el.addEventListener(event, fn, captureMode); 1222 | } 1223 | 1224 | 1225 | function _off(el, event, fn) { 1226 | el.removeEventListener(event, fn, captureMode); 1227 | } 1228 | 1229 | 1230 | function _toggleClass(el, name, state) { 1231 | if (el) { 1232 | if (el.classList) { 1233 | el.classList[state ? 'add' : 'remove'](name); 1234 | } 1235 | else { 1236 | var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); 1237 | el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); 1238 | } 1239 | } 1240 | } 1241 | 1242 | 1243 | function _css(el, prop, val) { 1244 | var style = el && el.style; 1245 | 1246 | if (style) { 1247 | if (val === void 0) { 1248 | if (document.defaultView && document.defaultView.getComputedStyle) { 1249 | val = document.defaultView.getComputedStyle(el, ''); 1250 | } 1251 | else if (el.currentStyle) { 1252 | val = el.currentStyle; 1253 | } 1254 | 1255 | return prop === void 0 ? val : val[prop]; 1256 | } 1257 | else { 1258 | if (!(prop in style)) { 1259 | prop = '-webkit-' + prop; 1260 | } 1261 | 1262 | style[prop] = val + (typeof val === 'string' ? '' : 'px'); 1263 | } 1264 | } 1265 | } 1266 | 1267 | 1268 | function _find(ctx, tagName, iterator) { 1269 | if (ctx) { 1270 | var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; 1271 | 1272 | if (iterator) { 1273 | for (; i < n; i++) { 1274 | iterator(list[i], i); 1275 | } 1276 | } 1277 | 1278 | return list; 1279 | } 1280 | 1281 | return []; 1282 | } 1283 | 1284 | 1285 | 1286 | function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex) { 1287 | sortable = (sortable || rootEl[expando]); 1288 | 1289 | var evt = document.createEvent('Event'), 1290 | options = sortable.options, 1291 | onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); 1292 | 1293 | evt.initEvent(name, true, true); 1294 | 1295 | evt.to = toEl || rootEl; 1296 | evt.from = fromEl || rootEl; 1297 | evt.item = targetEl || rootEl; 1298 | evt.clone = cloneEl; 1299 | 1300 | evt.oldIndex = startIndex; 1301 | evt.newIndex = newIndex; 1302 | 1303 | rootEl.dispatchEvent(evt); 1304 | 1305 | if (options[onName]) { 1306 | options[onName].call(sortable, evt); 1307 | } 1308 | } 1309 | 1310 | 1311 | function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { 1312 | var evt, 1313 | sortable = fromEl[expando], 1314 | onMoveFn = sortable.options.onMove, 1315 | retVal; 1316 | 1317 | evt = document.createEvent('Event'); 1318 | evt.initEvent('move', true, true); 1319 | 1320 | evt.to = toEl; 1321 | evt.from = fromEl; 1322 | evt.dragged = dragEl; 1323 | evt.draggedRect = dragRect; 1324 | evt.related = targetEl || toEl; 1325 | evt.relatedRect = targetRect || toEl.getBoundingClientRect(); 1326 | evt.willInsertAfter = willInsertAfter; 1327 | 1328 | fromEl.dispatchEvent(evt); 1329 | 1330 | if (onMoveFn) { 1331 | retVal = onMoveFn.call(sortable, evt, originalEvt); 1332 | } 1333 | 1334 | return retVal; 1335 | } 1336 | 1337 | 1338 | function _disableDraggable(el) { 1339 | el.draggable = false; 1340 | } 1341 | 1342 | 1343 | function _unsilent() { 1344 | _silent = false; 1345 | } 1346 | 1347 | 1348 | /** @returns {HTMLElement|false} */ 1349 | function _ghostIsLast(el, evt) { 1350 | var lastEl = el.lastElementChild, 1351 | rect = lastEl.getBoundingClientRect(); 1352 | 1353 | // 5 — min delta 1354 | // abs — нельзя добавлять, а то глюки при наведении сверху 1355 | return (evt.clientY - (rect.top + rect.height) > 5) || 1356 | (evt.clientX - (rect.left + rect.width) > 5); 1357 | } 1358 | 1359 | 1360 | /** 1361 | * Generate id 1362 | * @param {HTMLElement} el 1363 | * @returns {String} 1364 | * @private 1365 | */ 1366 | function _generateId(el) { 1367 | var str = el.tagName + el.className + el.src + el.href + el.textContent, 1368 | i = str.length, 1369 | sum = 0; 1370 | 1371 | while (i--) { 1372 | sum += str.charCodeAt(i); 1373 | } 1374 | 1375 | return sum.toString(36); 1376 | } 1377 | 1378 | /** 1379 | * Returns the index of an element within its parent for a selected set of 1380 | * elements 1381 | * @param {HTMLElement} el 1382 | * @param {selector} selector 1383 | * @return {number} 1384 | */ 1385 | function _index(el, selector) { 1386 | var index = 0; 1387 | 1388 | if (!el || !el.parentNode) { 1389 | return -1; 1390 | } 1391 | 1392 | while (el && (el = el.previousElementSibling)) { 1393 | if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { 1394 | index++; 1395 | } 1396 | } 1397 | 1398 | return index; 1399 | } 1400 | 1401 | function _matches(/**HTMLElement*/el, /**String*/selector) { 1402 | if (el) { 1403 | selector = selector.split('.'); 1404 | 1405 | var tag = selector.shift().toUpperCase(), 1406 | re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g'); 1407 | 1408 | return ( 1409 | (tag === '' || el.nodeName.toUpperCase() == tag) && 1410 | (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length) 1411 | ); 1412 | } 1413 | 1414 | return false; 1415 | } 1416 | 1417 | function _throttle(callback, ms) { 1418 | var args, _this; 1419 | 1420 | return function () { 1421 | if (args === void 0) { 1422 | args = arguments; 1423 | _this = this; 1424 | 1425 | setTimeout(function () { 1426 | if (args.length === 1) { 1427 | callback.call(_this, args[0]); 1428 | } else { 1429 | callback.apply(_this, args); 1430 | } 1431 | 1432 | args = void 0; 1433 | }, ms); 1434 | } 1435 | }; 1436 | } 1437 | 1438 | function _extend(dst, src) { 1439 | if (dst && src) { 1440 | for (var key in src) { 1441 | if (src.hasOwnProperty(key)) { 1442 | dst[key] = src[key]; 1443 | } 1444 | } 1445 | } 1446 | 1447 | return dst; 1448 | } 1449 | 1450 | function _clone(el) { 1451 | if (Polymer && Polymer.dom) { 1452 | return Polymer.dom(el).cloneNode(true); 1453 | } 1454 | else if ($) { 1455 | return $(el).clone(true)[0]; 1456 | } 1457 | else { 1458 | return el.cloneNode(true); 1459 | } 1460 | } 1461 | 1462 | function _saveInputCheckedState(root) { 1463 | var inputs = root.getElementsByTagName('input'); 1464 | var idx = inputs.length; 1465 | 1466 | while (idx--) { 1467 | var el = inputs[idx]; 1468 | el.checked && savedInputChecked.push(el); 1469 | } 1470 | } 1471 | 1472 | function _nextTick(fn) { 1473 | return setTimeout(fn, 0); 1474 | } 1475 | 1476 | function _cancelNextTick(id) { 1477 | return clearTimeout(id); 1478 | } 1479 | 1480 | // Fixed #973: 1481 | _on(document, 'touchmove', function (evt) { 1482 | if (Sortable.active) { 1483 | evt.preventDefault(); 1484 | } 1485 | }); 1486 | 1487 | try { 1488 | window.addEventListener('test', null, Object.defineProperty({}, 'passive', { 1489 | get: function () { 1490 | captureMode = { 1491 | capture: false, 1492 | passive: false 1493 | }; 1494 | } 1495 | })); 1496 | } catch (err) {} 1497 | 1498 | // Export utils 1499 | Sortable.utils = { 1500 | on: _on, 1501 | off: _off, 1502 | css: _css, 1503 | find: _find, 1504 | is: function (el, selector) { 1505 | return !!_closest(el, selector, el); 1506 | }, 1507 | extend: _extend, 1508 | throttle: _throttle, 1509 | closest: _closest, 1510 | toggleClass: _toggleClass, 1511 | clone: _clone, 1512 | index: _index, 1513 | nextTick: _nextTick, 1514 | cancelNextTick: _cancelNextTick 1515 | }; 1516 | 1517 | 1518 | /** 1519 | * Create sortable instance 1520 | * @param {HTMLElement} el 1521 | * @param {Object} [options] 1522 | */ 1523 | Sortable.create = function (el, options) { 1524 | return new Sortable(el, options); 1525 | }; 1526 | 1527 | 1528 | // Export 1529 | Sortable.version = '1.6.1'; 1530 | return Sortable; 1531 | }); 1532 | -------------------------------------------------------------------------------- /courses/js/vendor/jquery.fn.sortable.min.js: -------------------------------------------------------------------------------- 1 | /*! Sortable 1.6.1 - MIT | git://github.com/rubaxa/Sortable.git */ 2 | 3 | !function(t){"use strict";"function"==typeof define&&define.amd?define(t):"undefined"!=typeof module&&void 0!==module.exports?module.exports=t():window.Sortable=t()}(function(){"use strict";function t(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be HTMLElement, and not "+{}.toString.call(t);this.el=t,this.options=e=b({},e),t[z]=this;var n={group:Math.random(),sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,draggable:/[uo]l/i.test(t.nodeName)?"li":">*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0}};for(var o in n)!(o in e)&&(e[o]=n[o]);ct(e);for(var i in this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&et,r(t,"mousedown",this._onTapStart),r(t,"touchstart",this._onTapStart),r(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(r(t,"dragover",this),r(t,"dragenter",this)),lt.push(this._onDragOver),e.store&&this.sort(e.store.get(this))}function e(t,e){"clone"!==t.lastPullMode&&(e=!0),x&&x.state!==e&&(s(x,"display",e?"none":""),e||x.state&&(t.options.group.revertClone?(N.insertBefore(x,k),t._animate(C,x)):N.insertBefore(x,C)),x.state=e)}function n(t,e,n){if(t){n=n||Q;do{if(">*"===e&&t.parentNode===n||m(t,e))return t}while(t=o(t))}return null}function o(t){var e=t.host;return e&&e.nodeType?e:t.parentNode}function i(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move"),t.preventDefault()}function r(t,e,n){t.addEventListener(e,n,tt)}function a(t,e,n){t.removeEventListener(e,n,tt)}function l(t,e,n){if(t)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(V," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(V," ")}}function s(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return Q.defaultView&&Q.defaultView.getComputedStyle?n=Q.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function c(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i5||e.clientX-(n.left+n.width)>5}function g(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function v(t,e){var n=0;if(!t||!t.parentNode)return-1;for(;t&&(t=t.previousElementSibling);)"TEMPLATE"===t.nodeName.toUpperCase()||">*"!==e&&!m(t,e)||n++;return n}function m(t,e){if(t){var n=(e=e.split(".")).shift().toUpperCase(),o=new RegExp("\\s("+e.join("|")+")(?=\\s)","g");return!(""!==n&&t.nodeName.toUpperCase()!=n||e.length&&((" "+t.className+" ").match(o)||[]).length!=e.length)}return!1}function _(t,e){var n,o;return function(){void 0===n&&(n=arguments,o=this,J(function(){1===n.length?t.call(o,n[0]):t.apply(o,n),n=void 0},e))}}function b(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function D(t){return $&&$.dom?$.dom(t).cloneNode(!0):K?K(t).clone(!0)[0]:t.cloneNode(!0)}function y(t){for(var e=t.getElementsByTagName("input"),n=e.length;n--;){var o=e[n];o.checked&&at.push(o)}}function w(t){return J(t,0)}function T(t){return clearTimeout(t)}if("undefined"==typeof window||!window.document)return function(){throw new Error("Sortable.js requires a window with a document")};var C,S,E,x,N,k,B,Y,X,O,I,R,A,M,P,L,F,U,j,H,W={},V=/\s+/g,q=/left|right|inline/,z="Sortable"+(new Date).getTime(),G=window,Q=G.document,Z=G.parseInt,J=G.setTimeout,K=G.jQuery||G.Zepto,$=G.Polymer,tt=!1,et="draggable"in Q.createElement("div"),nt=function(t){return!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)&&(t=Q.createElement("x"),t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents)}(),ot=!1,it=Math.abs,rt=Math.min,at=[],lt=[],st=_(function(t,e,n){if(n&&e.scroll){var o,i,r,a,l,s,c=n[z],d=e.scrollSensitivity,h=e.scrollSpeed,u=t.clientX,f=t.clientY,p=window.innerWidth,g=window.innerHeight;if(X!==n&&(Y=e.scroll,X=n,O=e.scrollFn,!0===Y)){Y=n;do{if(Y.offsetWidth-1:i==t)}}var n={},o=t.group;o&&"object"==typeof o||(o={name:o}),n.name=o.name,n.checkPull=e(o.pull,!0),n.checkPut=e(o.put),n.revertClone=o.revertClone,t.group=n};t.prototype={constructor:t,_onTapStart:function(t){var e,o=this,i=this.el,r=this.options,a=r.preventOnFilter,l=t.type,s=t.touches&&t.touches[0],c=(s||t).target,h=t.target.shadowRoot&&t.path&&t.path[0]||c,u=r.filter;if(y(i),!C&&!(/mousedown|pointerdown/.test(l)&&0!==t.button||r.disabled)&&!h.isContentEditable&&(c=n(c,r.draggable,i))&&B!==c){if(e=v(c,r.draggable),"function"==typeof u){if(u.call(this,t,c,this))return d(o,h,"filter",c,i,i,e),void(a&&t.preventDefault())}else if(u&&(u=u.split(",").some(function(t){if(t=n(h,t.trim(),i))return d(o,t,"filter",c,i,i,e),!0})))return void(a&&t.preventDefault());r.handle&&!n(h,r.handle,i)||this._prepareDragStart(t,s,c,e)}},_prepareDragStart:function(t,e,n,o){var i,a=this,s=a.el,h=a.options,f=s.ownerDocument;n&&!C&&n.parentNode===s&&(U=t,N=s,S=(C=n).parentNode,k=C.nextSibling,B=n,L=h.group,M=o,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,C.style["will-change"]="all",i=function(){a._disableDelayedDrag(),C.draggable=a.nativeDraggable,l(C,h.chosenClass,!0),a._triggerDragStart(t,e),d(a,N,"choose",C,N,N,M)},h.ignore.split(",").forEach(function(t){c(C,t.trim(),u)}),r(f,"mouseup",a._onDrop),r(f,"touchend",a._onDrop),r(f,"touchcancel",a._onDrop),r(f,"pointercancel",a._onDrop),r(f,"selectstart",a),h.delay?(r(f,"mouseup",a._disableDelayedDrag),r(f,"touchend",a._disableDelayedDrag),r(f,"touchcancel",a._disableDelayedDrag),r(f,"mousemove",a._disableDelayedDrag),r(f,"touchmove",a._disableDelayedDrag),r(f,"pointermove",a._disableDelayedDrag),a._dragStartTimer=J(i,h.delay)):i())},_disableDelayedDrag:function(){var t=this.el.ownerDocument;clearTimeout(this._dragStartTimer),a(t,"mouseup",this._disableDelayedDrag),a(t,"touchend",this._disableDelayedDrag),a(t,"touchcancel",this._disableDelayedDrag),a(t,"mousemove",this._disableDelayedDrag),a(t,"touchmove",this._disableDelayedDrag),a(t,"pointermove",this._disableDelayedDrag)},_triggerDragStart:function(t,e){(e=e||("touch"==t.pointerType?t:null))?(U={target:C,clientX:e.clientX,clientY:e.clientY},this._onDragStart(U,"touch")):this.nativeDraggable?(r(C,"dragend",this),r(N,"dragstart",this._onDragStart)):this._onDragStart(U,!0);try{Q.selection?w(function(){Q.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(){if(N&&C){var e=this.options;l(C,e.ghostClass,!0),l(C,e.dragClass,!1),t.active=this,d(this,N,"start",C,N,N,M)}else this._nulling()},_emulateDragOver:function(){if(j){if(this._lastX===j.clientX&&this._lastY===j.clientY)return;this._lastX=j.clientX,this._lastY=j.clientY,nt||s(E,"display","none");var t=Q.elementFromPoint(j.clientX,j.clientY),e=t,n=lt.length;if(t&&t.shadowRoot&&(e=t=t.shadowRoot.elementFromPoint(j.clientX,j.clientY)),e)do{if(e[z]){for(;n--;)lt[n]({clientX:j.clientX,clientY:j.clientY,target:t,rootEl:e});break}t=e}while(e=e.parentNode);nt||s(E,"display","")}},_onTouchMove:function(e){if(U){var n=this.options,o=n.fallbackTolerance,i=n.fallbackOffset,r=e.touches?e.touches[0]:e,a=r.clientX-U.clientX+i.x,l=r.clientY-U.clientY+i.y,c=e.touches?"translate3d("+a+"px,"+l+"px,0)":"translate("+a+"px,"+l+"px)";if(!t.active){if(o&&rt(it(r.clientX-this._lastX),it(r.clientY-this._lastY))C.offsetWidth,T=i.offsetHeight>C.offsetHeight,B=(y?(o.clientX-a.left)/b:(o.clientY-a.top)/D)>.5,Y=i.nextElementSibling,X=!1;if(y){var O=C.offsetTop,M=i.offsetTop;X=O===M?i.previousElementSibling===C&&!w||B&&w:i.previousElementSibling===C||C.previousElementSibling===i?(o.clientY-a.top)/D>.5:M>O}else m||(X=Y!==C&&!T||B&&T);var P=h(N,c,C,r,i,a,o,X);!1!==P&&(1!==P&&-1!==P||(X=1===P),ot=!0,J(f,30),e(g,v),C.contains(c)||(X&&!Y?c.appendChild(C):i.parentNode.insertBefore(C,X?Y:i)),S=C.parentNode,this._animate(r,C),this._animate(a,i))}}},_animate:function(t,e){var n=this.options.animation;if(n){var o=e.getBoundingClientRect();1===t.nodeType&&(t=t.getBoundingClientRect()),s(e,"transition","none"),s(e,"transform","translate3d("+(t.left-o.left)+"px,"+(t.top-o.top)+"px,0)"),e.offsetWidth,s(e,"transition","all "+n+"ms"),s(e,"transform","translate3d(0,0,0)"),clearTimeout(e.animated),e.animated=J(function(){s(e,"transition",""),s(e,"transform",""),e.animated=!1},n)}},_offUpEvents:function(){var t=this.el.ownerDocument;a(Q,"touchmove",this._onTouchMove),a(Q,"pointermove",this._onTouchMove),a(t,"mouseup",this._onDrop),a(t,"touchend",this._onDrop),a(t,"pointerup",this._onDrop),a(t,"touchcancel",this._onDrop),a(t,"pointercancel",this._onDrop),a(t,"selectstart",this)},_onDrop:function(e){var n=this.el,o=this.options;clearInterval(this._loopId),clearInterval(W.pid),clearTimeout(this._dragStartTimer),T(this._cloneId),T(this._dragStartId),a(Q,"mouseover",this),a(Q,"mousemove",this._onTouchMove),this.nativeDraggable&&(a(Q,"drop",this),a(n,"dragstart",this._onDragStart)),this._offUpEvents(),e&&(H&&(e.preventDefault(),!o.dropBubble&&e.stopPropagation()),E&&E.parentNode&&E.parentNode.removeChild(E),N!==S&&"clone"===t.active.lastPullMode||x&&x.parentNode&&x.parentNode.removeChild(x),C&&(this.nativeDraggable&&a(C,"dragend",this),u(C),C.style["will-change"]="",l(C,this.options.ghostClass,!1),l(C,this.options.chosenClass,!1),d(this,N,"unchoose",C,S,N,M),N!==S?(P=v(C,o.draggable))>=0&&(d(null,S,"add",C,S,N,M,P),d(this,N,"remove",C,S,N,M,P),d(null,S,"sort",C,S,N,M,P),d(this,N,"sort",C,S,N,M,P)):C.nextSibling!==k&&(P=v(C,o.draggable))>=0&&(d(this,N,"update",C,S,N,M,P),d(this,N,"sort",C,S,N,M,P)),t.active&&(null!=P&&-1!==P||(P=M),d(this,N,"end",C,S,N,M,P),this.save()))),this._nulling()},_nulling:function(){N=C=S=E=k=x=B=Y=X=U=j=H=P=I=R=F=L=t.active=null,at.forEach(function(t){t.checked=!0}),at.length=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragover":case"dragenter":C&&(this._onDragOver(t),i(t));break;case"mouseover":this._onDrop(t);break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],o=this.el.children,i=0,r=o.length,a=this.options;i{{ question.quiz.course.title }} 9 |
  • {{ question.quiz.title }}
  • 10 |
  • {{ question.prompt }}
  • 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
    15 | {{ block.super }} 16 |
    17 |
    18 |

    Answers

    19 |
    20 | {% csrf_token %} 21 |
    22 | {% bootstrap_formset formset %} 23 |
    24 | 25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /courses/templates/courses/course_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load humanize course_extras %} 3 | 4 | {% block title %}{{ course.title }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    {{ course.title }}

    9 | 10 |
    11 | {{ course.description|markdown_to_html }} 12 |
    13 | 14 |

    There are {{ course.step_set.count|apnumber }} step{{ course.step_set.count|pluralize }} in this course: {{ course.step_set.all|join:", " }}

    15 | 16 |
    17 | 18 | {% for step in steps %} 19 |
    20 |

    {{ step.title }}

    21 |
    22 |

    {{ step.description }}

    23 |
    24 | {% endfor %} 25 | 26 |
    27 | 28 |
    29 | 30 | {% if user.is_authenticated %} 31 |
    32 | New Quiz 33 | {% endif %} 34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /courses/templates/courses/course_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load static %} 3 | {% block static %}{% endblock %} 4 | {% block title %}Available Courses{% endblock %} 5 | 6 | {% block content %} 7 | 8 |
    9 | {% for course in courses %} 10 |
    11 |
    12 |
    13 | {{ course.title }} 14 |
    15 |
    16 | {% if course.description|wordcount <= 5 %} 17 |

    {{ course.description }}

    18 | {% else %} 19 |

    {{ course.description|truncatewords:5 }}

    20 | Read more 21 | {% endif %} 22 |
    Created on: {{ course.created_at|date:"F j, Y" }}
    23 | 24 |
    25 |
    26 |
    27 | {% endfor %} 28 |
    29 |
    Have questions? Contact us! {{ email|urlize }}
    30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /courses/templates/courses/course_nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | {% for course in courses %} 5 | 6 | {% endfor %} 7 |
    -------------------------------------------------------------------------------- /courses/templates/courses/question_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load course_extras %} 3 | {% load bootstrap4 %} 4 | 5 | {% block title %}{{ form.instance.prompt|default:"New Question" }} | {{ quiz.course.title }} {{ block.super }}{% endblock %} 6 | 7 | {% block breadcrumbs %} 8 |
  • {{ quiz.course.title }}
  • 9 |
  • {{ quiz.title }}
  • 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
    14 | {{ block.super }} 15 |

    {{ form.instance.prompt|default:"Make a new question"}}

    16 |
    17 | {% csrf_token %} 18 | {% bootstrap_form form %} 19 | 20 | {{ formset.management_form }} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for form in formset %} 33 | 34 | 35 | 36 | 37 | {% if form.instance.pk %} 38 | 39 | {% else %} 40 | 41 | {% endif %} 42 | 43 | {% endfor %} 44 | 45 |
    OrderTextCorrect?Delete
    {{ form.id }}{{ form.order }}{{ form.text }}{{ form.correct }}{{ form.DELETE }}
    46 | 47 | 48 |
    49 | 53 |
    54 | {% endblock %} 55 | 56 | 57 | {% block css %} 58 | {{ form.media.css }} 59 | {% endblock %} 60 | 61 | 62 | {% block javascript %} 63 | {% load static %} 64 | {{ form.media.js }} 65 | 66 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /courses/templates/courses/quiz_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load course_extras %} 3 | 4 | {% block title %}{{ step.title }} | {{ step.course.title }}{{ block.super }}{% endblock %} 5 | 6 | {% block breadcrumbs %} 7 |
  • {{ step.course.title }}
  • 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 | {{ block.super }} 14 |

    {{step.title}}

    15 |
      16 | {% for question in step.question_set.all %} 17 |
    • 18 |

      {{ question.prompt }}

      19 | {% for answer in question.answer_set.all %} 20 |
      21 | {{ answer.text }} 22 |
      23 |
      24 | {% endfor %} 25 | {% if user.is_authenticated %} 26 | Edit 27 |
      28 | {% endif %} 29 |
    • 30 | {% endfor %} 31 |
    32 |
    33 | 34 | {% if user.is_authenticated %} 35 | 41 | {% endif %} 42 | 43 |
    44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /courses/templates/courses/quiz_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load bootstrap4 %} 3 | 4 | {% block title %}{{ form.instance.title|default:"New Quiz"}} | {{ course.title }}{{ block.super }}{% endblock %} 5 | 6 | {% block breadcrumbs %} 7 |
  • {{ course.title }}
  • 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 | {{ block.super }} 13 |

    {{ form.instance.title|default:"Make a new quiz"}}

    14 |
    15 | {% csrf_token %} 16 | {% bootstrap_form form %} 17 | 18 |
    19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /courses/templates/courses/step_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load course_extras %} 3 | {% block title %}{{ step.title }} - {{ step.course.title }}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ step.course.title }}

    8 |

    {{ step.title }}

    9 | 10 | {% with con=step.content %} 11 | {{ con|linebreaks }} Content: {{ con|wordcount }} words. Estimated time to complete: {{ con|wordcount|time_estimate }} minute{{ con|wordcount|time_estimate|pluralize }}. 12 | {% endwith %} 13 |
    14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /courses/templates/courses/text_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load course_extras %} 3 | 4 | {% block title %}{{ step.title }} | {{ step.course.title }}{{ block.super }}{% endblock %} 5 | 6 | {% block breadcrumbs %} 7 |
  • {{ step.course.title }}
  • 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    13 | {{ block.super }} 14 |

    {{step.title}}

    15 | Quiz questions here 16 |
    17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /courses/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/templatetags/__init__.py -------------------------------------------------------------------------------- /courses/templatetags/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/templatetags/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /courses/templatetags/__pycache__/course_extras.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/courses/templatetags/__pycache__/course_extras.cpython-38.pyc -------------------------------------------------------------------------------- /courses/templatetags/course_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | from courses.models import Course 4 | import markdown 5 | 6 | register = template.Library() 7 | 8 | @register.simple_tag 9 | def newest_course(): 10 | ''' Gets the most recent course that was added to the library. ''' 11 | return Course.objects.latest('created_at') 12 | # register.simple_tag('newest_course') 13 | 14 | @register.inclusion_tag('courses/course_nav.html') 15 | def nav_courses_list(): 16 | ''' Return dictionary of courses to display as navigation pane ''' 17 | courses = Course.objects.all() 18 | return {'courses':courses} 19 | # register.inclusion_tag('courses/course_nav.html')(nav_courses_list) 20 | 21 | @register.filter('time_estimate') 22 | def time_estimate(word_count): 23 | ''' Estimate the number of minutes it will take to complete a step based 24 | on the passed-in wordcount. ''' 25 | minutes = round(word_count/20) 26 | return minutes 27 | 28 | @register.filter('markdown_to_html') 29 | def markdown_to_html(markdown_text): 30 | ''' Converts markdown text to HTML ''' 31 | html_body = markdown.markdown(markdown_text) 32 | return mark_safe(html_body) -------------------------------------------------------------------------------- /courses/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.test import TestCase 3 | from django.utils import timezone 4 | 5 | from .models import Course, Step 6 | 7 | class CourseModelTests(TestCase): 8 | def test_course_creation(self): 9 | course = Course.objects.create( 10 | title = "Python Regular Expressions", 11 | description = "Learn to write regular expression in Python" 12 | ) 13 | now = timezone.now() 14 | self.assertLess(course.created_at, now) 15 | 16 | 17 | class StepModelTests(TestCase): 18 | def setUp(self): 19 | self.course = Course.objects.create( 20 | title = "Python Testing", 21 | description = "Learn to write tests in Python" 22 | ) 23 | def test_step_creation(self): 24 | step = Step.objects.create( 25 | title = "Intro Docttest", 26 | description = "Learn to write Docttest string" 27 | # course=self.course 28 | ) 29 | self.assertIn(step, self.course.step_set.all()) 30 | 31 | 32 | class CourseViewsTests(TestCase): 33 | def setUp(self): 34 | self.course = Course.objects.create( 35 | title = "Python Testing", 36 | description = "Learn to write tests in Python" 37 | ) 38 | self.course2 = Course.objects.create( 39 | title = "New Course", 40 | description = "A new course" 41 | ) 42 | self.step = Step.objects.create( 43 | title = "Introduction to Doctest", 44 | description = "Learn to write tests in Your Doc String", 45 | course = self.course 46 | ) 47 | 48 | def test_course_list_view(self): 49 | resp = self.client.get(reverse('course_list')) 50 | self.assertEqual(resp.status_code, 200) 51 | self.assertIn(self.course, resp.context['courses']) 52 | self.assertIn(self.course2, resp.context['courses']) 53 | -------------------------------------------------------------------------------- /courses/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include,url 2 | from . import views 3 | 4 | urlpatterns = [ 5 | url(r'^$', views.course_list, name='course_list'), 6 | url(r'(?P\d+)/t(?P\d+)/$', views.text_detail, name='text_detail'), 7 | url(r'(?P\d+)/q(?P\d+)/$', views.quiz_detail, name='quiz_detail'), 8 | url(r'(?P\d+)/create_quiz/$', views.quiz_create, name='create_quiz'), 9 | url(r'(?P\d+)/edit_quiz(?P\d+)/$', views.quiz_edit, name='edit_quiz'), 10 | url(r'(?P\d+)/create_question/(?Pmc|tf)/$', views.create_question, name='create_question'), 11 | url(r'(?P\d+)/edit_question(?P\d+)/$', views.edit_question, name='edit_question'), 12 | url(r'(?P\d+)/create_answer/$', views.answer_form, name='create_answer'), 13 | url(r'(?P\d+)/$', views.course_detail, name='course_detail'), # This url should be under the step_detail url. 14 | ] 15 | -------------------------------------------------------------------------------- /courses/views.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from django.contrib import messages 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import HttpResponseRedirect 5 | 6 | from django.shortcuts import render, get_object_or_404 7 | from django.http import HttpResponse 8 | 9 | from . import forms 10 | from . import models 11 | 12 | 13 | def course_list(request): 14 | courses = models.Course.objects.all() 15 | email = 'questions@learning_site.com' 16 | # output = ', '.join([str(course) for course in courses]) # Joined Them Together With Commas 17 | # return HttpResponse(output) 18 | return render(request, 'courses/course_list.html', {'courses':courses, 'email':email}) 19 | 20 | 21 | def course_detail(request, pk): 22 | # course = Course.objects.get(pk=pk) 23 | course = get_object_or_404(models.Course, pk=pk) 24 | steps = sorted(chain(course.text_set.all(), course.quiz_set.all()), key=lambda step: step.order) 25 | return render(request, 'courses/course_detail.html', {'course':course, 'steps':steps}) 26 | 27 | 28 | def text_detail(request, course_pk, step_pk): 29 | step = get_object_or_404(models.Text, course_id=course_pk, pk=step_pk) 30 | return render(request, 'courses/step_detail.html', {'step':step}) 31 | 32 | 33 | def quiz_detail(request, course_pk, step_pk): 34 | step = get_object_or_404(models.Quiz, course_id=course_pk, pk=step_pk) 35 | return render(request, 'courses/quiz_detail.html', {'step':step}) 36 | 37 | @login_required 38 | def quiz_create(request, course_pk): 39 | course = get_object_or_404(models.Course, pk=course_pk) 40 | form = forms.QuizForm() 41 | 42 | if request.method == 'POST': 43 | form = forms.QuizForm(request.POST) 44 | if form.is_valid(): 45 | quiz = form.save(commit=False) 46 | quiz.course = course 47 | quiz.save() 48 | messages.add_message(request, messages.SUCCESS, "Quiz Added!") 49 | return HttpResponseRedirect(quiz.get_absolute_url()) 50 | 51 | return render(request, 'courses/quiz_form.html', {'form':form, 'course':course}) 52 | 53 | 54 | 55 | @login_required 56 | def quiz_edit(request, course_pk, quiz_pk): 57 | quiz = get_object_or_404(models.Quiz, pk=quiz_pk, course_id=course_pk) 58 | form = forms.QuizForm(instance=quiz) 59 | 60 | if request.method == 'POST': 61 | form = forms.QuizForm(instance=quiz, data=request.POST) 62 | if form.is_valid(): 63 | form.save() 64 | messages.success(request, "Updated {}".format(form.cleaned_data['title'])) 65 | return HttpResponseRedirect(quiz.get_absolute_url()) 66 | 67 | return render(request, 'courses/quiz_form.html', {'form':form, 'course':quiz.course}) 68 | 69 | @login_required 70 | def create_question(request, quiz_pk, question_type): 71 | quiz = get_object_or_404(models.Quiz, pk=quiz_pk) 72 | if question_type == 'tf': 73 | form_class = forms.TrueFalseQuestionForm 74 | else: 75 | form_class = forms.MultipleChoiceQuestionForm 76 | 77 | form = form_class() 78 | answer_forms = forms.AnswerInlineFormSet( 79 | queryset = models.Answer.objects.none() 80 | ) 81 | 82 | if request.method == 'POST': 83 | form = form_class(request.POST) 84 | answer_forms = forms.AnswerInlineFormSet(request.POST, queryset = models.Answer.objects.none()) 85 | 86 | if form.is_valid() and answer_forms.is_valid(): 87 | question = form.save(commit=False) 88 | question.quiz = quiz 89 | question.save() 90 | answers = answer_forms.save(commit=False) 91 | for answer in answers: 92 | answer.question = question 93 | answer.save() 94 | messages.success(request, "Added Question") 95 | return HttpResponseRedirect(quiz.get_absolute_url()) 96 | 97 | return render(request, 'courses/question_form.html', {'form':form, 'quiz':quiz, 'formset':answer_forms}) 98 | 99 | 100 | @login_required 101 | def edit_question(request, quiz_pk, question_pk): 102 | question = get_object_or_404(models.Question, pk=question_pk, quiz_id=quiz_pk) 103 | if hasattr(question, 'truefalsequestion'): 104 | form_class = forms.TrueFalseQuestionForm 105 | question = question.truefalsequestion 106 | else: 107 | form_class = forms.MultipleChoiceQuestionForm 108 | question = question.multiplechoicequestion 109 | form = form_class(instance=question) 110 | answer_forms = forms.AnswerInlineFormSet( 111 | queryset = form.instance.answer_set.all() 112 | ) 113 | 114 | if request.method == 'POST': 115 | form = form_class(request.POST, instance=question) 116 | answer_forms = forms.AnswerInlineFormSet( 117 | request.POST, 118 | queryset = form.instance.answer_set.all() 119 | ) 120 | if form.is_valid() and answer_forms.is_valid(): 121 | form.save() 122 | answers = answer_forms.save(commit=False) 123 | for answer in answers: 124 | answer.question = question 125 | answer.save() 126 | 127 | for answer in answer_forms.deleted_objects: 128 | answer.delete() 129 | messages.success(request, "Updated Question") 130 | return HttpResponseRedirect(question.quiz.get_absolute_url()) 131 | 132 | return render(request, 'courses/question_form.html', {'form':form, 'quiz':question.quiz, 'formset':answer_forms}) 133 | 134 | 135 | @login_required 136 | def answer_form(request, question_pk): 137 | question = get_object_or_404(models.Question, pk=question_pk) 138 | formset = forms.AnswerFormSet(queryset=question.answer_set.all()) 139 | 140 | if request.method == 'POST': 141 | formset = forms.AnswerFormSet(request.POST, queryset=question.answer_set.all()) 142 | if formset.is_valid(): 143 | answers = formset.save(commit=False) 144 | 145 | for answer in answers: 146 | answer.question = question 147 | answer.save() 148 | messages.success(request, "Added Answers") 149 | return HttpResponseRedirect(question.quiz.get_absolute_url()) 150 | 151 | return render(request, 'courses/answer_form.html', {'formset':formset, 'question':question}) 152 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/db.sqlite3 -------------------------------------------------------------------------------- /learning_site/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__init__.py -------------------------------------------------------------------------------- /learning_site/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/__pycache__/forms.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/forms.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/__pycache__/settings.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/settings.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/__pycache__/wsgi.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/learning_site/__pycache__/wsgi.cpython-38.pyc -------------------------------------------------------------------------------- /learning_site/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for learning_site project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learning_site.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /learning_site/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core import validators 3 | 4 | 5 | def must_be_empty(value): 6 | if value: 7 | raise forms.ValidationError('is not empty') 8 | 9 | class SuggestionForm(forms.Form): 10 | name = forms.CharField() 11 | email = forms.EmailField() 12 | verify_email = forms.EmailField(help_text="Please verify your email address") 13 | suggestion = forms.CharField(widget=forms.Textarea) 14 | honeypot = forms.CharField(required=False, 15 | widget=forms.HiddenInput, label="Leave empty", 16 | validators=[must_be_empty]) 17 | 18 | def clean(self): 19 | cleaned_data = super().clean() 20 | email = cleaned_data.get['email'] 21 | verify = cleaned_data.get['verify_email'] 22 | 23 | if email != verify: 24 | raise forms.ValidationError("You need to enter the same email in both fields") 25 | -------------------------------------------------------------------------------- /learning_site/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for learning_site project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'g)klwg_7p0att0(4w3)0j(yf)e=1bazzh7rw7n6^7*j@=8&5ky' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'courses', 41 | 'accounts', 42 | 'django.contrib.humanize', 43 | 'bootstrap4', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'learning_site.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': ['templates',], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'learning_site.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.sqlite3', 83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'Asia/Dhaka' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | 126 | STATICFILES_DIRS = ( 127 | os.path.join(BASE_DIR, 'assets'), 128 | ) 129 | 130 | LOGIN_REDIRECT_URL = 'home' 131 | 132 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 133 | MEDIA_URL = '/media/' 134 | 135 | # When Form Is Submitted Then You Will Get An Email. 136 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 137 | EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'suggestions') 138 | 139 | LOGIN_URL = 'login' 140 | -------------------------------------------------------------------------------- /learning_site/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from django.conf.urls import include,url 5 | from . import views 6 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 7 | from django.conf import settings 8 | from django.conf.urls.static import static 9 | 10 | urlpatterns = [ 11 | url(r'^admin/', admin.site.urls), 12 | url(r'^courses/', include('courses.urls')), 13 | url(r'^', include('accounts.urls')), 14 | url(r'^suggest/$', views.suggestion_view, name='suggestion'), 15 | url(r'^$', views.home, name='home'), 16 | ] 17 | 18 | urlpatterns += staticfiles_urlpatterns() 19 | 20 | if settings.DEBUG: 21 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -------------------------------------------------------------------------------- /learning_site/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.core.mail import send_mail 3 | from django.urls import reverse 4 | from django.http import HttpResponseRedirect 5 | 6 | from django.shortcuts import render 7 | from .import forms 8 | 9 | def home(request): 10 | return render(request, 'home.html') 11 | 12 | 13 | def suggestion_view(request): 14 | form = forms.SuggestionForm() 15 | if request.method == 'POST': 16 | form = forms.SuggestionForm(request.POST) 17 | if form.is_valid(): 18 | send_mail( 19 | 'Suggestion from {}'.format(form.cleaned_data['name']), # Subject 20 | form.cleaned_data['suggestion'], # Body 21 | '{name} <{email}>'.format(**form.cleaned_data), # From email 22 | ['omarfaruk2468@omar.com'], # Sent To 23 | ) 24 | messages.add_message(request, messages.SUCCESS, 'Thanks For Your Suggestion!') 25 | return HttpResponseRedirect(reverse('suggestion')) 26 | return render(request, 'suggestion_form.html', {'form': form}) -------------------------------------------------------------------------------- /learning_site/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for learning_site project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learning_site.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learning_site.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /media/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/media/default.jpg -------------------------------------------------------------------------------- /media/profile_pics/FB_IMG_1563211510479.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IamOmaR22/eLearning-Website-with-Django-and-Python/673199794a17ed2c9e15e015b82e48f139115482/media/profile_pics/FB_IMG_1563211510479.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.7 2 | beautifulsoup4==4.9.0 3 | Django==3.1.14 4 | django-bootstrap4==1.1.1 5 | django-crispy-forms==1.9.2 6 | Markdown==3.2.1 7 | Pillow==9.0.1 8 | pytz==2019.3 9 | soupsieve==2.0 10 | sqlparse==0.3.1 11 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Well Hello There!{% endblock %} 4 | 5 | {% block content %} 6 |

    Welcome!

    7 | {% endblock %} -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load course_extras %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock %} 10 | 11 | {% block css %}{% endblock %} 12 | 13 | 14 | 15 |
    16 |
    {% nav_courses_list %}
    17 | 31 | 32 | 33 | {% block content %}{% endblock %} 34 | 35 | 36 | 37 | 38 |
    39 |
    40 |
    41 |

    Omar's eLearning Site

    42 |
    43 |
    44 | Courses 45 |
    46 |
    47 | 48 |
    49 | 50 | 51 | 52 | 53 | {% block javascript %}{% endblock %} 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /templates/suggestion_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load bootstrap4 %} 3 | 4 | {% block title %}Suggest an idea!{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 | {% csrf_token %} 11 | {% bootstrap_form form %} 12 | 13 | 14 |
    15 |
    16 |
    17 | {% endblock %} -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load bootstrap4 %} 3 | 4 | {% block title %}Login!{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    Sign In

    9 |
    10 | {% csrf_token %} 11 | {% bootstrap_form form %} 12 | 13 |
    14 |
    15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /templates/users/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load bootstrap4 %} 3 | 4 | {% block title %}Profile!{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |
    9 |
    10 | 11 |
    12 | 13 |

    {{ user.email }}

    14 |
    15 |
    16 | 17 | 18 |
    19 | {% csrf_token %} 20 | 21 |
    22 | Profile Info 23 | 24 | {% bootstrap_form u_form %} 25 | {% bootstrap_form p_form %} 26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 | 33 |
    34 |
    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load bootstrap4 %} 3 | 4 | {% block title %}Registration!{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    Registration

    9 |
    10 | {% csrf_token %} 11 | {% bootstrap_form form %} 12 | 13 |
    14 |
    15 | {% endblock content %} --------------------------------------------------------------------------------