├── .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 | [](https://python.org)
4 | [](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 | 
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 |