├── .gitignore ├── LICENSE ├── README.md ├── django_school ├── classroom │ ├── __init__.py │ ├── apps.py │ ├── decorators.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_create_initial_subjects.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── classroom │ │ │ ├── home.html │ │ │ ├── students │ │ │ ├── _header.html │ │ │ ├── interests_form.html │ │ │ ├── quiz_list.html │ │ │ ├── take_quiz_form.html │ │ │ └── taken_quiz_list.html │ │ │ └── teachers │ │ │ ├── question_add_form.html │ │ │ ├── question_change_form.html │ │ │ ├── question_delete_confirm.html │ │ │ ├── quiz_add_form.html │ │ │ ├── quiz_change_form.html │ │ │ ├── quiz_change_list.html │ │ │ ├── quiz_delete_confirm.html │ │ │ └── quiz_results.html │ ├── urls.py │ └── views │ │ ├── __init__.py │ │ ├── classroom.py │ │ ├── students.py │ │ └── teachers.py ├── django_school │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── static │ ├── css │ │ ├── app.css │ │ ├── students.css │ │ └── teachers.css │ ├── img │ │ ├── favicon.ico │ │ └── favicon.png │ └── vendor │ │ └── fontello-2f186091 │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── config.json │ │ ├── css │ │ ├── animation.css │ │ ├── fontello-codes.css │ │ ├── fontello-embedded.css │ │ ├── fontello-ie7-codes.css │ │ ├── fontello-ie7.css │ │ └── fontello.css │ │ ├── demo.html │ │ └── font │ │ ├── fontello.eot │ │ ├── fontello.svg │ │ ├── fontello.ttf │ │ ├── fontello.woff │ │ └── fontello.woff2 └── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ └── registration │ ├── login.html │ ├── signup.html │ └── signup_form.html └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | *.sqlite3 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simple is Better Than Complex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django School 2 | 3 | [![Python Version](https://img.shields.io/badge/python-3.6-brightgreen.svg)](https://python.org) 4 | [![Django Version](https://img.shields.io/badge/django-2.0-brightgreen.svg)](https://djangoproject.com) 5 | 6 | This is an example project to illustrate an implementation of multiple user types. In this Django app, teachers can create quizzes and students can sign up and take quizzes related to their interests. 7 | 8 | ![Django School Screenshot](https://simpleisbetterthancomplex.com/media/2018/01/teacher-quiz.png) 9 | 10 | Read the blog post [How to Implement Multiple User Types with Django](https://simpleisbetterthancomplex.com/tutorial/2018/01/18/how-to-implement-multiple-user-types-with-django.html). 11 | 12 | ## Running the Project Locally 13 | 14 | First, clone the repository to your local machine: 15 | 16 | ```bash 17 | git clone https://github.com/sibtc/django-multiple-user-types-example.git 18 | ``` 19 | 20 | Install the requirements: 21 | 22 | ```bash 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | Create the database: 27 | 28 | ```bash 29 | python manage.py migrate 30 | ``` 31 | 32 | Finally, run the development server: 33 | 34 | ```bash 35 | python manage.py runserver 36 | ``` 37 | 38 | The project will be available at **127.0.0.1:8000**. 39 | 40 | 41 | ## License 42 | 43 | The source code is released under the [MIT License](https://github.com/sibtc/django-multiple-user-types-example/blob/master/LICENSE). 44 | -------------------------------------------------------------------------------- /django_school/classroom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/classroom/__init__.py -------------------------------------------------------------------------------- /django_school/classroom/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ClassroomConfig(AppConfig): 5 | name = 'classroom' 6 | -------------------------------------------------------------------------------- /django_school/classroom/decorators.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import REDIRECT_FIELD_NAME 2 | from django.contrib.auth.decorators import user_passes_test 3 | 4 | 5 | def student_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'): 6 | ''' 7 | Decorator for views that checks that the logged in user is a student, 8 | redirects to the log-in page if necessary. 9 | ''' 10 | actual_decorator = user_passes_test( 11 | lambda u: u.is_active and u.is_student, 12 | login_url=login_url, 13 | redirect_field_name=redirect_field_name 14 | ) 15 | if function: 16 | return actual_decorator(function) 17 | return actual_decorator 18 | 19 | 20 | def teacher_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'): 21 | ''' 22 | Decorator for views that checks that the logged in user is a teacher, 23 | redirects to the log-in page if necessary. 24 | ''' 25 | actual_decorator = user_passes_test( 26 | lambda u: u.is_active and u.is_teacher, 27 | login_url=login_url, 28 | redirect_field_name=redirect_field_name 29 | ) 30 | if function: 31 | return actual_decorator(function) 32 | return actual_decorator 33 | -------------------------------------------------------------------------------- /django_school/classroom/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.db import transaction 4 | from django.forms.utils import ValidationError 5 | 6 | from classroom.models import (Answer, Question, Student, StudentAnswer, 7 | Subject, User) 8 | 9 | 10 | class TeacherSignUpForm(UserCreationForm): 11 | class Meta(UserCreationForm.Meta): 12 | model = User 13 | 14 | def save(self, commit=True): 15 | user = super().save(commit=False) 16 | user.is_teacher = True 17 | if commit: 18 | user.save() 19 | return user 20 | 21 | 22 | class StudentSignUpForm(UserCreationForm): 23 | interests = forms.ModelMultipleChoiceField( 24 | queryset=Subject.objects.all(), 25 | widget=forms.CheckboxSelectMultiple, 26 | required=True 27 | ) 28 | 29 | class Meta(UserCreationForm.Meta): 30 | model = User 31 | 32 | @transaction.atomic 33 | def save(self): 34 | user = super().save(commit=False) 35 | user.is_student = True 36 | user.save() 37 | student = Student.objects.create(user=user) 38 | student.interests.add(*self.cleaned_data.get('interests')) 39 | return user 40 | 41 | 42 | class StudentInterestsForm(forms.ModelForm): 43 | class Meta: 44 | model = Student 45 | fields = ('interests', ) 46 | widgets = { 47 | 'interests': forms.CheckboxSelectMultiple 48 | } 49 | 50 | 51 | class QuestionForm(forms.ModelForm): 52 | class Meta: 53 | model = Question 54 | fields = ('text', ) 55 | 56 | 57 | class BaseAnswerInlineFormSet(forms.BaseInlineFormSet): 58 | def clean(self): 59 | super().clean() 60 | 61 | has_one_correct_answer = False 62 | for form in self.forms: 63 | if not form.cleaned_data.get('DELETE', False): 64 | if form.cleaned_data.get('is_correct', False): 65 | has_one_correct_answer = True 66 | break 67 | if not has_one_correct_answer: 68 | raise ValidationError('Mark at least one answer as correct.', code='no_correct_answer') 69 | 70 | 71 | class TakeQuizForm(forms.ModelForm): 72 | answer = forms.ModelChoiceField( 73 | queryset=Answer.objects.none(), 74 | widget=forms.RadioSelect(), 75 | required=True, 76 | empty_label=None) 77 | 78 | class Meta: 79 | model = StudentAnswer 80 | fields = ('answer', ) 81 | 82 | def __init__(self, *args, **kwargs): 83 | question = kwargs.pop('question') 84 | super().__init__(*args, **kwargs) 85 | self.fields['answer'].queryset = question.answers.order_by('text') 86 | -------------------------------------------------------------------------------- /django_school/classroom/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-18 18:07 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | from django.conf import settings 8 | from django.db import migrations, models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0009_alter_user_last_name_max_length'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='User', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('is_student', models.BooleanField(default=False)), 35 | ('is_teacher', models.BooleanField(default=False)), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | migrations.CreateModel( 47 | name='Answer', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('text', models.CharField(max_length=255, verbose_name='Answer')), 51 | ('is_correct', models.BooleanField(default=False, verbose_name='Correct answer')), 52 | ], 53 | ), 54 | migrations.CreateModel( 55 | name='Question', 56 | fields=[ 57 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('text', models.CharField(max_length=255, verbose_name='Question')), 59 | ], 60 | ), 61 | migrations.CreateModel( 62 | name='Quiz', 63 | fields=[ 64 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 65 | ('name', models.CharField(max_length=255)), 66 | ], 67 | ), 68 | migrations.CreateModel( 69 | name='StudentAnswer', 70 | fields=[ 71 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 72 | ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='classroom.Answer')), 73 | ], 74 | ), 75 | migrations.CreateModel( 76 | name='Subject', 77 | fields=[ 78 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 79 | ('name', models.CharField(max_length=30)), 80 | ('color', models.CharField(default='#007bff', max_length=7)), 81 | ], 82 | ), 83 | migrations.CreateModel( 84 | name='TakenQuiz', 85 | fields=[ 86 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 87 | ('score', models.FloatField()), 88 | ('date', models.DateTimeField(auto_now_add=True)), 89 | ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taken_quizzes', to='classroom.Quiz')), 90 | ], 91 | ), 92 | migrations.CreateModel( 93 | name='Student', 94 | fields=[ 95 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), 96 | ('interests', models.ManyToManyField(related_name='interested_students', to='classroom.Subject')), 97 | ], 98 | ), 99 | migrations.AddField( 100 | model_name='quiz', 101 | name='owner', 102 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to=settings.AUTH_USER_MODEL), 103 | ), 104 | migrations.AddField( 105 | model_name='quiz', 106 | name='subject', 107 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='classroom.Subject'), 108 | ), 109 | migrations.AddField( 110 | model_name='question', 111 | name='quiz', 112 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='classroom.Quiz'), 113 | ), 114 | migrations.AddField( 115 | model_name='answer', 116 | name='question', 117 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='classroom.Question'), 118 | ), 119 | migrations.AddField( 120 | model_name='user', 121 | name='groups', 122 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), 123 | ), 124 | migrations.AddField( 125 | model_name='user', 126 | name='user_permissions', 127 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), 128 | ), 129 | migrations.AddField( 130 | model_name='takenquiz', 131 | name='student', 132 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taken_quizzes', to='classroom.Student'), 133 | ), 134 | migrations.AddField( 135 | model_name='studentanswer', 136 | name='student', 137 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz_answers', to='classroom.Student'), 138 | ), 139 | migrations.AddField( 140 | model_name='student', 141 | name='quizzes', 142 | field=models.ManyToManyField(through='classroom.TakenQuiz', to='classroom.Quiz'), 143 | ), 144 | ] 145 | -------------------------------------------------------------------------------- /django_school/classroom/migrations/0002_create_initial_subjects.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-18 18:07 2 | 3 | from django.db import migrations 4 | 5 | 6 | def create_subjects(apps, schema_editor): 7 | Subject = apps.get_model('classroom', 'Subject') 8 | Subject.objects.create(name='Arts', color='#343a40') 9 | Subject.objects.create(name='Computing', color='#007bff') 10 | Subject.objects.create(name='Math', color='#28a745') 11 | Subject.objects.create(name='Biology', color='#17a2b8') 12 | Subject.objects.create(name='History', color='#ffc107') 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('classroom', '0001_initial'), 19 | ] 20 | 21 | operations = [ 22 | migrations.RunPython(create_subjects), 23 | ] 24 | -------------------------------------------------------------------------------- /django_school/classroom/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/classroom/migrations/__init__.py -------------------------------------------------------------------------------- /django_school/classroom/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | from django.utils.html import escape, mark_safe 4 | 5 | 6 | class User(AbstractUser): 7 | is_student = models.BooleanField(default=False) 8 | is_teacher = models.BooleanField(default=False) 9 | 10 | 11 | class Subject(models.Model): 12 | name = models.CharField(max_length=30) 13 | color = models.CharField(max_length=7, default='#007bff') 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | def get_html_badge(self): 19 | name = escape(self.name) 20 | color = escape(self.color) 21 | html = '%s' % (color, name) 22 | return mark_safe(html) 23 | 24 | 25 | class Quiz(models.Model): 26 | owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quizzes') 27 | name = models.CharField(max_length=255) 28 | subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='quizzes') 29 | 30 | def __str__(self): 31 | return self.name 32 | 33 | 34 | class Question(models.Model): 35 | quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='questions') 36 | text = models.CharField('Question', max_length=255) 37 | 38 | def __str__(self): 39 | return self.text 40 | 41 | 42 | class Answer(models.Model): 43 | question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers') 44 | text = models.CharField('Answer', max_length=255) 45 | is_correct = models.BooleanField('Correct answer', default=False) 46 | 47 | def __str__(self): 48 | return self.text 49 | 50 | 51 | class Student(models.Model): 52 | user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) 53 | quizzes = models.ManyToManyField(Quiz, through='TakenQuiz') 54 | interests = models.ManyToManyField(Subject, related_name='interested_students') 55 | 56 | def get_unanswered_questions(self, quiz): 57 | answered_questions = self.quiz_answers \ 58 | .filter(answer__question__quiz=quiz) \ 59 | .values_list('answer__question__pk', flat=True) 60 | questions = quiz.questions.exclude(pk__in=answered_questions).order_by('text') 61 | return questions 62 | 63 | def __str__(self): 64 | return self.user.username 65 | 66 | 67 | class TakenQuiz(models.Model): 68 | student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='taken_quizzes') 69 | quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='taken_quizzes') 70 | score = models.FloatField() 71 | date = models.DateTimeField(auto_now_add=True) 72 | 73 | 74 | class StudentAnswer(models.Model): 75 | student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='quiz_answers') 76 | answer = models.ForeignKey(Answer, on_delete=models.CASCADE, related_name='+') 77 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Welcome to the Django Schools!

5 |

6 | If you already have an account, go ahead and log in. If you are new to Django Schools, get started 7 | by creating a student account or a teacher account. 8 |

9 |
10 |

What's this about?

11 |

12 | This Django application is an example I created to illustrate a blog post about how to implement multiple user types. 13 | In this application, users can sign up as a student or a teacher. Teachers can create quizzes and students can answer quizzes 14 | based on their interests. 15 |

16 |

Want to run this code locally? Read detailed instructions on how to run this project.

17 |

Vitor Freitas
@vitorfs

18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/students/_header.html: -------------------------------------------------------------------------------- 1 |

Quizzes

2 |

3 | Subjects:{% for subject in user.student.interests.all %} {{ subject.get_html_badge }}{% endfor %} 4 | (update interests) 5 |

6 | 7 | 15 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/students/interests_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 |

Update your interests

7 |
8 | {% csrf_token %} 9 | {{ form|crispy }} 10 | 11 | Nevermind 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/students/quiz_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | {% include 'classroom/students/_header.html' with active='new' %} 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for quiz in quizzes %} 17 | 18 | 19 | 20 | 21 | 24 | 25 | {% empty %} 26 | 27 | 28 | 29 | {% endfor %} 30 | 31 |
QuizSubjectLength
{{ quiz.name }}{{ quiz.subject.get_html_badge }}{{ quiz.questions_count }} questions 22 | Start quiz 23 |
No quiz matching your interests right now.
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/students/take_quiz_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

{{ quiz.name }}

10 |

{{ question.text }}

11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/students/taken_quiz_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | {% include 'classroom/students/_header.html' with active='taken' %} 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for taken_quiz in taken_quizzes %} 16 | 17 | 18 | 19 | 20 | 21 | {% empty %} 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
QuizSubjectScore
{{ taken_quiz.quiz.name }}{{ taken_quiz.quiz.subject.get_html_badge }}{{ taken_quiz.score }}
You haven't completed any quiz yet.
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/question_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | 13 |

Add a new question

14 |

Add first the text of the question. In the next step you will be able to add the possible answers.

15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 | Nevermind 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/question_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags crispy_forms_filters %} 4 | 5 | {% block content %} 6 | 13 |

{{ question.txt }}

14 |
15 | {% csrf_token %} 16 | {{ formset.management_form }} 17 | {{ form|crispy }} 18 |
19 |
20 |
21 |
22 | Answers 23 |
24 |
25 | Correct? 26 |
27 |
28 | Delete? 29 |
30 |
31 |
32 | {% for error in formset.non_form_errors %} 33 |
{{ error }}
34 | {% endfor %} 35 |
36 | {% for form in formset %} 37 |
38 |
39 |
40 | {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} 41 | {{ form.text|as_crispy_field }} 42 | {% if form.instance.pk and form.text.value != form.instance.text %}

Old answer: {{ form.instance.text }}

{% endif %} 43 |
44 |
45 | {{ form.is_correct }} 46 |
47 |
48 | {% if form.instance.pk %} 49 | {{ form.DELETE }} 50 | {% endif %} 51 |
52 |
53 |
54 | {% endfor %} 55 |
56 |
57 |

58 | Your question may have at least 2 answers and maximum 10 answers. Select at least one correct answer. 59 |

60 | 61 | Nevermind 62 | Delete 63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/question_delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | 14 |

Confirm deletion

15 |

Are you sure you want to delete the question "{{ question.text }}"? There is no going back.

16 |
17 | {% csrf_token %} 18 | 19 | Nevermind 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/quiz_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | 12 |

Add a new quiz

13 |
14 |
15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 | Nevermind 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/quiz_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | 12 |

13 | {{ quiz.name }} 14 | View results 15 |

16 |
17 |
18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 | Nevermind 23 | Delete 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Questions 32 |
33 |
34 | Answers 35 |
36 |
37 |
38 |
39 | {% for question in questions %} 40 |
41 |
42 | 45 |
46 | {{ question.answers_count }} 47 |
48 |
49 |
50 | {% empty %} 51 |
52 |

You haven't created any questions yet. Go ahead and add the first question.

53 |
54 | {% endfor %} 55 |
56 | 59 |
60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/quiz_change_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 9 |

My Quizzes

10 | Add quiz 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for quiz in quizzes %} 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | {% empty %} 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 |
QuizSubjectQuestionsTaken
{{ quiz.name }}{{ quiz.subject.get_html_badge }}{{ quiz.questions_count }}{{ quiz.taken_count }} 30 | View results 31 |
You haven't created any quiz yet.
40 |
41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/quiz_delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | 13 |

Confirm deletion

14 |

Are you sure you want to delete the quiz "{{ quiz.name }}"? There is no going back.

15 |
16 | {% csrf_token %} 17 | 18 | Nevermind 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /django_school/classroom/templates/classroom/teachers/quiz_results.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags humanize %} 4 | 5 | {% block content %} 6 | 13 |

{{ quiz.name }} Results

14 | 15 |
16 |
17 | Taken Quizzes 18 | Average Score: {{ quiz_score.average_score|default_if_none:0.0 }} 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for taken_quiz in taken_quizzes %} 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
StudentDateScore
{{ taken_quiz.student.user.username }}{{ taken_quiz.date|naturaltime }}{{ taken_quiz.score }}
38 | 41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /django_school/classroom/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from .views import classroom, students, teachers 4 | 5 | urlpatterns = [ 6 | path('', classroom.home, name='home'), 7 | 8 | path('students/', include(([ 9 | path('', students.QuizListView.as_view(), name='quiz_list'), 10 | path('interests/', students.StudentInterestsView.as_view(), name='student_interests'), 11 | path('taken/', students.TakenQuizListView.as_view(), name='taken_quiz_list'), 12 | path('quiz//', students.take_quiz, name='take_quiz'), 13 | ], 'classroom'), namespace='students')), 14 | 15 | path('teachers/', include(([ 16 | path('', teachers.QuizListView.as_view(), name='quiz_change_list'), 17 | path('quiz/add/', teachers.QuizCreateView.as_view(), name='quiz_add'), 18 | path('quiz//', teachers.QuizUpdateView.as_view(), name='quiz_change'), 19 | path('quiz//delete/', teachers.QuizDeleteView.as_view(), name='quiz_delete'), 20 | path('quiz//results/', teachers.QuizResultsView.as_view(), name='quiz_results'), 21 | path('quiz//question/add/', teachers.question_add, name='question_add'), 22 | path('quiz//question//', teachers.question_change, name='question_change'), 23 | path('quiz//question//delete/', teachers.QuestionDeleteView.as_view(), name='question_delete'), 24 | ], 'classroom'), namespace='teachers')), 25 | ] 26 | -------------------------------------------------------------------------------- /django_school/classroom/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/classroom/views/__init__.py -------------------------------------------------------------------------------- /django_school/classroom/views/classroom.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect, render 2 | from django.views.generic import TemplateView 3 | 4 | 5 | class SignUpView(TemplateView): 6 | template_name = 'registration/signup.html' 7 | 8 | 9 | def home(request): 10 | if request.user.is_authenticated: 11 | if request.user.is_teacher: 12 | return redirect('teachers:quiz_change_list') 13 | else: 14 | return redirect('students:quiz_list') 15 | return render(request, 'classroom/home.html') 16 | -------------------------------------------------------------------------------- /django_school/classroom/views/students.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import login 3 | from django.contrib.auth.decorators import login_required 4 | from django.db import transaction 5 | from django.db.models import Count 6 | from django.shortcuts import get_object_or_404, redirect, render 7 | from django.urls import reverse_lazy 8 | from django.utils.decorators import method_decorator 9 | from django.views.generic import CreateView, ListView, UpdateView 10 | 11 | from ..decorators import student_required 12 | from ..forms import StudentInterestsForm, StudentSignUpForm, TakeQuizForm 13 | from ..models import Quiz, Student, TakenQuiz, User 14 | 15 | 16 | class StudentSignUpView(CreateView): 17 | model = User 18 | form_class = StudentSignUpForm 19 | template_name = 'registration/signup_form.html' 20 | 21 | def get_context_data(self, **kwargs): 22 | kwargs['user_type'] = 'student' 23 | return super().get_context_data(**kwargs) 24 | 25 | def form_valid(self, form): 26 | user = form.save() 27 | login(self.request, user) 28 | return redirect('students:quiz_list') 29 | 30 | 31 | @method_decorator([login_required, student_required], name='dispatch') 32 | class StudentInterestsView(UpdateView): 33 | model = Student 34 | form_class = StudentInterestsForm 35 | template_name = 'classroom/students/interests_form.html' 36 | success_url = reverse_lazy('students:quiz_list') 37 | 38 | def get_object(self): 39 | return self.request.user.student 40 | 41 | def form_valid(self, form): 42 | messages.success(self.request, 'Interests updated with success!') 43 | return super().form_valid(form) 44 | 45 | 46 | @method_decorator([login_required, student_required], name='dispatch') 47 | class QuizListView(ListView): 48 | model = Quiz 49 | ordering = ('name', ) 50 | context_object_name = 'quizzes' 51 | template_name = 'classroom/students/quiz_list.html' 52 | 53 | def get_queryset(self): 54 | student = self.request.user.student 55 | student_interests = student.interests.values_list('pk', flat=True) 56 | taken_quizzes = student.quizzes.values_list('pk', flat=True) 57 | queryset = Quiz.objects.filter(subject__in=student_interests) \ 58 | .exclude(pk__in=taken_quizzes) \ 59 | .annotate(questions_count=Count('questions')) \ 60 | .filter(questions_count__gt=0) 61 | return queryset 62 | 63 | 64 | @method_decorator([login_required, student_required], name='dispatch') 65 | class TakenQuizListView(ListView): 66 | model = TakenQuiz 67 | context_object_name = 'taken_quizzes' 68 | template_name = 'classroom/students/taken_quiz_list.html' 69 | 70 | def get_queryset(self): 71 | queryset = self.request.user.student.taken_quizzes \ 72 | .select_related('quiz', 'quiz__subject') \ 73 | .order_by('quiz__name') 74 | return queryset 75 | 76 | 77 | @login_required 78 | @student_required 79 | def take_quiz(request, pk): 80 | quiz = get_object_or_404(Quiz, pk=pk) 81 | student = request.user.student 82 | 83 | if student.quizzes.filter(pk=pk).exists(): 84 | return render(request, 'students/taken_quiz.html') 85 | 86 | total_questions = quiz.questions.count() 87 | unanswered_questions = student.get_unanswered_questions(quiz) 88 | total_unanswered_questions = unanswered_questions.count() 89 | progress = 100 - round(((total_unanswered_questions - 1) / total_questions) * 100) 90 | question = unanswered_questions.first() 91 | 92 | if request.method == 'POST': 93 | form = TakeQuizForm(question=question, data=request.POST) 94 | if form.is_valid(): 95 | with transaction.atomic(): 96 | student_answer = form.save(commit=False) 97 | student_answer.student = student 98 | student_answer.save() 99 | if student.get_unanswered_questions(quiz).exists(): 100 | return redirect('students:take_quiz', pk) 101 | else: 102 | correct_answers = student.quiz_answers.filter(answer__question__quiz=quiz, answer__is_correct=True).count() 103 | score = round((correct_answers / total_questions) * 100.0, 2) 104 | TakenQuiz.objects.create(student=student, quiz=quiz, score=score) 105 | if score < 50.0: 106 | messages.warning(request, 'Better luck next time! Your score for the quiz %s was %s.' % (quiz.name, score)) 107 | else: 108 | messages.success(request, 'Congratulations! You completed the quiz %s with success! You scored %s points.' % (quiz.name, score)) 109 | return redirect('students:quiz_list') 110 | else: 111 | form = TakeQuizForm(question=question) 112 | 113 | return render(request, 'classroom/students/take_quiz_form.html', { 114 | 'quiz': quiz, 115 | 'question': question, 116 | 'form': form, 117 | 'progress': progress 118 | }) 119 | -------------------------------------------------------------------------------- /django_school/classroom/views/teachers.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import login 3 | from django.contrib.auth.decorators import login_required 4 | from django.db import transaction 5 | from django.db.models import Avg, Count 6 | from django.forms import inlineformset_factory 7 | from django.shortcuts import get_object_or_404, redirect, render 8 | from django.urls import reverse, reverse_lazy 9 | from django.utils.decorators import method_decorator 10 | from django.views.generic import (CreateView, DeleteView, DetailView, ListView, 11 | UpdateView) 12 | 13 | from ..decorators import teacher_required 14 | from ..forms import BaseAnswerInlineFormSet, QuestionForm, TeacherSignUpForm 15 | from ..models import Answer, Question, Quiz, User 16 | 17 | 18 | class TeacherSignUpView(CreateView): 19 | model = User 20 | form_class = TeacherSignUpForm 21 | template_name = 'registration/signup_form.html' 22 | 23 | def get_context_data(self, **kwargs): 24 | kwargs['user_type'] = 'teacher' 25 | return super().get_context_data(**kwargs) 26 | 27 | def form_valid(self, form): 28 | user = form.save() 29 | login(self.request, user) 30 | return redirect('teachers:quiz_change_list') 31 | 32 | 33 | @method_decorator([login_required, teacher_required], name='dispatch') 34 | class QuizListView(ListView): 35 | model = Quiz 36 | ordering = ('name', ) 37 | context_object_name = 'quizzes' 38 | template_name = 'classroom/teachers/quiz_change_list.html' 39 | 40 | def get_queryset(self): 41 | queryset = self.request.user.quizzes \ 42 | .select_related('subject') \ 43 | .annotate(questions_count=Count('questions', distinct=True)) \ 44 | .annotate(taken_count=Count('taken_quizzes', distinct=True)) 45 | return queryset 46 | 47 | 48 | @method_decorator([login_required, teacher_required], name='dispatch') 49 | class QuizCreateView(CreateView): 50 | model = Quiz 51 | fields = ('name', 'subject', ) 52 | template_name = 'classroom/teachers/quiz_add_form.html' 53 | 54 | def form_valid(self, form): 55 | quiz = form.save(commit=False) 56 | quiz.owner = self.request.user 57 | quiz.save() 58 | messages.success(self.request, 'The quiz was created with success! Go ahead and add some questions now.') 59 | return redirect('teachers:quiz_change', quiz.pk) 60 | 61 | 62 | @method_decorator([login_required, teacher_required], name='dispatch') 63 | class QuizUpdateView(UpdateView): 64 | model = Quiz 65 | fields = ('name', 'subject', ) 66 | context_object_name = 'quiz' 67 | template_name = 'classroom/teachers/quiz_change_form.html' 68 | 69 | def get_context_data(self, **kwargs): 70 | kwargs['questions'] = self.get_object().questions.annotate(answers_count=Count('answers')) 71 | return super().get_context_data(**kwargs) 72 | 73 | def get_queryset(self): 74 | ''' 75 | This method is an implicit object-level permission management 76 | This view will only match the ids of existing quizzes that belongs 77 | to the logged in user. 78 | ''' 79 | return self.request.user.quizzes.all() 80 | 81 | def get_success_url(self): 82 | return reverse('teachers:quiz_change', kwargs={'pk': self.object.pk}) 83 | 84 | 85 | @method_decorator([login_required, teacher_required], name='dispatch') 86 | class QuizDeleteView(DeleteView): 87 | model = Quiz 88 | context_object_name = 'quiz' 89 | template_name = 'classroom/teachers/quiz_delete_confirm.html' 90 | success_url = reverse_lazy('teachers:quiz_change_list') 91 | 92 | def delete(self, request, *args, **kwargs): 93 | quiz = self.get_object() 94 | messages.success(request, 'The quiz %s was deleted with success!' % quiz.name) 95 | return super().delete(request, *args, **kwargs) 96 | 97 | def get_queryset(self): 98 | return self.request.user.quizzes.all() 99 | 100 | 101 | @method_decorator([login_required, teacher_required], name='dispatch') 102 | class QuizResultsView(DetailView): 103 | model = Quiz 104 | context_object_name = 'quiz' 105 | template_name = 'classroom/teachers/quiz_results.html' 106 | 107 | def get_context_data(self, **kwargs): 108 | quiz = self.get_object() 109 | taken_quizzes = quiz.taken_quizzes.select_related('student__user').order_by('-date') 110 | total_taken_quizzes = taken_quizzes.count() 111 | quiz_score = quiz.taken_quizzes.aggregate(average_score=Avg('score')) 112 | extra_context = { 113 | 'taken_quizzes': taken_quizzes, 114 | 'total_taken_quizzes': total_taken_quizzes, 115 | 'quiz_score': quiz_score 116 | } 117 | kwargs.update(extra_context) 118 | return super().get_context_data(**kwargs) 119 | 120 | def get_queryset(self): 121 | return self.request.user.quizzes.all() 122 | 123 | 124 | @login_required 125 | @teacher_required 126 | def question_add(request, pk): 127 | # By filtering the quiz by the url keyword argument `pk` and 128 | # by the owner, which is the logged in user, we are protecting 129 | # this view at the object-level. Meaning only the owner of 130 | # quiz will be able to add questions to it. 131 | quiz = get_object_or_404(Quiz, pk=pk, owner=request.user) 132 | 133 | if request.method == 'POST': 134 | form = QuestionForm(request.POST) 135 | if form.is_valid(): 136 | question = form.save(commit=False) 137 | question.quiz = quiz 138 | question.save() 139 | messages.success(request, 'You may now add answers/options to the question.') 140 | return redirect('teachers:question_change', quiz.pk, question.pk) 141 | else: 142 | form = QuestionForm() 143 | 144 | return render(request, 'classroom/teachers/question_add_form.html', {'quiz': quiz, 'form': form}) 145 | 146 | 147 | @login_required 148 | @teacher_required 149 | def question_change(request, quiz_pk, question_pk): 150 | # Simlar to the `question_add` view, this view is also managing 151 | # the permissions at object-level. By querying both `quiz` and 152 | # `question` we are making sure only the owner of the quiz can 153 | # change its details and also only questions that belongs to this 154 | # specific quiz can be changed via this url (in cases where the 155 | # user might have forged/player with the url params. 156 | quiz = get_object_or_404(Quiz, pk=quiz_pk, owner=request.user) 157 | question = get_object_or_404(Question, pk=question_pk, quiz=quiz) 158 | 159 | AnswerFormSet = inlineformset_factory( 160 | Question, # parent model 161 | Answer, # base model 162 | formset=BaseAnswerInlineFormSet, 163 | fields=('text', 'is_correct'), 164 | min_num=2, 165 | validate_min=True, 166 | max_num=10, 167 | validate_max=True 168 | ) 169 | 170 | if request.method == 'POST': 171 | form = QuestionForm(request.POST, instance=question) 172 | formset = AnswerFormSet(request.POST, instance=question) 173 | if form.is_valid() and formset.is_valid(): 174 | with transaction.atomic(): 175 | form.save() 176 | formset.save() 177 | messages.success(request, 'Question and answers saved with success!') 178 | return redirect('teachers:quiz_change', quiz.pk) 179 | else: 180 | form = QuestionForm(instance=question) 181 | formset = AnswerFormSet(instance=question) 182 | 183 | return render(request, 'classroom/teachers/question_change_form.html', { 184 | 'quiz': quiz, 185 | 'question': question, 186 | 'form': form, 187 | 'formset': formset 188 | }) 189 | 190 | 191 | @method_decorator([login_required, teacher_required], name='dispatch') 192 | class QuestionDeleteView(DeleteView): 193 | model = Question 194 | context_object_name = 'question' 195 | template_name = 'classroom/teachers/question_delete_confirm.html' 196 | pk_url_kwarg = 'question_pk' 197 | 198 | def get_context_data(self, **kwargs): 199 | question = self.get_object() 200 | kwargs['quiz'] = question.quiz 201 | return super().get_context_data(**kwargs) 202 | 203 | def delete(self, request, *args, **kwargs): 204 | question = self.get_object() 205 | messages.success(request, 'The question %s was deleted with success!' % question.text) 206 | return super().delete(request, *args, **kwargs) 207 | 208 | def get_queryset(self): 209 | return Question.objects.filter(quiz__owner=self.request.user) 210 | 211 | def get_success_url(self): 212 | question = self.get_object() 213 | return reverse('teachers:quiz_change', kwargs={'pk': question.quiz_id}) 214 | -------------------------------------------------------------------------------- /django_school/django_school/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/django_school/__init__.py -------------------------------------------------------------------------------- /django_school/django_school/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_school project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | from django.contrib.messages import constants as messages 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'd$pxg6fisc4iwzk&vz^s_d0lkf&k63l5a8f!obktw!jg#4zvp3' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'django.contrib.humanize', 42 | 43 | 'crispy_forms', 44 | 45 | 'classroom', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'django_school.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [ 64 | os.path.join(BASE_DIR, 'templates') 65 | ], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'django_school.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 88 | } 89 | } 90 | 91 | 92 | # Internationalization 93 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 94 | 95 | LANGUAGE_CODE = 'en-us' 96 | 97 | TIME_ZONE = 'UTC' 98 | 99 | USE_I18N = True 100 | 101 | USE_L10N = True 102 | 103 | USE_TZ = True 104 | 105 | 106 | # Static files (CSS, JavaScript, Images) 107 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 108 | 109 | STATIC_URL = '/static/' 110 | 111 | STATICFILES_DIRS = [ 112 | os.path.join(BASE_DIR, 'static'), 113 | ] 114 | 115 | 116 | # Custom Django auth settings 117 | 118 | AUTH_USER_MODEL = 'classroom.User' 119 | 120 | LOGIN_URL = 'login' 121 | 122 | LOGOUT_URL = 'logout' 123 | 124 | LOGIN_REDIRECT_URL = 'home' 125 | 126 | LOGOUT_REDIRECT_URL = 'home' 127 | 128 | 129 | # Messages built-in framework 130 | 131 | MESSAGE_TAGS = { 132 | messages.DEBUG: 'alert-secondary', 133 | messages.INFO: 'alert-info', 134 | messages.SUCCESS: 'alert-success', 135 | messages.WARNING: 'alert-warning', 136 | messages.ERROR: 'alert-danger', 137 | } 138 | 139 | 140 | # Third party apps configuration 141 | 142 | CRISPY_TEMPLATE_PACK = 'bootstrap4' 143 | -------------------------------------------------------------------------------- /django_school/django_school/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from classroom.views import classroom, students, teachers 4 | 5 | urlpatterns = [ 6 | path('', include('classroom.urls')), 7 | path('accounts/', include('django.contrib.auth.urls')), 8 | path('accounts/signup/', classroom.SignUpView.as_view(), name='signup'), 9 | path('accounts/signup/student/', students.StudentSignUpView.as_view(), name='student_signup'), 10 | path('accounts/signup/teacher/', teachers.TeacherSignUpView.as_view(), name='teacher_signup'), 11 | ] 12 | -------------------------------------------------------------------------------- /django_school/django_school/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_school 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/2.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", "django_school.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django_school/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_school.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /django_school/static/css/app.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | font-family: 'Clicker Script', cursive; 3 | } 4 | 5 | .logo a { 6 | color: #212529; 7 | text-decoration: none; 8 | } 9 | 10 | footer { 11 | font-size: .9rem; 12 | } 13 | 14 | footer a { 15 | color: #212529; 16 | } 17 | 18 | .list-group-formset label { 19 | display: none; 20 | } 21 | 22 | .list-group-formset .form-group, 23 | .list-group-formset .invalid-feedback { 24 | margin-bottom: 0; 25 | } 26 | 27 | .list-group-formset .form-control { 28 | padding: .25rem .5rem; 29 | font-size: .875rem; 30 | line-height: 1.5; 31 | border-radius: .2rem; 32 | } 33 | 34 | .btn-student { 35 | color: #fff; 36 | background-color: #91afb6; 37 | border-color: #91afb6; 38 | } 39 | 40 | .btn-student:hover, 41 | .btn-student:active { 42 | color: #fff; 43 | background-color: #608993; 44 | border-color: #608993; 45 | } 46 | 47 | .btn-teacher { 48 | color: #fff; 49 | background-color: #8980a5; 50 | border-color: #8980a5; 51 | } 52 | 53 | .btn-teacher:hover, 54 | .btn-teacher:active { 55 | color: #fff; 56 | background-color: #66598B; 57 | border-color: #66598B; 58 | } 59 | 60 | .has-danger .radio, 61 | .has-danger .checkbox { 62 | color: #dc3545; 63 | } 64 | 65 | .has-danger .invalid-feedback { 66 | display: block; 67 | } -------------------------------------------------------------------------------- /django_school/static/css/students.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #c2d4d8; 3 | } 4 | -------------------------------------------------------------------------------- /django_school/static/css/teachers.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #b0aac2; 3 | } 4 | -------------------------------------------------------------------------------- /django_school/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/img/favicon.ico -------------------------------------------------------------------------------- /django_school/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/img/favicon.png -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Fontelico 5 | 6 | Copyright (C) 2012 by Fontello project 7 | 8 | Author: Crowdsourced, for Fontello project 9 | License: SIL (http://scripts.sil.org/OFL) 10 | Homepage: http://fontello.com 11 | 12 | 13 | ## Entypo 14 | 15 | Copyright (C) 2012 by Daniel Bruce 16 | 17 | Author: Daniel Bruce 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://www.entypo.com 20 | 21 | 22 | -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | 278 | 279 | 291 | 292 | 293 |
294 |

295 | fontello 296 | font demo 297 |

298 | 301 |
302 |
303 |
304 |
icon-emo-happy0xe801
305 |
icon-graduation-cap-10xe803
306 |
icon-feather0xe808
307 |
308 |
309 | 310 | 311 | -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/vendor/fontello-2f186091/font/fontello.eot -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2018 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/vendor/fontello-2f186091/font/fontello.ttf -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/vendor/fontello-2f186091/font/fontello.woff -------------------------------------------------------------------------------- /django_school/static/vendor/fontello-2f186091/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibtc/django-multiple-user-types-example/c1aaa062d91bbe70f408a07026570bf2b2349d6a/django_school/static/vendor/fontello-2f186091/font/fontello.woff2 -------------------------------------------------------------------------------- /django_school/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Django School 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

Django School

18 |

404

19 |

Page not found ¯\_(ツ)_/¯

20 |

Go to the home page →

21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /django_school/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Django School 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

Django School

18 |

500

19 |

Something went wrong 😭

20 |

Mind to send me a tweet about this issue?

21 |

Go to the home page →

22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /django_school/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}Django School{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | {% if user.is_authenticated and user.is_teacher %} 13 | 14 | {% else %} 15 | 16 | {% endif %} 17 | 18 | 19 | Fork me on GitHub 20 |
21 |
22 |
23 |
24 | 38 |
39 | {% if user.is_authenticated %} 40 |

Logged in as {{ user.username }}. Log out.

41 | {% else %} 42 | Log in 43 | Sign up 44 | {% endif %} 45 |
46 |
47 |
48 |
49 | {% for message in messages %} 50 | 56 | {% endfor %} 57 | {% block content %} 58 | {% endblock %} 59 |
60 |
61 | 68 |
69 |
70 |
71 | 72 | 73 | 74 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /django_school/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 | {% if form.non_field_errors %} 7 | 15 | {% endif %} 16 |
17 |
18 |

Log in

19 |
20 | {% csrf_token %} 21 | 22 | {{ form.username|as_crispy_field }} 23 | {{ form.password|as_crispy_field }} 24 | 25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /django_school/templates/registration/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Sign up for a free account

5 |

Select below the type of account you want to create

6 | I'm a student 7 | I'm a teacher 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /django_school/templates/registration/signup_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Sign up as a {{ user_type }}

9 |
10 | {% csrf_token %} 11 | 12 | {{ form|crispy }} 13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.0.1 2 | django-crispy-forms==1.7.0 3 | pytz==2017.3 4 | --------------------------------------------------------------------------------