21 |
22 |
28 |
49 |
50 |
51 |
52 | {%if form.errors %}
53 |
Your username and password do not match. Please try again
54 | {% else %}
55 |
Login
56 | {% endif %}
57 |
58 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Buza
2 | ====
3 |
4 | This is the code for Buza mobi site
5 |
6 | Getting started
7 | ---------------
8 |
9 | With Vagrant
10 | ^^^^^^^^^^^^
11 |
12 | The easiest way to get a development instance of Buza up and running is to use `Vagrant`_.
13 |
14 | .. _`Vagrant`: https://www.vagrantup.com/
15 |
16 | After installing Vagrant, run the following to provision a Buza virtual machine::
17 |
18 | vagrant up
19 |
20 | (This will take a while the first time you run it, but will be faster on subsequent runs.)
21 |
22 | To run the Django development server, you can execute the following command::
23 |
24 | vagrant ssh -c 'cd /vagrant && pipenv run django-admin runserver 0.0.0.0:8000'
25 |
26 | You can also log into the virtual machine and activate the project,
27 | in order to run other Django development commands. For example::
28 |
29 | $ vagrant ssh
30 | vagrant@ubuntu-bionic:~$ cd /vagrant
31 | vagrant@ubuntu-bionic:/vagrant$ pipenv shell
32 | Loading .env environment variables...
33 | Launching subshell in virtual environment…
34 | (vagrant-gKDsaKU3) vagrant@ubuntu-bionic:/vagrant$ django-admin check
35 | System check identified no issues (0 silenced).
36 | (vagrant-gKDsaKU3) vagrant@ubuntu-bionic:/vagrant$
37 |
38 | When you're finished working, you can stop the Vagrant virtual machine by running ``vagrant halt``.
39 | Running ``vagrant up`` again will restart it.
40 |
41 | To destroy the virtual machine completely, run ``vagrant destroy``.
42 |
43 | You can also create the docker image for the project to run it::
44 |
45 | $ docker-compose up
46 |
47 |
48 | With Pipenv
49 | ^^^^^^^^^^^
50 |
51 | To set up a conventional Python development environment,
52 | make sure you have the following tools installed:
53 |
54 | * Pipenv_
55 | * Yarn_
56 |
57 | .. _Pipenv: https://docs.pipenv.org/install/#installing-pipenv
58 | .. _Yarn: https://yarnpkg.com/lang/en/docs/install/
59 |
60 | Django requires the ``DJANGO_SETTINGS_MODULE`` environment variable to run.
61 | To set this and get started, copy the ``env.example`` file to ``.env``::
62 |
63 | $ cp .env.example .env
64 |
65 | (Pipenv will `automatically load`_ the environment variables defined in this ``.env`` file.)
66 |
67 | .. _`automatically load`: https://docs.pipenv.org/advanced/#automatic-loading-of-env
68 |
69 | Fetch the Yarn dependencies::
70 |
71 | $ yarn
72 |
73 | Install the Pipenv dependencies, and activate the environment::
74 |
75 | $ pipenv install --dev
76 | $ pipenv shell
77 |
78 | Initialise the database, and run the Django development server::
79 |
80 | $ django-admin migrate
81 | $ django-admin createsuperuser
82 | $ django-admin runserver
83 |
84 |
85 | Running checks and tests
86 | ------------------------
87 |
88 | To run all the static checks and tests, invoke Tox::
89 |
90 | $ tox
91 |
92 | To run the checks and tests individually, see the "commands" section of ``tox.ini``.
93 |
94 |
95 | Git pre-commit hook
96 | -------------------
97 |
98 | To run our main quick checks before each commit, add the following to ``.git/hooks/pre-commit``::
99 |
100 | #!/bin/sh -e
101 |
102 | mypy -i src tests
103 | flake8
104 | isort --check-only
105 |
106 |
--------------------------------------------------------------------------------
/src/buza/static/buza/css/subjects.css:
--------------------------------------------------------------------------------
1 | /* Styles for Subjects. */
2 |
3 | /* Block: Subjects */
4 |
5 | subject-nav_bar{
6 | background-color: silver;
7 | font-weight: bold;
8 | border-radius: 10%;
9 | }
10 |
11 | .subject_heading {
12 | font-weight: bold;
13 | text-align: left;
14 | color:#007bff;
15 | }
16 |
17 | .subject__deck {
18 | padding: 1em 2em;
19 | text-align: center;
20 | display: inline-block;
21 | width: 20em;
22 | }
23 | /* The Subject Following Card*/
24 | .subject__card_following {
25 | padding: 0.5em 1em;
26 | background: white;
27 | }
28 |
29 | .subject__card_following:hover {
30 | box-shadow: 12px 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
31 | }
32 |
33 | .subject__card_focus{
34 | background: var(--buza-light-green);
35 | padding-bottom: 1em;
36 | padding-top: 1em;
37 | box-shadow: 0 16px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
38 | }
39 |
40 | .subject__title_following {
41 | text-align: left;
42 | color: var(--buza-light-green);
43 | font-weight:bold;
44 | }
45 |
46 | .subject__title_focus{
47 | color: white;
48 | background: var(--buza-light-green);
49 | font-weight:bold;
50 | text-align: left;
51 | }
52 |
53 | .subject__title_following:hover {
54 | font-weight: bold;
55 | text-decoration: None;
56 | }
57 | /* The Subject Follow Card*/
58 | .subject__card_follow {
59 | padding: 0.5em 1em;
60 |
61 | }
62 | .subject__card_follow:hover {
63 | box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
64 | }
65 | .subject__title_follow {
66 | text-align: left;
67 | font-weight:bold;
68 | color: var(--buza-grey);
69 | }
70 | .subject__title_follow:hover {
71 | text-decoration: None;
72 | }
73 |
74 | /* The Follow/Following buttons*/
75 | .subject__buttons {
76 | border: 1px solid var(--buza-green);
77 | padding: 0.2em 0.2em;
78 | border-radius: 50%;
79 | font-weight: bold;
80 | text-align: right;
81 | }
82 |
83 | .subject__buttons-follow{
84 | background-color: white;
85 | color: var(--buza-grey);
86 | text-align: right;
87 |
88 | }
89 |
90 | .subject__buttons-following{
91 | border: 1px solid var(--buza-light-green);
92 | background: var(--buza-light-green);
93 | color: white;
94 | }
95 |
96 | .subject__subjects-nav{
97 | padding: 1% 4%;
98 | text-align: center;
99 | text-decoration: none;
100 | display: inline-block;
101 | font-size: 16px;
102 | border: 2px solid var(--buza-green); /* Green */
103 | }
104 |
105 | .subject__subjects-nav-view{
106 | background: var(--buza-green); /* Green */
107 | }
108 |
109 | .subject__subjects-nav-view, .subject__subjects-nav-view:hover{
110 | color: white;
111 | }
112 |
113 | .subject__subjects-nav-hidden{
114 | background-color:white;
115 | }
116 |
117 | .subject__subjects-nav-hidden, .subject__subjects-nav-hidden:hover{
118 | color: #32CD32; /* Green */
119 | }
120 | /* Subject Detail flext box */
121 | .subject_detail-container {
122 | display: flex;
123 | align-items: stretch;
124 | }
125 | .btn__new_question{
126 | padding-left: 15em;
127 | }
128 |
129 | .subject__question_list{
130 | flex-grow: 3;
131 | }
132 |
--------------------------------------------------------------------------------
/src/buza/settings_base.py:
--------------------------------------------------------------------------------
1 | """
2 | Base Django settings for a buza-website instance.
3 | """
4 | import os
5 | from pathlib import Path
6 |
7 | import environ
8 | from django.urls import reverse_lazy
9 |
10 |
11 | env = environ.Env()
12 |
13 | # Assume we're running from a Git checkout directory.
14 | checkout_dir: Path = Path(__file__).parent.parent.parent
15 | if checkout_dir.joinpath('.git').exists():
16 | assert checkout_dir.joinpath('.git').exists(), checkout_dir
17 |
18 | ROOT_URLCONF = 'buza.urls'
19 |
20 | INSTALLED_APPS = [
21 | # Buza
22 | 'buza',
23 |
24 | # Third-party apps
25 | 'crispy_forms',
26 | 'taggit',
27 |
28 | # Django apps
29 | 'django.contrib.admin',
30 | 'django.contrib.auth',
31 | 'django.contrib.contenttypes',
32 | 'django.contrib.sessions',
33 | 'django.contrib.messages',
34 | 'django.contrib.staticfiles',
35 | 'social_django',
36 | 'django.contrib.humanize',
37 | ]
38 |
39 | MIDDLEWARE = [
40 | 'social_django.middleware.SocialAuthExceptionMiddleware',
41 | 'django.middleware.security.SecurityMiddleware',
42 | 'django.contrib.sessions.middleware.SessionMiddleware',
43 | 'django.middleware.common.CommonMiddleware',
44 | 'django.middleware.csrf.CsrfViewMiddleware',
45 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
46 | 'django.contrib.messages.middleware.MessageMiddleware',
47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
48 | ]
49 |
50 | TEMPLATES = [{
51 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
52 | 'APP_DIRS': True,
53 | 'OPTIONS': {
54 | 'context_processors': [
55 | 'django.contrib.auth.context_processors.auth',
56 | 'django.contrib.messages.context_processors.messages',
57 | ],
58 | },
59 | }]
60 |
61 | AUTHENTICATION_BACKENDS = [
62 | 'social_core.backends.facebook.FacebookOAuth2',
63 | 'social_core.backends.google.GoogleOAuth2',
64 | 'social_core.backends.google.GoogleOAuth',
65 | 'social_core.backends.google.GooglePlusAuth',
66 | 'django.contrib.auth.backends.ModelBackend',
67 | ]
68 | # Internationalization
69 | USE_I18N = True
70 | USE_L10N = True
71 | USE_TZ = True
72 |
73 |
74 | # django.contrib.auth
75 | AUTH_USER_MODEL = 'buza.User'
76 | SOCIAL_AUTH_LOGIN_REDIRECT_URL = 'home'
77 | LOGIN_URL = reverse_lazy('login')
78 | LOGOUT_URL = reverse_lazy('logout')
79 | LOGIN_REDIRECT_URL = reverse_lazy('home')
80 |
81 | # django-crispy-forms
82 | CRISPY_TEMPLATE_PACK = 'bootstrap4'
83 |
84 |
85 | STATICFILES_DIRS = [
86 | # Path to Yarn's packages
87 | str(checkout_dir.joinpath('node_modules')),
88 | ]
89 |
90 | # Include the local host by default for development.
91 | ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
92 |
93 | BASE_DIR = os.environ.get('BASE_DIR') or str(checkout_dir.joinpath('buza-instance'))
94 |
95 | SECRET_KEY = 'secret-key'
96 |
97 | MEDIA_ROOT = os.environ.get("MEDIA_ROOT", "media")
98 |
99 | STATIC_ROOT = os.environ.get("STATIC_ROOT", "static")
100 | STATIC_URL = env('DJANGO_STATIC_URL', default='/static/')
101 |
102 | # Internationalization
103 | LANGUAGE_CODE = env('DJANGO_LANGUAGE_CODE', default='en-ZA')
104 | TIME_ZONE = env('DJANGO_TIME_ZONE', default='Africa/Johannesburg')
105 |
--------------------------------------------------------------------------------
/src/buza/models.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from typing import Union
3 |
4 | from django.contrib.auth.models import AbstractUser, AnonymousUser
5 | from django.core.validators import MaxValueValidator, MinValueValidator
6 | from django.db import models
7 | from django.utils.translation import ugettext_lazy as _
8 |
9 |
10 | # Shortcuts:
11 | _CharField = partial(models.CharField, max_length=1024)
12 |
13 |
14 | class TimestampedModel(models.Model):
15 | """
16 | Base class with `created` and `modified` fields.
17 | """
18 | created = models.DateTimeField(auto_now_add=True, db_index=True)
19 | modified = models.DateTimeField(auto_now=True, db_index=True)
20 |
21 | class Meta:
22 | abstract = True
23 |
24 |
25 | class Subject(models.Model):
26 | title = _CharField()
27 | short_title = models.TextField()
28 | description = models.TextField()
29 |
30 | def __str__(self) -> str:
31 | return str(self.title)
32 |
33 |
34 | class User(AbstractUser):
35 |
36 | # Authentication fields
37 | phone = models.CharField(
38 | _('phone number'),
39 | blank=True,
40 | max_length=11,
41 | )
42 |
43 | # School fields
44 | school = models.CharField(_('school name'), blank=True, null=True, max_length=100)
45 | school_address = models.CharField(
46 | _('school address'),
47 | blank=True, null=True,
48 | max_length=300,
49 | )
50 | grade = models.IntegerField(blank=True, null=True, default=7)
51 |
52 | # Personal fields
53 | photo = models.ImageField(_('profile photo'),
54 | upload_to='users/%Y/%m/%d',
55 | blank=True)
56 | bio = models.CharField(blank=True, null=True, max_length=250)
57 |
58 | subjects = models.ManyToManyField(Subject)
59 |
60 | def __str__(self) -> str:
61 | return str(self.username)
62 |
63 |
64 | #: Helper type for Django request users: either anonymous or signed-in.
65 | RequestUser = Union[AnonymousUser, User]
66 |
67 |
68 | class Question(TimestampedModel, models.Model):
69 |
70 | author = models.ForeignKey(User, on_delete=models.PROTECT)
71 |
72 | title = _CharField(
73 | verbose_name='Question Summary',
74 | help_text='Write a short sentence summarising your question',
75 | )
76 | body = models.TextField(
77 | blank=True,
78 | verbose_name='Question Description',
79 | help_text='Give a detailed description of your question')
80 | subject = models.ForeignKey(Subject, on_delete=models.PROTECT)
81 | grade = models.IntegerField(
82 | validators=[MinValueValidator(7), MaxValueValidator(12)],
83 | help_text="Which grade it this question most relevant for? "
84 | "By default this will be the grade that you are in.",
85 | )
86 |
87 | def __str__(self) -> str:
88 | return f'By {self.author}: {self.title}'
89 |
90 |
91 | class Answer(TimestampedModel, models.Model):
92 |
93 | author = models.ForeignKey(User, on_delete=models.PROTECT)
94 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
95 |
96 | body = models.TextField()
97 |
98 | def __str__(self) -> str:
99 | question: Question = self.question
100 | return f'By {self.author} to question {question.pk}: {question.title}'
101 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure
5 | # configures the configuration version (we support older styles for
6 | # backwards compatibility). Please don't change it unless you know what
7 | # you're doing.
8 | Vagrant.configure("2") do |config|
9 | # The most common configuration options are documented and commented below.
10 | # For a complete reference, please see the online documentation at
11 | # https://docs.vagrantup.com.
12 |
13 | # Every Vagrant development environment requires a box. You can search for
14 | # boxes at https://vagrantcloud.com/search.
15 | config.vm.box = "ubuntu/bionic64"
16 |
17 | # Disable automatic box update checking. If you disable this, then
18 | # boxes will only be checked for updates when the user runs
19 | # `vagrant box outdated`. This is not recommended.
20 | # config.vm.box_check_update = false
21 |
22 | # Create a forwarded port mapping which allows access to a specific port
23 | # within the machine from a port on the host machine. In the example below,
24 | # accessing "localhost:8080" will access port 80 on the guest machine.
25 | # NOTE: This will enable public access to the opened port
26 | # config.vm.network "forwarded_port", guest: 80, host: 8080
27 |
28 | # Create a forwarded port mapping which allows access to a specific port
29 | # within the machine from a port on the host machine and only allow access
30 | # via 127.0.0.1 to disable public access
31 | config.vm.network "forwarded_port", guest: 8000, host: 8000, host_ip: "127.0.0.1"
32 |
33 | # Create a private network, which allows host-only access to the machine
34 | # using a specific IP.
35 | # config.vm.network "private_network", ip: "192.168.33.10"
36 |
37 | # Create a public network, which generally matched to bridged network.
38 | # Bridged networks make the machine appear as another physical device on
39 | # your network.
40 | # config.vm.network "public_network"
41 |
42 | # Share an additional folder to the guest VM. The first argument is
43 | # the path on the host to the actual folder. The second argument is
44 | # the path on the guest to mount the folder. And the optional third
45 | # argument is a set of non-required options.
46 | # config.vm.synced_folder "../data", "/vagrant_data"
47 |
48 | # Provider-specific configuration so you can fine-tune various
49 | # backing providers for Vagrant. These expose provider-specific options.
50 | # Example for VirtualBox:
51 | #
52 | # config.vm.provider "virtualbox" do |vb|
53 | # # Display the VirtualBox GUI when booting the machine
54 | # vb.gui = true
55 | #
56 | # # Customize the amount of memory on the VM:
57 | # vb.memory = "1024"
58 | # end
59 | #
60 | # View the documentation for the provider you are using for more
61 | # information on available options.
62 |
63 | # Enable provisioning with a shell script. Additional provisioners such as
64 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
65 | # documentation for more information about their specific syntax and use.
66 |
67 | # System dependencies.
68 | config.vm.provision "shell", privileged: true, inline: <<-SHELL
69 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
70 | echo "deb https://dl.yarnpkg.com/debian/ stable main" >/etc/apt/sources.list.d/yarn.list
71 |
72 | apt-get update
73 | apt-get install -y python3-pip yarn
74 | SHELL
75 |
76 | # User dependencies.
77 | config.vm.provision "shell", privileged: false, inline: <<-SHELL
78 | pip3 install --user pipenv
79 | SHELL
80 |
81 | # Project setup
82 | config.vm.provision "shell", privileged: false, inline: <<-SHELL
83 | cd /vagrant
84 | yarn
85 | cp -p .env.example .env
86 | pipenv install --dev
87 | pipenv run django-admin migrate
88 | pipenv run django-admin loaddata examples/example-data.json
89 | SHELL
90 |
91 | # Show a usage message:
92 | config.vm.provision "shell", privileged: false, run: "always", inline: <<-SHELL
93 | echo "Buza environment ready for use."
94 | echo "To run the Django development server:"
95 | echo "vagrant ssh -c 'cd /vagrant && pipenv run django-admin runserver 0.0.0.0:8000'"
96 | SHELL
97 |
98 | end
99 |
--------------------------------------------------------------------------------
/src/buza/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.7 on 2018-07-30 15:13
2 |
3 | from django.conf import settings
4 | import django.contrib.auth.models
5 | import django.contrib.auth.validators
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 | import django.utils.timezone
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 | ('phone', models.CharField(blank=True, max_length=11, verbose_name='phone number')),
35 | ('school', models.CharField(blank=True, max_length=100, null=True, verbose_name='school name')),
36 | ('school_address', models.CharField(blank=True, max_length=300, null=True, verbose_name='school address')),
37 | ('grade', models.IntegerField(blank=True, default=7, null=True)),
38 | ('photo', models.ImageField(blank=True, upload_to='users/%Y/%m/%d', verbose_name='profile photo')),
39 | ('bio', models.CharField(blank=True, max_length=250, null=True)),
40 | ('groups', 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')),
41 | ],
42 | options={
43 | 'verbose_name': 'user',
44 | 'verbose_name_plural': 'users',
45 | 'abstract': False,
46 | },
47 | managers=[
48 | ('objects', django.contrib.auth.models.UserManager()),
49 | ],
50 | ),
51 | migrations.CreateModel(
52 | name='Answer',
53 | fields=[
54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
55 | ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
56 | ('modified', models.DateTimeField(auto_now=True, db_index=True)),
57 | ('body', models.TextField()),
58 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
59 | ],
60 | options={
61 | 'abstract': False,
62 | },
63 | ),
64 | migrations.CreateModel(
65 | name='Question',
66 | fields=[
67 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
68 | ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
69 | ('modified', models.DateTimeField(auto_now=True, db_index=True)),
70 | ('title', models.CharField(max_length=1024)),
71 | ('body', models.TextField(blank=True)),
72 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
73 | ],
74 | options={
75 | 'abstract': False,
76 | },
77 | ),
78 | migrations.CreateModel(
79 | name='Subject',
80 | fields=[
81 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
82 | ('title', models.CharField(max_length=1024)),
83 | ('description', models.TextField()),
84 | ],
85 | ),
86 | migrations.AddField(
87 | model_name='question',
88 | name='subject',
89 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='buza.Subject'),
90 | ),
91 | migrations.AddField(
92 | model_name='answer',
93 | name='question',
94 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='buza.Question'),
95 | ),
96 | migrations.AddField(
97 | model_name='user',
98 | name='subjects',
99 | field=models.ManyToManyField(to='buza.Subject'),
100 | ),
101 | migrations.AddField(
102 | model_name='user',
103 | name='user_permissions',
104 | 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'),
105 | ),
106 | ]
107 |
--------------------------------------------------------------------------------
/src/buza/templates/accounts/privacy_policy.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {%block title%} Privacy Policy - Buza Answers {%endblock%}
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
Last updated April 2019
10 |
At Buza Answers, accessible from
11 | buza.co.za, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by Buza Answers and how we use it.
12 |
13 |
If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sewagodimo.matlapeng@gmail.com
14 |
15 |
Log Files:
16 | Buza Answers follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.
17 |
18 |
Cookies and Web Beacons:
19 | Like any other website, Buza Answers uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.
20 |
21 |
22 |
Google DoubleClick DART Cookie:
23 | Google is one of a third-party vendor on our site. It also uses cookies,
24 | known as DART cookies, to serve ads to our site visitors based upon their visit to
25 | www.website.com and other sites on the internet. However, visitors may choose to decline
26 | the use of DART cookies by visiting the Google ad and content network Privacy Policy at the following URL – https://policies.google.com/technologies/ads
27 |
28 |
29 |
Our Advertising Partnerse:
30 | Some of advertisers on our site may use cookies and web beacons. Our advertising partners are listed below. Each of our advertising partners has their own Privacy Policy for their policies on user data. For easier access, we hyperlinked to their Privacy Policies below.
31 |
32 |
38 |
39 |
40 |
Privacy Policies:
41 | You may consult this list to find the Privacy Policy for each of the advertising
42 | partners of Buza Answers. Our Privacy Policy was created with the help of the
43 | GDPR Privacy Policy Generator and the Privacy Policy Generator from TermsFeed plus the Terms and Conditions Template.
44 |
45 |
Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on Buza Answers, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.
46 |
47 |
Note that Buza Answers has no access to or control over these cookies that are used by third-party advertisers.
48 |
49 |
50 |
Third Pary Privacy Policies:
51 | Buza Answers's Privacy Policy does not apply to other advertisers or websites.
52 | Thus, we are advising you to consult the respective Privacy Policies of these third-party ad
53 | servers for more detailed information. It may include their practices and instructions about how
54 | to opt-out of certain options..
55 |
56 |
You can choose to disable cookies through your individual browser options.
57 | To know more detailed information about cookie management with specific web browsers,
58 | it can be found at the browsers' respective websites. What Are Cookies?
59 |
60 |
Your Content:
61 | We collect and store the information and content that you post to the Buza Answers Platform,
62 | including your questions, answers, photos, and comments. Unless you have posted certain content anonymously,
63 | Your Content, date and time stamps, and all associated comments are publicly viewable on the Buza Answers Platform,
64 | along with your name. This also may be indexed by search engines and be republished elsewhere on the Internet
65 | in accordance with our Terms of Service.
66 |
67 |
Children's Information:
68 | Another part of our priority is adding protection for children while using the internet.
69 | We encourage parents and guardians to observe, participate in, and/or monitor and guide their online
70 | activity.
71 |
72 |
Buza Answers does not knowingly collect any Personal Identifiable Information from
73 | children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.
74 |
75 |
Online Privacy Policy Only:
76 | This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in Buza Answers. This policy is not applicable to any information collected offline or via channels other than this website.
77 |
78 |
Consent:
79 | By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.
80 |
81 |
back to our home
82 |
83 |
84 | {% endblock%}
--------------------------------------------------------------------------------
/src/buza/templates/accounts/terms_of_service.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {%block title%} Terms of Service - Buza Answers {%endblock%}
4 |
5 | {%block content %}
6 |
7 |
Welcome to Buza Answers
8 |
9 |
Terms of Service for Buza Answers
10 |
These terms and conditions outline the rules and regulations for the use of Buza Answers's Website.
11 |
Buza Answers is located at:
12 |
Cape Town
Western Cape , South Africa
13 |
14 |
By accessing this website we assume you accept these terms and conditions in full. Do not continue to use Buza's website
15 | if you do not accept all of the terms and conditions stated on this page.
16 |
The following terminology applies to these Terms and Conditions, Privacy Statement and Disclaimer Notice
17 | and any or all Agreements: “Client”, “You” and “Your” refers to you, the person accessing this website
18 | and accepting the Company’s terms and conditions. “The Company”, “Ourselves”, “We”, “Our” and “Us”, refers
19 | to our Company. “Party”, “Parties”, or “Us”, refers to both the Client and ourselves, or either the Client
20 | or ourselves. All terms refer to the offer, acceptance and consideration of payment necessary to undertake
21 | the process of our assistance to the Client in the most appropriate manner, whether by formal meetings
22 | of a fixed duration, or any other means, for the express purpose of meeting the Client’s needs in respect
23 | of provision of the Company’s stated services/products, in accordance with and subject to, prevailing law
24 | of South Africa. Any use of the above terminology or other words in the singular, plural,
25 | capitalisation and/or he/she or they, are taken as interchangeable and therefore as referring to same.
Cookies
26 |
We employ the use of cookies. By using Buza's website you consent to the use of cookies
27 | in accordance with Buza’s privacy policy.
Most of the modern day interactive web sites
28 | use cookies to enable us to retrieve user details for each visit. Cookies are used in some areas of our site
29 | to enable the functionality of this area and ease of use for those people visiting. Some of our
30 | affiliate / advertising partners may also use cookies.
License
31 |
Unless otherwise stated, Buza and/or it’s licensors own the intellectual property rights for
32 | all material on Buza. All intellectual property rights are reserved. You may view and/or print
33 | pages from http://buza.co.za/home/ for your own personal use subject to restrictions set in these terms and conditions.
34 |
You must not:
35 |
36 | - Republish material from http://buza.co.za/home/
37 | - Sell, rent or sub-license material from http://buza.co.za/home/
38 | - Reproduce, duplicate or copy material from http://buza.co.za/home/
39 |
40 |
Redistribute content from Buza (unless content is specifically made for redistribution).
41 |
Hyperlinking to our Content
42 |
43 | - The following organizations may link to our Web site without prior written approval:
44 |
45 | - Government agencies;
46 | - Search engines;
47 | - News organizations;
48 | - Online directory distributors when they list us in the directory may link to our Web site in the same
49 | manner as they hyperlink to the Web sites of other listed businesses; and
50 | - Systemwide Accredited Businesses except soliciting non-profit organizations, charity shopping malls,
51 | and charity fundraising groups which may not hyperlink to our Web site.
52 |
53 |
54 |
55 |
56 | - These organizations may link to our home page, to publications or to other Web site information so long
57 | as the link: (a) is not in any way misleading; (b) does not falsely imply sponsorship, endorsement or
58 | approval of the linking party and its products or services; and (c) fits within the context of the linking
59 | party's site.
60 |
61 | - We may consider and approve in our sole discretion other link requests from the following types of organizations:
62 |
63 | - commonly-known consumer and/or business information sources such as Chambers of Commerce, American
64 | Automobile Association, AARP and Consumers Union;
65 | - dot.com community sites;
66 | - associations or other groups representing charities, including charity giving sites,
67 | - online directory distributors;
68 | - internet portals;
69 | - accounting, law and consulting firms whose primary clients are businesses; and
70 | - educational institutions and trade associations.
71 |
72 |
73 |
74 |
We will approve link requests from these organizations if we determine that: (a) the link would not reflect
75 | unfavorably on us or our accredited businesses (for example, trade associations or other organizations
76 | representing inherently suspect types of business, such as work-at-home opportunities, shall not be allowed
77 | to link); (b)the organization does not have an unsatisfactory record with us; (c) the benefit to us from
78 | the visibility associated with the hyperlink outweighs the absence of =$companyName?>; and (d) where the
79 | link is in the context of general resource information or is otherwise consistent with editorial content
80 | in a newsletter or similar product furthering the mission of the organization.
81 |
82 |
These organizations may link to our home page, to publications or to other Web site information so long as
83 | the link: (a) is not in any way misleading; (b) does not falsely imply sponsorship, endorsement or approval
84 | of the linking party and it products or services; and (c) fits within the context of the linking party's
85 | site.
86 |
87 |
If you are among the organizations listed in paragraph 2 above and are interested in linking to our website,
88 | you must notify us by sending an e-mail to buza4education@gmail.com.
89 | Please include your name, your organization name, contact information (such as a phone number and/or e-mail
90 | address) as well as the URL of your site, a list of any URLs from which you intend to link to our Web site,
91 | and a list of the URL(s) on our site to which you would like to link. Allow 2-3 weeks for a response.
92 |
93 |
Approved organizations may hyperlink to our Web site as follows:
94 |
95 |
96 | - By use of our corporate name; or
97 | - By use of the uniform resource locator (Web address) being linked to; or
98 | - By use of any other description of our Web site or material being linked to that makes sense within the
99 | context and format of content on the linking party's site.
100 |
101 |
No use of Buza’s logo or other artwork will be allowed for linking absent a trademark license
102 | agreement.
103 |
Iframes
104 |
Without prior approval and express written permission, you may not create frames around our Web pages or
105 | use other techniques that alter in any way the visual presentation or appearance of our Web site.
106 |
Reservation of Rights
107 |
We reserve the right at any time and in its sole discretion to request that you remove all links or any particular
108 | link to our Web site. You agree to immediately remove all links to our Web site upon such request. We also
109 | reserve the right to amend these terms and conditions and its linking policy at any time. By continuing
110 | to link to our Web site, you agree to be bound to and abide by these linking terms and conditions.
111 |
Removal of links from our website
112 |
If you find any link on our Web site or any linked web site objectionable for any reason, you may contact
113 | us about this. We will consider requests to remove links but will have no obligation to do so or to respond
114 | directly to you.
115 |
Whilst we endeavour to ensure that the information on this website is correct, we do not warrant its completeness
116 | or accuracy; nor do we commit to ensuring that the website remains available or that the material on the
117 | website is kept up to date.
118 |
Content Liability
119 |
We shall have no responsibility or liability for any content appearing on your Web site. You agree to indemnify
120 | and defend us against all claims arising out of or based upon your Website. No link(s) may appear on any
121 | page on your Web site or within any context containing content or materials that may be interpreted as
122 | libelous, obscene or criminal, or which infringes, otherwise violates, or advocates the infringement or
123 | other violation of, any third party rights.
124 |
Disclaimer
125 |
To the maximum extent permitted by applicable law, we exclude all representations, warranties and conditions relating to our website and the use of this website (including, without limitation, any warranties implied by law in respect of satisfactory quality, fitness for purpose and/or the use of reasonable care and skill). Nothing in this disclaimer will:
126 |
127 | - limit or exclude our or your liability for death or personal injury resulting from negligence;
128 | - limit or exclude our or your liability for fraud or fraudulent misrepresentation;
129 | - limit any of our or your liabilities in any way that is not permitted under applicable law; or
130 | - exclude any of our or your liabilities that may not be excluded under applicable law.
131 |
132 |
The limitations and exclusions of liability set out in this Section and elsewhere in this disclaimer: (a)
133 | are subject to the preceding paragraph; and (b) govern all liabilities arising under the disclaimer or
134 | in relation to the subject matter of this disclaimer, including liabilities arising in contract, in tort
135 | (including negligence) and for breach of statutory duty.
136 |
To the extent that the website and the information and services on the website are provided free of charge,
137 | we will not be liable for any loss or damage of any nature.
138 |
139 |
back to our home
140 |
141 |
142 | {% endblock%}
--------------------------------------------------------------------------------
/src/buza/views.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional, Type
2 |
3 | from crispy_forms import layout
4 | from crispy_forms.helper import FormHelper
5 | from django import forms
6 | from django.contrib.auth.forms import UserCreationForm
7 | from django.contrib.auth.mixins import LoginRequiredMixin
8 | from django.core.exceptions import PermissionDenied
9 | from django.db.models import BooleanField, Exists, OuterRef, QuerySet, Value
10 | from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
11 | from django.shortcuts import get_object_or_404, render
12 | from django.urls import reverse
13 | from django.views import generic
14 | from django.views.generic.edit import FormMixin, ModelFormMixin
15 |
16 | from buza import models
17 |
18 |
19 | class CrispyFormMixin(FormMixin):
20 | """
21 | Helper class for crispy-forms rendering.
22 | """
23 |
24 | def get_form(
25 | self,
26 | form_class: Optional[Type[forms.BaseForm]] = None,
27 | ) -> forms.BaseForm:
28 | """
29 | Add this view's crispy-forms ``helper`` to the form instance.
30 | """
31 | form = super().get_form(form_class)
32 | form.helper = self.get_form_helper(form)
33 | return form
34 |
35 | def get_form_helper(self, form: forms.BaseForm) -> FormHelper:
36 | """
37 | Return the `FormHelper` to use for this view.
38 |
39 | Extend this to customise
40 | """
41 | return FormHelper(form)
42 |
43 |
44 | # TODO: Migrate to class based views
45 |
46 |
47 | class BuzaUserCreationForm(UserCreationForm):
48 | """
49 | Like Django's `UserCreationForm`, but point at Buza's `User` model.
50 | """
51 |
52 | class Meta(UserCreationForm.Meta):
53 | model = models.User
54 |
55 |
56 | def register(request: HttpRequest) -> HttpResponse:
57 | """
58 | Register a user account.
59 | """
60 | if request.method == 'POST':
61 | user_form = BuzaUserCreationForm(request.POST)
62 |
63 | if user_form.is_valid():
64 | # Save the new user.
65 | new_user = user_form.save()
66 | return render(
67 | request,
68 | 'accounts/register_done.html',
69 | {'new_user': new_user},
70 | )
71 | else:
72 | # User did not fill in form correctly
73 | user_form = BuzaUserCreationForm()
74 | return render(
75 | request,
76 | 'accounts/register.html',
77 | {'user_form': user_form},
78 | )
79 |
80 |
81 | class HomePageView(generic.RedirectView):
82 | permanent = False
83 | query_string = True
84 | pattern_name = 'login'
85 |
86 | def get_redirect_url(self, *args, **kwargs):
87 | user = self.request.user
88 | if user.is_authenticated:
89 | return reverse('user-detail', kwargs=dict(pk=user.pk))
90 | else:
91 | return '/auth/login/'
92 |
93 |
94 | class PrivacyPolicyView(generic.TemplateView):
95 | template_name = "accounts/privacy_policy.html"
96 |
97 |
98 | class TermsOfService(generic.TemplateView):
99 | template_name = "accounts/terms_of_service.html"
100 |
101 |
102 | class UserUpdate(CrispyFormMixin, LoginRequiredMixin, generic.UpdateView):
103 | model = models.User
104 | fields = [
105 | 'email',
106 | 'phone',
107 | 'photo',
108 | 'first_name',
109 | 'last_name',
110 | 'school',
111 | 'school_address',
112 | 'grade',
113 | 'bio',
114 | ]
115 |
116 | # TODO: Merge with and migrate to existing buza/user_form.html ?
117 | template_name = 'accounts/edit.html'
118 |
119 | def get_object(self, queryset: QuerySet = None) -> models.User:
120 | """
121 | Only allow users to update their own profile.
122 | """
123 | user: models.User = super().get_object(queryset)
124 | if not user == self.request.user:
125 | raise PermissionDenied('You can only update your own profile.')
126 | return user
127 |
128 | def get_form_helper(self, form: forms.ModelForm) -> FormHelper:
129 | user: models.User = self.object
130 | helper = super().get_form_helper(form)
131 | helper.form_action = reverse('user-update', kwargs=dict(pk=user.pk))
132 | helper.add_input(layout.Submit('submit', 'Save Changes'))
133 | return helper
134 |
135 | def get_success_url(self) -> str:
136 | user: models.User = self.object
137 | success_url: str = reverse('user-detail', kwargs=dict(pk=user.pk))
138 | return success_url
139 |
140 |
141 | class SubjectDetail(generic.DetailView):
142 | model = models.Subject
143 |
144 | def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
145 | """
146 | Add the question to the context.
147 | """
148 | context_data: Dict[str, Any] = super().get_context_data(**kwargs)
149 | queryset = models.Subject.objects.all()
150 | user: models.RequestUser = self.request.user
151 |
152 | if user.is_authenticated:
153 | # Subquery existence check for relation to user:
154 | queryset = queryset.annotate(
155 | user_following=Exists(
156 | models.User.objects.filter(pk=user.pk, subjects=OuterRef('pk')),
157 | ),
158 | )
159 | else:
160 | # For anonymous users, always false.
161 | queryset = queryset.annotate(
162 | user_following=Value(False, output_field=BooleanField()),
163 | )
164 | queryset = queryset.order_by('-user_following', 'title')
165 | context_data.setdefault('subject_list', queryset)
166 | return context_data
167 |
168 | def post(self, request, *args, **kwargs):
169 | user: models.RequestUser = self.request.user
170 | subject: models.Subject = models.Subject.objects.get(pk=kwargs.get('pk'))
171 | if user.is_authenticated:
172 | if 'follow-subject' in request.POST:
173 | follow_subject: models.Subject = models.Subject.objects.get(
174 | pk=request.POST['follow-subject'],
175 | )
176 | request.user.subjects.add(follow_subject)
177 | return HttpResponseRedirect(
178 | reverse('subject-detail', kwargs=dict(pk=subject.pk)))
179 | elif 'following-subject' in request.POST:
180 | follow_subject = models.Subject.objects.get(
181 | pk=request.POST['following-subject'],
182 | )
183 | request.user.subjects.remove(follow_subject)
184 | return HttpResponseRedirect(
185 | reverse('subject-detail', kwargs=dict(pk=follow_subject.pk)))
186 | else:
187 | return HttpResponseRedirect(
188 | reverse('subject-detail', kwargs=dict(pk=subject.pk)))
189 | return HttpResponseRedirect(
190 | f'/auth/login/?next=/subjects/{subject.pk}/')
191 |
192 |
193 | class UserDetail(generic.DetailView):
194 | model = models.User
195 |
196 | # Avoid conflicting with 'user' (the logged-in user)
197 | context_object_name = 'user_object'
198 |
199 |
200 | class QuestionDetail(generic.DetailView):
201 | model = models.Question
202 |
203 |
204 | class QuestionList(generic.ListView):
205 | model = models.Question
206 | ordering = ['-created']
207 |
208 | def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
209 | """
210 | Add the question to the context.
211 | """
212 | context_data: Dict[str, Any] = super().get_context_data(**kwargs)
213 | queryset = models.Subject.objects.all()
214 | user: models.RequestUser = self.request.user
215 |
216 | if user.is_authenticated:
217 | # Subquery existence check for relation to user:
218 | queryset = queryset.annotate(
219 | user_following=Exists(
220 | models.User.objects.filter(pk=user.pk, subjects=OuterRef('pk')),
221 | ),
222 | )
223 | else:
224 | # For anonymous users, always false.
225 | queryset = queryset.annotate(
226 | user_following=Value(False, output_field=BooleanField()),
227 | )
228 | queryset = queryset.order_by('-user_following', 'title')
229 | context_data.setdefault('subject_list', queryset)
230 | return context_data
231 |
232 | def post(self, request, *args, **kwargs):
233 | user: models.RequestUser = self.request.user
234 | if user.is_authenticated:
235 | if 'follow-subject' in request.POST:
236 | follow_subject: models.Subject = models.Subject.objects.get(
237 | pk=request.POST['follow-subject'],
238 | )
239 | request.user.subjects.add(follow_subject)
240 | return HttpResponseRedirect(
241 | reverse(
242 | 'subject-detail',
243 | kwargs=dict(pk=request.POST['follow-subject'])),
244 | )
245 | elif 'following-subject' in request.POST:
246 | following_subject = models.Subject.objects.get(
247 | pk=request.POST['following-subject'],
248 | )
249 | request.user.subjects.remove(following_subject)
250 | return HttpResponseRedirect(
251 | reverse('subject-detail', kwargs=dict(pk=following_subject.pk)))
252 | else:
253 | return HttpResponseRedirect(
254 | reverse('question-list'))
255 | return HttpResponseRedirect(
256 | f'/auth/login/?next=/')
257 |
258 |
259 | class QuestionModelFormMixin(CrispyFormMixin, LoginRequiredMixin, ModelFormMixin):
260 | """
261 | Base class for the Question create & update views.
262 | """
263 | model = models.Question
264 | fields = [
265 | 'title',
266 | 'body',
267 | ]
268 | subject: models.Subject
269 |
270 | def get_success_url(self) -> str:
271 | """
272 | Redirect to the question.
273 | """
274 | question: models.Question = self.object
275 | success_url: str = reverse('question-detail', kwargs=dict(pk=question.pk))
276 | return success_url
277 |
278 |
279 | class QuestionCreate(QuestionModelFormMixin, generic.CreateView):
280 |
281 | def dispatch(
282 | self,
283 | request: HttpRequest,
284 | *args: Any,
285 | subject_pk: int,
286 | **kwargs: Any,
287 | ) -> HttpResponse:
288 | """
289 | Look up the question, and set `self.question`.
290 | """
291 | self.subject: models.Subject = get_object_or_404(models.Subject, pk=subject_pk)
292 | return super().dispatch(request, *args, **kwargs)
293 |
294 | def get_form_helper(self, form: forms.ModelForm) -> FormHelper:
295 | helper = super().get_form_helper(form)
296 | helper.form_action = reverse(
297 | 'question-create',
298 | kwargs=dict(subject_pk=self.subject.pk),
299 | )
300 | helper.add_input(layout.Submit(
301 | name='submit',
302 | value='Ask question',
303 | css_class='btn-buza-green',
304 | ))
305 | return helper
306 |
307 | def form_valid(self, form: forms.ModelForm) -> HttpResponse:
308 | """
309 | Set the question's author to the posting user.
310 | """
311 | question: models.Question = form.instance
312 | author: models.User = self.request.user
313 | assert author.is_authenticated, author
314 | question.author = author
315 | question.subject = self.subject
316 | question.grade = author.grade
317 | return super().form_valid(form)
318 |
319 |
320 | class QuestionUpdate(QuestionModelFormMixin, generic.UpdateView):
321 |
322 | def get_object(self, queryset=None):
323 | """
324 | Permission check: Users can only edit their own questions.
325 |
326 | TODO (Pi): Use django-auth-utils for this?
327 | """
328 | question: models.Question = super().get_object(queryset)
329 | if question.author != self.request.user:
330 | raise PermissionDenied('You can only edit your own questions.')
331 | return question
332 |
333 | def get_form_helper(self, form: forms.ModelForm) -> FormHelper:
334 | helper = super().get_form_helper(form)
335 | helper.form_action = reverse(
336 | 'question-update',
337 | kwargs=dict(pk=form.instance.pk),
338 | )
339 | helper.add_input(layout.Submit(
340 | name='submit',
341 | value='Save',
342 | css_class='btn-buza-green',
343 | ))
344 | return helper
345 |
346 |
347 | class AnswerCreate(LoginRequiredMixin, generic.CreateView):
348 | """
349 | Post a new answer to a question.
350 |
351 | Expects `question_pk` as a view argument.
352 | """
353 |
354 | model = models.Answer
355 | fields = [
356 | 'body',
357 | ]
358 |
359 | question: models.Question
360 |
361 | def dispatch(
362 | self,
363 | request: HttpRequest,
364 | *args: Any,
365 | question_pk: int,
366 | **kwargs: Any,
367 | ) -> HttpResponse:
368 | """
369 | Look up the question, and set `self.question`.
370 | """
371 | self.question = get_object_or_404(models.Question, pk=question_pk)
372 | return super().dispatch(request, *args, **kwargs)
373 |
374 | def form_valid(self, form: forms.ModelForm) -> HttpResponse:
375 | """
376 | Set the answer's author to the posting user.
377 | """
378 | answer: models.Answer = form.instance
379 | author: models.User = self.request.user
380 | assert author.is_authenticated, author
381 | answer.author = author
382 | answer.question = self.question
383 | return super().form_valid(form)
384 |
385 | def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
386 | """
387 | Add the question to the context.
388 | """
389 | context_data: Dict[str, Any] = super().get_context_data(**kwargs)
390 | context_data.setdefault('question', self.question)
391 | return context_data
392 |
393 | def get_success_url(self) -> str:
394 | """
395 | Redirect to the question.
396 | """
397 | answer: models.Answer = self.object
398 | question: models.Question = answer.question
399 | success_url: str = reverse('question-detail', kwargs=dict(pk=question.pk))
400 | return success_url
401 |
402 |
403 | class AnswerUpdate(LoginRequiredMixin, generic.UpdateView):
404 | """
405 | Post a new answer to a question.
406 |
407 | Expects `question_pk` as a view argument.
408 | """
409 |
410 | model = models.Answer
411 | fields = [
412 | 'body',
413 | ]
414 |
415 | def get_object(self, queryset: QuerySet = None) -> models.Answer:
416 | """
417 | Permission check: Users can only edit their own answers.
418 |
419 | TODO (Pi): Use django-auth-utils for this?
420 | """
421 | answer: models.Answer = super().get_object(queryset)
422 | if answer.author != self.request.user:
423 | raise PermissionDenied('You can only edit your own answers.')
424 | return answer
425 |
426 | def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
427 | """
428 | Add the question to the context.
429 | """
430 | answer: models.Answer = self.object
431 | context_data: Dict[str, Any] = super().get_context_data(**kwargs)
432 | context_data.setdefault('question', answer.question)
433 | return context_data
434 |
435 | def get_success_url(self) -> str:
436 | """
437 | Redirect to the question.
438 | """
439 | answer: models.Answer = self.object
440 | question: models.Question = answer.question
441 | success_url: str = reverse('question-detail', kwargs=dict(pk=question.pk))
442 | return success_url
443 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "1d6cbe29aca0a1267c50b66f0091ab3acb4c099ef8a3ed269881971d58ee07a0"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.6"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "buza-website": {
20 | "editable": true,
21 | "path": "."
22 | },
23 | "certifi": {
24 | "hashes": [
25 | "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
26 | "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
27 | ],
28 | "version": "==2019.3.9"
29 | },
30 | "chardet": {
31 | "hashes": [
32 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
33 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
34 | ],
35 | "version": "==3.0.4"
36 | },
37 | "defusedxml": {
38 | "hashes": [
39 | "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
40 | "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
41 | ],
42 | "markers": "python_version >= '3.0'",
43 | "version": "==0.6.0"
44 | },
45 | "django": {
46 | "hashes": [
47 | "sha256:0fd54e4f27bc3e0b7054a11e6b3a18fa53f2373f6b2df8a22e8eadfe018970a5",
48 | "sha256:f3b28084101d516f56104856761bc247f85a2a5bbd9da39d9f6197ff461b3ee4"
49 | ],
50 | "version": "==2.1.8"
51 | },
52 | "django-crispy-forms": {
53 | "hashes": [
54 | "sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f",
55 | "sha256:705ededc554ad8736157c666681165fe22ead2dec0d5446d65fc9dd976a5a876"
56 | ],
57 | "version": "==1.7.2"
58 | },
59 | "django-environ": {
60 | "hashes": [
61 | "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde",
62 | "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"
63 | ],
64 | "version": "==0.4.5"
65 | },
66 | "django-taggit": {
67 | "hashes": [
68 | "sha256:01bf163f66f385de3777378f43338aba93aae8673891d8ba9a20695b2ffb8e10",
69 | "sha256:c13dfc1808a3084b64898e591af1d2f49b672d108388654804b170ee0ac5caf0"
70 | ],
71 | "version": "==1.1.0"
72 | },
73 | "humanize": {
74 | "hashes": [
75 | "sha256:a43f57115831ac7c70de098e6ac46ac13be00d69abbf60bdcac251344785bb19"
76 | ],
77 | "index": "pypi",
78 | "version": "==0.5.1"
79 | },
80 | "idna": {
81 | "hashes": [
82 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
83 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
84 | ],
85 | "version": "==2.8"
86 | },
87 | "oauthlib": {
88 | "hashes": [
89 | "sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298",
90 | "sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e"
91 | ],
92 | "version": "==3.0.1"
93 | },
94 | "pillow": {
95 | "hashes": [
96 | "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55",
97 | "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479",
98 | "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a",
99 | "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d",
100 | "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb",
101 | "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb",
102 | "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8",
103 | "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72",
104 | "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754",
105 | "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f",
106 | "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce",
107 | "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601",
108 | "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5",
109 | "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734",
110 | "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b",
111 | "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b",
112 | "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1",
113 | "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91",
114 | "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8",
115 | "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239",
116 | "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af",
117 | "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8",
118 | "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232",
119 | "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a",
120 | "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3",
121 | "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062"
122 | ],
123 | "version": "==6.0.0"
124 | },
125 | "psycopg2-binary": {
126 | "hashes": [
127 | "sha256:007ca0df127b1862fc010125bc4100b7a630efc6841047bd11afceadb4754611",
128 | "sha256:03c49e02adf0b4d68f422fdbd98f7a7c547beb27e99a75ed02298f85cb48406a",
129 | "sha256:0a1232cdd314e08848825edda06600455ad2a7adaa463ebfb12ece2d09f3370e",
130 | "sha256:131c80d0958c89273d9720b9adf9df1d7600bb3120e16019a7389ab15b079af5",
131 | "sha256:2de34cc3b775724623f86617d2601308083176a495f5b2efc2bbb0da154f483a",
132 | "sha256:2eddc31500f73544a2a54123d4c4b249c3c711d31e64deddb0890982ea37397a",
133 | "sha256:484f6c62bdc166ee0e5be3aa831120423bf399786d1f3b0304526c86180fbc0b",
134 | "sha256:4c2d9369ed40b4a44a8ccd6bc3a7db6272b8314812d2d1091f95c4c836d92e06",
135 | "sha256:70f570b5fa44413b9f30dbc053d17ef3ce6a4100147a10822f8662e58d473656",
136 | "sha256:7a2b5b095f3bd733aab101c89c0e1a3f0dfb4ebdc26f6374805c086ffe29d5b2",
137 | "sha256:804914a669186e2843c1f7fbe12b55aad1b36d40a28274abe6027deffad9433d",
138 | "sha256:8520c03172da18345d012949a53617a963e0191ccb3c666f23276d5326af27b5",
139 | "sha256:90da901fc33ea393fc644607e4a3916b509387e9339ec6ebc7bfded45b7a0ae9",
140 | "sha256:a582416ad123291a82c300d1d872bdc4136d69ad0b41d57dc5ca3df7ef8e3088",
141 | "sha256:ac8c5e20309f4989c296d62cac20ee456b69c41fd1bc03829e27de23b6fa9dd0",
142 | "sha256:b2cf82f55a619879f8557fdaae5cec7a294fac815e0087c4f67026fdf5259844",
143 | "sha256:b59d6f8cfca2983d8fdbe457bf95d2192f7b7efdb2b483bf5fa4e8981b04e8b2",
144 | "sha256:be08168197021d669b9964bd87628fa88f910b1be31e7010901070f2540c05fd",
145 | "sha256:be0f952f1c365061041bad16e27e224e29615d4eb1fb5b7e7760a1d3d12b90b6",
146 | "sha256:c1c9a33e46d7c12b9c96cf2d4349d783e3127163fd96254dcd44663cf0a1d438",
147 | "sha256:d18c89957ac57dd2a2724ecfe9a759912d776f96ecabba23acb9ecbf5c731035",
148 | "sha256:d7e7b0ff21f39433c50397e60bf0995d078802c591ca3b8d99857ea18a7496ee",
149 | "sha256:da0929b2bf0d1f365345e5eb940d8713c1d516312e010135b14402e2a3d2404d",
150 | "sha256:de24a4962e361c512d3e528ded6c7480eab24c655b8ca1f0b761d3b3650d2f07",
151 | "sha256:e45f93ff3f7dae2202248cf413a87aeb330821bf76998b3cf374eda2fc893dd7",
152 | "sha256:f046aeae1f7a845041b8661bb7a52449202b6c5d3fb59eb4724e7ca088811904",
153 | "sha256:f1dc2b7b2748084b890f5d05b65a47cd03188824890e9a60818721fd492249fb",
154 | "sha256:fcbe7cf3a786572b73d2cd5f34ed452a5f5fac47c9c9d1e0642c457a148f9f88"
155 | ],
156 | "index": "pypi",
157 | "version": "==2.8.2"
158 | },
159 | "pyjwt": {
160 | "hashes": [
161 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
162 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
163 | ],
164 | "version": "==1.7.1"
165 | },
166 | "python3-openid": {
167 | "hashes": [
168 | "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa",
169 | "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"
170 | ],
171 | "markers": "python_version >= '3.0'",
172 | "version": "==3.1.0"
173 | },
174 | "pytz": {
175 | "hashes": [
176 | "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
177 | "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
178 | ],
179 | "version": "==2019.1"
180 | },
181 | "requests": {
182 | "hashes": [
183 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
184 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
185 | ],
186 | "version": "==2.21.0"
187 | },
188 | "requests-oauthlib": {
189 | "hashes": [
190 | "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57",
191 | "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140"
192 | ],
193 | "version": "==1.2.0"
194 | },
195 | "six": {
196 | "hashes": [
197 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
198 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
199 | ],
200 | "version": "==1.12.0"
201 | },
202 | "social-auth-app-django": {
203 | "hashes": [
204 | "sha256:6d0dd18c2d9e71ca545097d57b44d26f59e624a12833078e8e52f91baf849778",
205 | "sha256:9237e3d7b6f6f59494c3b02e0cce6efc69c9d33ad9d1a064e3b2318bcbe89ae3",
206 | "sha256:f151396e5b16e2eee12cd2e211004257826ece24fc4ae97a147df386c1cd7082"
207 | ],
208 | "version": "==3.1.0"
209 | },
210 | "social-auth-core": {
211 | "hashes": [
212 | "sha256:65122fb4287c70ff7915be0f52150fc1a9b9515eab3c3f0e4cd9dbb2a442a5c3",
213 | "sha256:cc871fb4528f7cbba67efdba0bc0f7d7c6eeb92113b0cdc9368dd91ffe965782",
214 | "sha256:f9f36dfa6af2823efb35a5ef65dfd02f66c944f389c33c25dd9621f8bb75a7da"
215 | ],
216 | "version": "==3.1.0"
217 | },
218 | "urllib3": {
219 | "hashes": [
220 | "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
221 | "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
222 | ],
223 | "version": "==1.24.2"
224 | }
225 | },
226 | "develop": {
227 | "atomicwrites": {
228 | "hashes": [
229 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
230 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
231 | ],
232 | "version": "==1.3.0"
233 | },
234 | "attrs": {
235 | "hashes": [
236 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
237 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
238 | ],
239 | "version": "==19.1.0"
240 | },
241 | "entrypoints": {
242 | "hashes": [
243 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
244 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
245 | ],
246 | "version": "==0.3"
247 | },
248 | "flake8": {
249 | "hashes": [
250 | "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
251 | "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
252 | ],
253 | "index": "pypi",
254 | "version": "==3.7.7"
255 | },
256 | "flake8-commas": {
257 | "hashes": [
258 | "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7",
259 | "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e"
260 | ],
261 | "index": "pypi",
262 | "version": "==2.0.0"
263 | },
264 | "isort": {
265 | "hashes": [
266 | "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43",
267 | "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a"
268 | ],
269 | "index": "pypi",
270 | "version": "==4.3.17"
271 | },
272 | "mccabe": {
273 | "hashes": [
274 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
275 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
276 | ],
277 | "version": "==0.6.1"
278 | },
279 | "more-itertools": {
280 | "hashes": [
281 | "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
282 | "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
283 | ],
284 | "markers": "python_version > '2.7'",
285 | "version": "==7.0.0"
286 | },
287 | "mypy": {
288 | "hashes": [
289 | "sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
290 | "sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
291 | "sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
292 | "sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
293 | "sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
294 | "sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
295 | "sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
296 | "sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
297 | "sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
298 | "sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
299 | "sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
300 | ],
301 | "index": "pypi",
302 | "version": "==0.701"
303 | },
304 | "mypy-extensions": {
305 | "hashes": [
306 | "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
307 | "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
308 | ],
309 | "version": "==0.4.1"
310 | },
311 | "pluggy": {
312 | "hashes": [
313 | "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
314 | "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
315 | ],
316 | "version": "==0.9.0"
317 | },
318 | "py": {
319 | "hashes": [
320 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
321 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
322 | ],
323 | "version": "==1.8.0"
324 | },
325 | "pycodestyle": {
326 | "hashes": [
327 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
328 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
329 | ],
330 | "version": "==2.5.0"
331 | },
332 | "pyflakes": {
333 | "hashes": [
334 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
335 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
336 | ],
337 | "version": "==2.1.1"
338 | },
339 | "pytest": {
340 | "hashes": [
341 | "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
342 | "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
343 | ],
344 | "index": "pypi",
345 | "version": "==4.4.1"
346 | },
347 | "pytest-django": {
348 | "hashes": [
349 | "sha256:30d773f1768e8f214a3106f1090e00300ce6edfcac8c55fd13b675fe1cbd1c85",
350 | "sha256:4d3283e774fe1d40630ee58bf34929b83875e4751b525eeb07a7506996eb42ee"
351 | ],
352 | "index": "pypi",
353 | "version": "==3.4.8"
354 | },
355 | "setuptools-scm": {
356 | "hashes": [
357 | "sha256:057a67cb0a33e0f95edd828e47809f49b7104f4bc333a98fd35d4d05738c6187",
358 | "sha256:52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358"
359 | ],
360 | "index": "pypi",
361 | "version": "==3.2.0"
362 | },
363 | "six": {
364 | "hashes": [
365 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
366 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
367 | ],
368 | "version": "==1.12.0"
369 | },
370 | "typed-ast": {
371 | "hashes": [
372 | "sha256:04894d268ba6eab7e093d43107869ad49e7b5ef40d1a94243ea49b352061b200",
373 | "sha256:16616ece19daddc586e499a3d2f560302c11f122b9c692bc216e821ae32aa0d0",
374 | "sha256:252fdae740964b2d3cdfb3f84dcb4d6247a48a6abe2579e8029ab3be3cdc026c",
375 | "sha256:2af80a373af123d0b9f44941a46df67ef0ff7a60f95872412a145f4500a7fc99",
376 | "sha256:2c88d0a913229a06282b285f42a31e063c3bf9071ff65c5ea4c12acb6977c6a7",
377 | "sha256:2ea99c029ebd4b5a308d915cc7fb95b8e1201d60b065450d5d26deb65d3f2bc1",
378 | "sha256:3d2e3ab175fc097d2a51c7a0d3fda442f35ebcc93bb1d7bd9b95ad893e44c04d",
379 | "sha256:4766dd695548a15ee766927bf883fb90c6ac8321be5a60c141f18628fb7f8da8",
380 | "sha256:56b6978798502ef66625a2e0f80cf923da64e328da8bbe16c1ff928c70c873de",
381 | "sha256:5cddb6f8bce14325b2863f9d5ac5c51e07b71b462361fd815d1d7706d3a9d682",
382 | "sha256:644ee788222d81555af543b70a1098f2025db38eaa99226f3a75a6854924d4db",
383 | "sha256:64cf762049fc4775efe6b27161467e76d0ba145862802a65eefc8879086fc6f8",
384 | "sha256:68c362848d9fb71d3c3e5f43c09974a0ae319144634e7a47db62f0f2a54a7fa7",
385 | "sha256:6c1f3c6f6635e611d58e467bf4371883568f0de9ccc4606f17048142dec14a1f",
386 | "sha256:b213d4a02eec4ddf622f4d2fbc539f062af3788d1f332f028a2e19c42da53f15",
387 | "sha256:bb27d4e7805a7de0e35bd0cb1411bc85f807968b2b0539597a49a23b00a622ae",
388 | "sha256:c9d414512eaa417aadae7758bc118868cd2396b0e6138c1dd4fda96679c079d3",
389 | "sha256:f0937165d1e25477b01081c4763d2d9cdc3b18af69cb259dd4f640c9b900fe5e",
390 | "sha256:fb96a6e2c11059ecf84e6741a319f93f683e440e341d4489c9b161eca251cf2a",
391 | "sha256:fc71d2d6ae56a091a8d94f33ec9d0f2001d1cb1db423d8b4355debfe9ce689b7"
392 | ],
393 | "version": "==1.3.4"
394 | }
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/examples/example-data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "buza.subject",
4 | "pk": 1,
5 | "fields": {
6 | "title": "English",
7 | "description": "Literature, most generically, is any body of written works. More restrictively, literature refers to writing considered to be an art form, or any single writing deemed to have artistic or intellectual value, often due to deploying language in ways that differ from ordinary usage.\r\n\r\nIts Latin root literatura/litteratura (derived itself from littera: letter or handwriting) was used to refer to all written accounts, though contemporary definitions extend the term to include texts that are spoken or sung (oral literature). The concept has changed meaning over time: nowadays it can broaden to have non-written verbal art forms, and thus it is difficult to agree on its origin, which can be paired with that of language or writing itself. Developments in print technology have allowed an ever-growing distribution and proliferation of written works, culminating in electronic literature."
8 | }
9 | },
10 | {
11 | "model": "buza.subject",
12 | "pk": 2,
13 | "fields": {
14 | "title": "Life Science",
15 | "description": "The life sciences or biological sciences comprise the branches of science that involve the scientific study of life and organisms \u2013 such as microorganisms, plants, and animals including human beings.\r\n\r\nLife science is one of the two major branches of natural science, the other being physical science, which is concerned with non-living matter.\r\n\r\nBy definition, biology is the natural science that studies life and living organisms, with the other life sciences being its sub-disciplines.\r\n\r\nSome life sciences focus on a specific type of organism. For example, zoology is the study of animals, while botany is the study of plants. Other life sciences focus on aspects common to all or many life forms, such as anatomy and genetics. Some focus on the micro scale (e.g. molecular biology, biochemistry) other on larger scales (e.g. cytology, immunology, ethology, ecology). Another major, branch of life sciences involves understanding the mind \u2013 neuroscience.\r\n\r\nLife sciences discoveries are helpful in improving the quality and standard of life, and have applications in health, agriculture, medicine, and the pharmaceutical and food science industries."
16 | }
17 | },
18 | {
19 | "model": "buza.subject",
20 | "pk": 3,
21 | "fields": {
22 | "title": "Setswana",
23 | "description": "Setswana ke puo e e buiwang mo mafatsheng a Aforika Borwa, Botswana, Namibia le Zimbabwe. Ke nngwe ya dipuo tsa semmuso kwa Aforika Borwa fa kwa Botswana gone e le puo ee tsewang e le yone ya lefatshe ka bophara. Banni bangwe ba Botswana ba tsalwa ba sa bue Setswana jaaka Bakgalagadi, Bakalaka le ba bangwe. Ba se ithuta mo mebileng le kwa sekolong. Molao motheo wa Zimbabwe o moswa o kaya puo ya Setswana e le nngwe ya dipuo tsa semmuso tsa lefatshe leo."
24 | }
25 | },
26 | {
27 | "model": "buza.subject",
28 | "pk": 4,
29 | "fields": {
30 | "title": "Economics and Management Science",
31 | "description": "Economic and Management Sciences is made up of the School of Management Sciences, the School of Economic and Financial Sciences, and the School of Public & Operations Management, The Institute For Corporate Citizenship and five Centres"
32 | }
33 | },
34 | {
35 | "model": "buza.subject",
36 | "pk": 5,
37 | "fields": {
38 | "title": "Accounting",
39 | "description": "Accounting or accountancy is the measurement, processing, and communication of financial information about economic entities[1][2] such as businesses and corporations. The modern field was established by the Italian mathematician Luca Pacioli in 1494.[3] Accounting, which has been called the \"language of business\",[4] measures the results of an organization's economic activities and conveys this information to a variety of users, including investors, creditors, management, and regulators.[5] Practitioners of accounting are known as accountants. The terms \"accounting\" and \"financial reporting\" are often used as synonyms."
40 | }
41 | },
42 | {
43 | "model": "buza.subject",
44 | "pk": 6,
45 | "fields": {
46 | "title": "Life Orientation",
47 | "description": "Life orientation is an excitingly diverse subject, incorporating many aspects of life. Most people who matriculated more than a decade ago, will remember life skills, guidance counselling, PT classes and religious studies. However, Life orientation (LO) has evolved into a holistic subject encompassing emotional, physical, spiritual and mental aspects of life. For example: Life orientation provides a learner with the necessary skills to compile a CV, understand relationships, find a career, learn about lifestyle diseases or understand why democracy is necessary in our country\u2026.to name but a few."
48 | }
49 | },
50 | {
51 | "model": "buza.subject",
52 | "pk": 7,
53 | "fields": {
54 | "title": "Information Technology",
55 | "description": "Computer science is the study of the theory, experimentation, and engineering that form the basis for the design and use of computers. It is the scientific and practical approach to computation and its applications and the systematic study of the feasibility, structure, expression, and mechanization of the methodical procedures (or algorithms) that underlie the acquisition, representation, processing, storage, communication of, and access to, information. An alternate, more succinct definition of computer science is the study of automating algorithmic processes that scale. A computer scientist specializes in the theory of computation and the design of computational systems.[1] See glossary of computer science."
56 | }
57 | },
58 | {
59 | "model": "buza.subject",
60 | "pk": 8,
61 | "fields": {
62 | "title": "Mathematics",
63 | "description": "Mathematics as \"the Queen of the Sciences\". The science of numbers and their operations, interrelations, combinations, generalizations, and abstractions and of space configurations and their structure, measurement, transformations, and generalizations\r\n\r\n Algebra, arithmetic, calculus, geometry, and trigonometry are branches of mathematics."
64 | }
65 | },
66 | {
67 | "model": "buza.subject",
68 | "pk": 9,
69 | "fields": {
70 | "title": "Physics",
71 | "description": "Physics is the branch of science concerned with the nature and properties of matter and energy. The subject matter of physics includes mechanics, heat, light and other radiation, sound, electricity, magnetism, and the structure of atoms."
72 | }
73 | },
74 | {
75 | "model": "buza.subject",
76 | "pk": 10,
77 | "fields": {
78 | "title": "Economics",
79 | "description": "Economics is the social science that studies the production, distribution, and consumption of goods and services.[4]\r\n\r\nEconomics focuses on the behaviour and interactions of economic agents and how economies work. Microeconomics analyzes basic elements in the economy, including individual agents and markets, their interactions, and the outcomes of interactions. Individual agents may include, for example, households, firms, buyers, and sellers. Macroeconomics analyzes the entire economy (meaning aggregated production, consumption, savings, and investment) and issues affecting it, including unemployment of resources (labour, capital, and land), inflation, economic growth, and the public policies that address these issues (monetary, fiscal, and other policies). See glossary of economics."
80 | }
81 | },
82 | {
83 | "model": "buza.topic",
84 | "pk": 1,
85 | "fields": {
86 | "name": "",
87 | "slug": "",
88 | "description": "Trig is the study of shapes and stuff"
89 | }
90 | },
91 | {
92 | "model": "buza.topic",
93 | "pk": 2,
94 | "fields": {
95 | "name": "maths",
96 | "slug": "maths",
97 | "description": ""
98 | }
99 | },
100 | {
101 | "model": "buza.topic",
102 | "pk": 3,
103 | "fields": {
104 | "name": "trig",
105 | "slug": "trig",
106 | "description": "Trigonometry is a branch of mathematics that studies relationships involving lengths and angles of triangles. The field emerged in the Hellenistic world during ..."
107 | }
108 | },
109 | {
110 | "model": "buza.topic",
111 | "pk": 4,
112 | "fields": {
113 | "name": "calculus",
114 | "slug": "calculus",
115 | "description": "Numbers and stuff"
116 | }
117 | },
118 | {
119 | "model": "buza.topic",
120 | "pk": 5,
121 | "fields": {
122 | "name": "circles",
123 | "slug": "circles",
124 | "description": ""
125 | }
126 | },
127 | {
128 | "model": "buza.topic",
129 | "pk": 6,
130 | "fields": {
131 | "name": "president mangope",
132 | "slug": "president-mangope",
133 | "description": ""
134 | }
135 | },
136 | {
137 | "model": "buza.topic",
138 | "pk": 7,
139 | "fields": {
140 | "name": "girl",
141 | "slug": "girl",
142 | "description": ""
143 | }
144 | },
145 | {
146 | "model": "buza.topic",
147 | "pk": 8,
148 | "fields": {
149 | "name": "triangles",
150 | "slug": "triangles",
151 | "description": ""
152 | }
153 | },
154 | {
155 | "model": "buza.topic",
156 | "pk": 9,
157 | "fields": {
158 | "name": "area",
159 | "slug": "area",
160 | "description": ""
161 | }
162 | },
163 | {
164 | "model": "buza.topic",
165 | "pk": 10,
166 | "fields": {
167 | "name": "geometry",
168 | "slug": "geometry",
169 | "description": ""
170 | }
171 | },
172 | {
173 | "model": "buza.topic",
174 | "pk": 11,
175 | "fields": {
176 | "name": "Triangles",
177 | "slug": "triangles_1",
178 | "description": ""
179 | }
180 | },
181 | {
182 | "model": "buza.topic",
183 | "pk": 12,
184 | "fields": {
185 | "name": "dennotation",
186 | "slug": "dennotation",
187 | "description": ""
188 | }
189 | },
190 | {
191 | "model": "buza.topic",
192 | "pk": 13,
193 | "fields": {
194 | "name": "energy",
195 | "slug": "energy",
196 | "description": ""
197 | }
198 | },
199 | {
200 | "model": "buza.topic",
201 | "pk": 14,
202 | "fields": {
203 | "name": "work",
204 | "slug": "work",
205 | "description": ""
206 | }
207 | },
208 | {
209 | "model": "buza.topic",
210 | "pk": 15,
211 | "fields": {
212 | "name": "and",
213 | "slug": "and",
214 | "description": ""
215 | }
216 | },
217 | {
218 | "model": "buza.topic",
219 | "pk": 16,
220 | "fields": {
221 | "name": "forces",
222 | "slug": "forces",
223 | "description": ""
224 | }
225 | },
226 | {
227 | "model": "buza.topic",
228 | "pk": 17,
229 | "fields": {
230 | "name": "potential",
231 | "slug": "potential",
232 | "description": ""
233 | }
234 | },
235 | {
236 | "model": "buza.topic",
237 | "pk": 18,
238 | "fields": {
239 | "name": "velocity",
240 | "slug": "velocity",
241 | "description": ""
242 | }
243 | },
244 | {
245 | "model": "buza.topic",
246 | "pk": 19,
247 | "fields": {
248 | "name": "lediri",
249 | "slug": "lediri",
250 | "description": ""
251 | }
252 | },
253 | {
254 | "model": "buza.topic",
255 | "pk": 20,
256 | "fields": {
257 | "name": "rule",
258 | "slug": "rule",
259 | "description": ""
260 | }
261 | },
262 | {
263 | "model": "buza.topic",
264 | "pk": 21,
265 | "fields": {
266 | "name": "The",
267 | "slug": "the",
268 | "description": ""
269 | }
270 | },
271 | {
272 | "model": "buza.topic",
273 | "pk": 22,
274 | "fields": {
275 | "name": "Trigonometric",
276 | "slug": "trigonometric",
277 | "description": ""
278 | }
279 | },
280 | {
281 | "model": "buza.topic",
282 | "pk": 23,
283 | "fields": {
284 | "name": "poetry",
285 | "slug": "poetry",
286 | "description": ""
287 | }
288 | },
289 | {
290 | "model": "buza.topic",
291 | "pk": 24,
292 | "fields": {
293 | "name": "poems/",
294 | "slug": "poems",
295 | "description": ""
296 | }
297 | },
298 | {
299 | "model": "buza.questiontopic",
300 | "pk": 11,
301 | "fields": {
302 | "content_type": [
303 | "buza",
304 | "question"
305 | ],
306 | "object_id": 4,
307 | "tag": 8
308 | }
309 | },
310 | {
311 | "model": "buza.questiontopic",
312 | "pk": 12,
313 | "fields": {
314 | "content_type": [
315 | "buza",
316 | "question"
317 | ],
318 | "object_id": 4,
319 | "tag": 9
320 | }
321 | },
322 | {
323 | "model": "buza.questiontopic",
324 | "pk": 13,
325 | "fields": {
326 | "content_type": [
327 | "buza",
328 | "question"
329 | ],
330 | "object_id": 4,
331 | "tag": 3
332 | }
333 | },
334 | {
335 | "model": "buza.questiontopic",
336 | "pk": 14,
337 | "fields": {
338 | "content_type": [
339 | "buza",
340 | "question"
341 | ],
342 | "object_id": 5,
343 | "tag": 10
344 | }
345 | },
346 | {
347 | "model": "buza.questiontopic",
348 | "pk": 15,
349 | "fields": {
350 | "content_type": [
351 | "buza",
352 | "question"
353 | ],
354 | "object_id": 6,
355 | "tag": 11
356 | }
357 | },
358 | {
359 | "model": "buza.questiontopic",
360 | "pk": 16,
361 | "fields": {
362 | "content_type": [
363 | "buza",
364 | "question"
365 | ],
366 | "object_id": 7,
367 | "tag": 12
368 | }
369 | },
370 | {
371 | "model": "buza.questiontopic",
372 | "pk": 17,
373 | "fields": {
374 | "content_type": [
375 | "buza",
376 | "question"
377 | ],
378 | "object_id": 8,
379 | "tag": 13
380 | }
381 | },
382 | {
383 | "model": "buza.questiontopic",
384 | "pk": 18,
385 | "fields": {
386 | "content_type": [
387 | "buza",
388 | "question"
389 | ],
390 | "object_id": 8,
391 | "tag": 14
392 | }
393 | },
394 | {
395 | "model": "buza.questiontopic",
396 | "pk": 20,
397 | "fields": {
398 | "content_type": [
399 | "buza",
400 | "question"
401 | ],
402 | "object_id": 9,
403 | "tag": 16
404 | }
405 | },
406 | {
407 | "model": "buza.questiontopic",
408 | "pk": 21,
409 | "fields": {
410 | "content_type": [
411 | "buza",
412 | "question"
413 | ],
414 | "object_id": 10,
415 | "tag": 17
416 | }
417 | },
418 | {
419 | "model": "buza.questiontopic",
420 | "pk": 22,
421 | "fields": {
422 | "content_type": [
423 | "buza",
424 | "question"
425 | ],
426 | "object_id": 11,
427 | "tag": 18
428 | }
429 | },
430 | {
431 | "model": "buza.questiontopic",
432 | "pk": 23,
433 | "fields": {
434 | "content_type": [
435 | "buza",
436 | "question"
437 | ],
438 | "object_id": 12,
439 | "tag": 19
440 | }
441 | },
442 | {
443 | "model": "buza.questiontopic",
444 | "pk": 24,
445 | "fields": {
446 | "content_type": [
447 | "buza",
448 | "question"
449 | ],
450 | "object_id": 13,
451 | "tag": 9
452 | }
453 | },
454 | {
455 | "model": "buza.questiontopic",
456 | "pk": 25,
457 | "fields": {
458 | "content_type": [
459 | "buza",
460 | "question"
461 | ],
462 | "object_id": 13,
463 | "tag": 20
464 | }
465 | },
466 | {
467 | "model": "buza.questiontopic",
468 | "pk": 26,
469 | "fields": {
470 | "content_type": [
471 | "buza",
472 | "question"
473 | ],
474 | "object_id": 13,
475 | "tag": 21
476 | }
477 | },
478 | {
479 | "model": "buza.questiontopic",
480 | "pk": 27,
481 | "fields": {
482 | "content_type": [
483 | "buza",
484 | "question"
485 | ],
486 | "object_id": 14,
487 | "tag": 22
488 | }
489 | },
490 | {
491 | "model": "buza.questiontopic",
492 | "pk": 28,
493 | "fields": {
494 | "content_type": [
495 | "buza",
496 | "question"
497 | ],
498 | "object_id": 15,
499 | "tag": 24
500 | }
501 | },
502 | {
503 | "model": "buza.questiontopic",
504 | "pk": 29,
505 | "fields": {
506 | "content_type": [
507 | "buza",
508 | "question"
509 | ],
510 | "object_id": 15,
511 | "tag": 23
512 | }
513 | },
514 | {
515 | "model": "buza.user",
516 | "fields": {
517 | "password": "pbkdf2_sha256$120000$FUddRKJRABdr$lb2RRI3eWo16yS7QO7twK3U0oKigcPLX8FGeGD2fexY=",
518 | "last_login": null,
519 | "is_superuser": true,
520 | "username": "admin",
521 | "first_name": "",
522 | "last_name": "",
523 | "email": "admin@example.org",
524 | "is_staff": true,
525 | "is_active": true,
526 | "date_joined": "2018-09-20T18:48:57.355Z",
527 | "phone": "",
528 | "school": null,
529 | "school_address": null,
530 | "grade": 7,
531 | "photo": "",
532 | "bio": null,
533 | "groups": [],
534 | "user_permissions": [],
535 | "subjects": []
536 | }
537 | },
538 | {
539 | "model": "buza.user",
540 | "fields": {
541 | "password": "pbkdf2_sha256$120000$Pch3idPc5KPS$ZizlHSrPaer1lPmE8hHnTbXeUebnoaCeBOKDCifQYIQ=",
542 | "last_login": "2018-08-24T17:29:28Z",
543 | "is_superuser": false,
544 | "username": "tester0",
545 | "first_name": "",
546 | "last_name": "",
547 | "email": "",
548 | "is_staff": false,
549 | "is_active": true,
550 | "date_joined": "2018-08-19T09:33:57Z",
551 | "phone": "",
552 | "school": null,
553 | "school_address": null,
554 | "grade": 7,
555 | "photo": "",
556 | "bio": null,
557 | "groups": [],
558 | "user_permissions": [],
559 | "subjects": [
560 | 1
561 | ]
562 | }
563 | },
564 | {
565 | "model": "buza.user",
566 | "fields": {
567 | "password": "pbkdf2_sha256$120000$q2xi6FdRA9w9$Pc+Vv+SJvRuHRlh/g0e0Z+9/Q6vvW6jlOuu3WHLds5g=",
568 | "last_login": "2018-08-21T07:08:07.390Z",
569 | "is_superuser": false,
570 | "username": "tester1",
571 | "first_name": "",
572 | "last_name": "",
573 | "email": "",
574 | "is_staff": false,
575 | "is_active": true,
576 | "date_joined": "2018-08-21T06:26:28.824Z",
577 | "phone": "",
578 | "school": null,
579 | "school_address": null,
580 | "grade": 7,
581 | "photo": "",
582 | "bio": null,
583 | "groups": [],
584 | "user_permissions": [],
585 | "subjects": [
586 | 3,
587 | 7,
588 | 8
589 | ]
590 | }
591 | },
592 | {
593 | "model": "buza.user",
594 | "fields": {
595 | "password": "pbkdf2_sha256$120000$ga58DyJKQM7P$nmkzjFEcuGeDZgbR8N+kVe/RXnELnJHNDsdt/+xSWMU=",
596 | "last_login": "2018-08-21T07:15:58.144Z",
597 | "is_superuser": false,
598 | "username": "tester2",
599 | "first_name": "",
600 | "last_name": "",
601 | "email": "",
602 | "is_staff": false,
603 | "is_active": true,
604 | "date_joined": "2018-08-21T06:26:55.654Z",
605 | "phone": "",
606 | "school": null,
607 | "school_address": null,
608 | "grade": 7,
609 | "photo": "",
610 | "bio": null,
611 | "groups": [],
612 | "user_permissions": [],
613 | "subjects": [
614 | 3
615 | ]
616 | }
617 | },
618 | {
619 | "model": "buza.user",
620 | "fields": {
621 | "password": "pbkdf2_sha256$120000$NtuN5wiiGowo$ImMgMxDWyGbd5NlmQupXXsZxqrqzOG/Yx9N+0r1IzTI=",
622 | "last_login": "2018-08-21T07:22:15.987Z",
623 | "is_superuser": false,
624 | "username": "tester3",
625 | "first_name": "",
626 | "last_name": "",
627 | "email": "",
628 | "is_staff": false,
629 | "is_active": true,
630 | "date_joined": "2018-08-21T06:27:17.042Z",
631 | "phone": "",
632 | "school": null,
633 | "school_address": null,
634 | "grade": 7,
635 | "photo": "",
636 | "bio": null,
637 | "groups": [],
638 | "user_permissions": [],
639 | "subjects": [
640 | 1,
641 | 6
642 | ]
643 | }
644 | },
645 | {
646 | "model": "buza.user",
647 | "fields": {
648 | "password": "pbkdf2_sha256$120000$LmgHyF6JmsnD$W48TkRhBLD79RX9i/nBXB2KCYMlm6QTRTEKToj4et8M=",
649 | "last_login": "2018-08-21T07:33:37.125Z",
650 | "is_superuser": false,
651 | "username": "tester4",
652 | "first_name": "",
653 | "last_name": "",
654 | "email": "",
655 | "is_staff": false,
656 | "is_active": true,
657 | "date_joined": "2018-08-21T06:27:39.957Z",
658 | "phone": "",
659 | "school": null,
660 | "school_address": null,
661 | "grade": 7,
662 | "photo": "",
663 | "bio": null,
664 | "groups": [],
665 | "user_permissions": [],
666 | "subjects": [
667 | 1
668 | ]
669 | }
670 | },
671 | {
672 | "model": "buza.user",
673 | "fields": {
674 | "password": "pbkdf2_sha256$120000$CTiDw7WZw8ms$gcX9Dw1OKQVga4s9KfrmYwrxSXN1iWZ3fiSBXV5/VZE=",
675 | "last_login": "2018-08-22T11:10:29.771Z",
676 | "is_superuser": false,
677 | "username": "tester5",
678 | "first_name": "",
679 | "last_name": "",
680 | "email": "",
681 | "is_staff": false,
682 | "is_active": true,
683 | "date_joined": "2018-08-22T10:59:38.076Z",
684 | "phone": "",
685 | "school": null,
686 | "school_address": null,
687 | "grade": 7,
688 | "photo": "",
689 | "bio": null,
690 | "groups": [],
691 | "user_permissions": [],
692 | "subjects": [
693 | 3,
694 | 7,
695 | 8
696 | ]
697 | }
698 | },
699 | {
700 | "model": "buza.user",
701 | "fields": {
702 | "password": "pbkdf2_sha256$120000$fvymldkyRDij$k2Ht4KIuDfKPcR+AsuUPMK8nCHb4S1kkY+XnK5mJNWA=",
703 | "last_login": "2018-08-22T11:21:25.126Z",
704 | "is_superuser": false,
705 | "username": "tester6",
706 | "first_name": "",
707 | "last_name": "",
708 | "email": "",
709 | "is_staff": false,
710 | "is_active": true,
711 | "date_joined": "2018-08-22T10:59:58.970Z",
712 | "phone": "",
713 | "school": null,
714 | "school_address": null,
715 | "grade": 7,
716 | "photo": "",
717 | "bio": null,
718 | "groups": [],
719 | "user_permissions": [],
720 | "subjects": [
721 | 2
722 | ]
723 | }
724 | },
725 | {
726 | "model": "buza.user",
727 | "fields": {
728 | "password": "pbkdf2_sha256$120000$i3y6bsBxyo0R$taVDjjMcFF+RcW/9cGBG46pYMjUZPkvwqlARpfDwLTo=",
729 | "last_login": "2018-08-22T11:30:34.453Z",
730 | "is_superuser": false,
731 | "username": "tester7",
732 | "first_name": "",
733 | "last_name": "",
734 | "email": "",
735 | "is_staff": false,
736 | "is_active": true,
737 | "date_joined": "2018-08-22T11:00:20.984Z",
738 | "phone": "",
739 | "school": null,
740 | "school_address": null,
741 | "grade": 7,
742 | "photo": "",
743 | "bio": null,
744 | "groups": [],
745 | "user_permissions": [],
746 | "subjects": [
747 | 8
748 | ]
749 | }
750 | },
751 | {
752 | "model": "buza.user",
753 | "fields": {
754 | "password": "pbkdf2_sha256$120000$eoMSZdcvDKQ8$tEppJ4pN7eSjjApsVYTzex2DU7SkoEOFjxyGpYT6wZE=",
755 | "last_login": "2018-08-22T11:38:21.137Z",
756 | "is_superuser": false,
757 | "username": "tester8",
758 | "first_name": "",
759 | "last_name": "",
760 | "email": "",
761 | "is_staff": false,
762 | "is_active": true,
763 | "date_joined": "2018-08-22T11:00:41.087Z",
764 | "phone": "",
765 | "school": null,
766 | "school_address": null,
767 | "grade": 7,
768 | "photo": "",
769 | "bio": null,
770 | "groups": [],
771 | "user_permissions": [],
772 | "subjects": [
773 | 1,
774 | 2
775 | ]
776 | }
777 | },
778 | {
779 | "model": "buza.user",
780 | "fields": {
781 | "password": "pbkdf2_sha256$120000$unkx3xPkws3J$r60J6aJ89R3UXfN6FPWFSqyaqDsTy3B9P2QMnOXiZUQ=",
782 | "last_login": "2018-08-24T06:52:03.605Z",
783 | "is_superuser": false,
784 | "username": "tester9",
785 | "first_name": "",
786 | "last_name": "",
787 | "email": "",
788 | "is_staff": false,
789 | "is_active": true,
790 | "date_joined": "2018-08-24T06:49:24.806Z",
791 | "phone": "",
792 | "school": null,
793 | "school_address": null,
794 | "grade": 7,
795 | "photo": "",
796 | "bio": null,
797 | "groups": [],
798 | "user_permissions": [],
799 | "subjects": []
800 | }
801 | },
802 | {
803 | "model": "buza.user",
804 | "fields": {
805 | "password": "pbkdf2_sha256$120000$25NPyzuMcPoR$IPpTcVYNcbHRC3VyjLiSBdcgVOL9tAuoiBfIh3J0kmg=",
806 | "last_login": "2018-08-24T07:05:37.686Z",
807 | "is_superuser": false,
808 | "username": "tester10",
809 | "first_name": "",
810 | "last_name": "",
811 | "email": "",
812 | "is_staff": false,
813 | "is_active": true,
814 | "date_joined": "2018-08-24T06:49:55.212Z",
815 | "phone": "",
816 | "school": null,
817 | "school_address": null,
818 | "grade": 7,
819 | "photo": "",
820 | "bio": null,
821 | "groups": [],
822 | "user_permissions": [],
823 | "subjects": [
824 | 3,
825 | 6
826 | ]
827 | }
828 | },
829 | {
830 | "model": "buza.user",
831 | "fields": {
832 | "password": "pbkdf2_sha256$120000$Ylx0hAasVf5D$88m+zfHTwgRLF9H+/PqtXvK8Y6wxD4xFTtRAvXs2cww=",
833 | "last_login": "2018-08-24T09:17:03.531Z",
834 | "is_superuser": false,
835 | "username": "tester11",
836 | "first_name": "",
837 | "last_name": "",
838 | "email": "",
839 | "is_staff": false,
840 | "is_active": true,
841 | "date_joined": "2018-08-24T09:05:21.988Z",
842 | "phone": "",
843 | "school": null,
844 | "school_address": null,
845 | "grade": 7,
846 | "photo": "",
847 | "bio": null,
848 | "groups": [],
849 | "user_permissions": [],
850 | "subjects": [
851 | 3,
852 | 7,
853 | 8,
854 | 9
855 | ]
856 | }
857 | },
858 | {
859 | "model": "buza.user",
860 | "fields": {
861 | "password": "pbkdf2_sha256$120000$zGudUZAOcO9S$tC5Rpy3UWjojw+PjjYQ74hhrYAg6R8H75ay7IzOvNfs=",
862 | "last_login": "2018-08-24T09:30:10.586Z",
863 | "is_superuser": false,
864 | "username": "tester12",
865 | "first_name": "",
866 | "last_name": "",
867 | "email": "",
868 | "is_staff": false,
869 | "is_active": true,
870 | "date_joined": "2018-08-24T09:05:33.210Z",
871 | "phone": "",
872 | "school": null,
873 | "school_address": null,
874 | "grade": 7,
875 | "photo": "",
876 | "bio": null,
877 | "groups": [],
878 | "user_permissions": [],
879 | "subjects": [
880 | 2
881 | ]
882 | }
883 | },
884 | {
885 | "model": "buza.user",
886 | "fields": {
887 | "password": "pbkdf2_sha256$120000$eYWZDOghZwTI$lI3rFaaJeJCXYf9RKxS8nr8H7W48BNsj0JRzMC4uBbY=",
888 | "last_login": null,
889 | "is_superuser": false,
890 | "username": "tester13",
891 | "first_name": "",
892 | "last_name": "",
893 | "email": "",
894 | "is_staff": false,
895 | "is_active": true,
896 | "date_joined": "2018-08-24T09:05:49.071Z",
897 | "phone": "",
898 | "school": null,
899 | "school_address": null,
900 | "grade": 7,
901 | "photo": "",
902 | "bio": null,
903 | "groups": [],
904 | "user_permissions": [],
905 | "subjects": []
906 | }
907 | },
908 | {
909 | "model": "buza.question",
910 | "pk": 4,
911 | "fields": {
912 | "created": "2018-08-20T18:09:15.604Z",
913 | "modified": "2018-08-20T18:09:15.604Z",
914 | "author": [
915 | "tester0"
916 | ],
917 | "title": "How do I calculate the area of a triangle",
918 | "body": "Given the height is 5 and base 16, how do I calculate the area of the triangle?",
919 | "subject": 8,
920 | "grade": 7
921 | }
922 | },
923 | {
924 | "model": "buza.question",
925 | "pk": 5,
926 | "fields": {
927 | "created": "2018-08-21T07:10:21.093Z",
928 | "modified": "2018-08-21T07:10:21.093Z",
929 | "author": [
930 | "tester1"
931 | ],
932 | "title": "how do you calculate area of a circle",
933 | "body": "how do you calculate area of a circle when given diameter of 8",
934 | "subject": 8,
935 | "grade": 7
936 | }
937 | },
938 | {
939 | "model": "buza.question",
940 | "pk": 6,
941 | "fields": {
942 | "created": "2018-08-21T07:25:49.811Z",
943 | "modified": "2018-08-21T07:26:15.666Z",
944 | "author": [
945 | "tester3"
946 | ],
947 | "title": "finding the area",
948 | "body": "how can i find the area of a triangle",
949 | "subject": 8,
950 | "grade": 7
951 | }
952 | },
953 | {
954 | "model": "buza.question",
955 | "pk": 7,
956 | "fields": {
957 | "created": "2018-08-21T07:36:10.340Z",
958 | "modified": "2018-08-21T07:36:35.848Z",
959 | "author": [
960 | "tester4"
961 | ],
962 | "title": "dennotation",
963 | "body": "define the word dennotation",
964 | "subject": 1,
965 | "grade": 7
966 | }
967 | },
968 | {
969 | "model": "buza.question",
970 | "pk": 8,
971 | "fields": {
972 | "created": "2018-08-22T11:13:31.835Z",
973 | "modified": "2018-08-23T17:53:42.288Z",
974 | "author": [
975 | "tester5"
976 | ],
977 | "title": "what is the defination of potential energy",
978 | "body": "what is the defination of potential energy",
979 | "subject": 9,
980 | "grade": 10
981 | }
982 | },
983 | {
984 | "model": "buza.question",
985 | "pk": 9,
986 | "fields": {
987 | "created": "2018-08-22T11:27:20.290Z",
988 | "modified": "2018-08-23T17:53:31.160Z",
989 | "author": [
990 | "tester6"
991 | ],
992 | "title": "what is gravitational force",
993 | "body": "the definition of gravitational force",
994 | "subject": 9,
995 | "grade": 10
996 | }
997 | },
998 | {
999 | "model": "buza.question",
1000 | "pk": 10,
1001 | "fields": {
1002 | "created": "2018-08-22T11:34:31.907Z",
1003 | "modified": "2018-08-23T17:53:22.357Z",
1004 | "author": [
1005 | "tester7"
1006 | ],
1007 | "title": "the formula of the gravitation force",
1008 | "body": "",
1009 | "subject": 9,
1010 | "grade": 10
1011 | }
1012 | },
1013 | {
1014 | "model": "buza.question",
1015 | "pk": 11,
1016 | "fields": {
1017 | "created": "2018-08-22T11:46:38.980Z",
1018 | "modified": "2018-08-23T17:53:02.755Z",
1019 | "author": [
1020 | "tester8"
1021 | ],
1022 | "title": "how do you calculate velocity",
1023 | "body": "how do you calculate velocity",
1024 | "subject": 9,
1025 | "grade": 10
1026 | }
1027 | },
1028 | {
1029 | "model": "buza.question",
1030 | "pk": 12,
1031 | "fields": {
1032 | "created": "2018-08-23T18:02:04.896Z",
1033 | "modified": "2018-08-23T18:02:04.896Z",
1034 | "author": [
1035 | "tester0"
1036 | ],
1037 | "title": "lediri ke eng",
1038 | "body": "mme o jesa ngwana",
1039 | "subject": 3,
1040 | "grade": 8
1041 | }
1042 | },
1043 | {
1044 | "model": "buza.question",
1045 | "pk": 13,
1046 | "fields": {
1047 | "created": "2018-08-24T07:03:26.578Z",
1048 | "modified": "2018-08-24T07:03:26.578Z",
1049 | "author": [
1050 | "tester9"
1051 | ],
1052 | "title": "What is the fomuler for distance?",
1053 | "body": "What is the fomuler for distance?",
1054 | "subject": 8,
1055 | "grade": 10
1056 | }
1057 | },
1058 | {
1059 | "model": "buza.question",
1060 | "pk": 14,
1061 | "fields": {
1062 | "created": "2018-08-24T07:11:59.418Z",
1063 | "modified": "2018-08-24T07:11:59.418Z",
1064 | "author": [
1065 | "tester10"
1066 | ],
1067 | "title": "how do you keep up with changing school topics",
1068 | "body": "goreng ga o tlhaloganya topic mo maths e be morutabana a fetola kgotsa a tla ka topic e e ntshwa?",
1069 | "subject": 8,
1070 | "grade": 11
1071 | }
1072 | },
1073 | {
1074 | "model": "buza.question",
1075 | "pk": 15,
1076 | "fields": {
1077 | "created": "2018-08-24T09:26:28.140Z",
1078 | "modified": "2018-08-24T09:26:46.895Z",
1079 | "author": [
1080 | "tester11"
1081 | ],
1082 | "title": "how to master poetry tests?",
1083 | "body": "what should i do to understand poetry test and the question",
1084 | "subject": 1,
1085 | "grade": 12
1086 | }
1087 | },
1088 | {
1089 | "model": "buza.answer",
1090 | "pk": 3,
1091 | "fields": {
1092 | "created": "2018-08-20T18:11:53.164Z",
1093 | "modified": "2018-08-20T18:11:53.164Z",
1094 | "author": [
1095 | "tester0"
1096 | ],
1097 | "question": 4,
1098 | "body": "The area of a triangle is the 1/2(heightXbase)\r\nSo in your problem that would be 1/2(5*16) which is 80.\r\nI hope that helps"
1099 | }
1100 | },
1101 | {
1102 | "model": "buza.answer",
1103 | "pk": 4,
1104 | "fields": {
1105 | "created": "2018-08-21T07:11:45.306Z",
1106 | "modified": "2018-08-21T07:12:27.797Z",
1107 | "author": [
1108 | "tester1"
1109 | ],
1110 | "question": 4,
1111 | "body": "1/2(base*height)"
1112 | }
1113 | },
1114 | {
1115 | "model": "buza.answer",
1116 | "pk": 5,
1117 | "fields": {
1118 | "created": "2018-08-21T07:17:55.917Z",
1119 | "modified": "2018-08-21T07:17:55.918Z",
1120 | "author": [
1121 | "tester2"
1122 | ],
1123 | "question": 5,
1124 | "body": "3.14*4^2"
1125 | }
1126 | },
1127 | {
1128 | "model": "buza.answer",
1129 | "pk": 6,
1130 | "fields": {
1131 | "created": "2018-08-21T07:28:56.610Z",
1132 | "modified": "2018-08-21T07:28:56.610Z",
1133 | "author": [
1134 | "tester3"
1135 | ],
1136 | "question": 5,
1137 | "body": "3.14*8=25.12"
1138 | }
1139 | },
1140 | {
1141 | "model": "buza.answer",
1142 | "pk": 7,
1143 | "fields": {
1144 | "created": "2018-08-22T11:18:27.473Z",
1145 | "modified": "2018-08-22T11:18:27.473Z",
1146 | "author": [
1147 | "tester5"
1148 | ],
1149 | "question": 6,
1150 | "body": "1/2(base)*height"
1151 | }
1152 | },
1153 | {
1154 | "model": "buza.answer",
1155 | "pk": 8,
1156 | "fields": {
1157 | "created": "2018-08-22T11:24:09.730Z",
1158 | "modified": "2018-08-22T11:24:26.786Z",
1159 | "author": [
1160 | "tester6"
1161 | ],
1162 | "question": 8,
1163 | "body": "it is the energy stored in an object"
1164 | }
1165 | },
1166 | {
1167 | "model": "buza.answer",
1168 | "pk": 9,
1169 | "fields": {
1170 | "created": "2018-08-22T11:35:48.932Z",
1171 | "modified": "2018-08-22T11:35:48.932Z",
1172 | "author": [
1173 | "tester7"
1174 | ],
1175 | "question": 6,
1176 | "body": "1/2 base*height"
1177 | }
1178 | },
1179 | {
1180 | "model": "buza.answer",
1181 | "pk": 10,
1182 | "fields": {
1183 | "created": "2018-08-22T11:41:13.718Z",
1184 | "modified": "2018-08-22T11:41:13.718Z",
1185 | "author": [
1186 | "tester8"
1187 | ],
1188 | "question": 4,
1189 | "body": "1/2*(5)*(16)"
1190 | }
1191 | },
1192 | {
1193 | "model": "buza.answer",
1194 | "pk": 11,
1195 | "fields": {
1196 | "created": "2018-08-24T06:57:01.066Z",
1197 | "modified": "2018-08-24T06:57:01.066Z",
1198 | "author": [
1199 | "tester9"
1200 | ],
1201 | "question": 7,
1202 | "body": "dictionary meaning.The actual meaning of the word."
1203 | }
1204 | },
1205 | {
1206 | "model": "buza.answer",
1207 | "pk": 12,
1208 | "fields": {
1209 | "created": "2018-08-24T09:20:25.886Z",
1210 | "modified": "2018-08-24T09:20:25.886Z",
1211 | "author": [
1212 | "tester11"
1213 | ],
1214 | "question": 12,
1215 | "body": "lediri ke lefoko le le dirang tiro\r\nin the sentence \"mme o jesa ngwana\"\r\nlediri \" jesa\"\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'"
1216 | }
1217 | },
1218 | {
1219 | "model": "buza.answer",
1220 | "pk": 13,
1221 | "fields": {
1222 | "created": "2018-08-24T09:34:19.994Z",
1223 | "modified": "2018-08-24T09:34:19.994Z",
1224 | "author": [
1225 | "tester12"
1226 | ],
1227 | "question": 14,
1228 | "body": "The thing is that teachers are there to keep their school learners up to date with the material the students need in order to be able to pass that particular date.So,you can make things easier for yourself by making sure that when ever you feel like you are getting lost in a topic,you actually go and ask for help from any person you feel comfortable asking questions with.Therefore it will be easy to inter-change between different topics."
1229 | }
1230 | }
1231 | ]
1232 |
--------------------------------------------------------------------------------
/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 |
3 | from django.forms import ModelForm
4 | from django.http import HttpResponse
5 | from django.test import TestCase
6 | from django.urls import reverse
7 |
8 | from buza import models
9 |
10 |
11 | class TestRegister(TestCase):
12 | """
13 | The `register` view should create users and log them in.
14 | """
15 | def setUp(self) -> None:
16 | self.path = reverse('register')
17 |
18 | def test_get(self) -> None:
19 | response = self.client.get(self.path)
20 | assert HTTPStatus.OK == response.status_code
21 | self.assertTemplateUsed(response, 'accounts/register.html')
22 | assert 'user_form' in response.context
23 |
24 | def test_get__authenticated(self) -> None:
25 | user: models.User = models.User.objects.create()
26 | self.client.force_login(user)
27 | response = self.client.get(self.path)
28 | assert HTTPStatus.OK == response.status_code
29 | self.assertTemplateUsed(response, 'accounts/register.html')
30 | assert 'user_form' in response.context
31 |
32 | def test_post__empty(self) -> None:
33 | """
34 | Test that when user submits an empty register from, the user is not created.
35 | """
36 | response: HttpResponse = self.client.post(self.path)
37 | assert HTTPStatus.OK == response.status_code
38 | assert self.assertTemplateUsed('accounts/register.html')
39 |
40 | form: ModelForm = response.context['user_form'] # noqa: E701
41 | assert [] == form.non_field_errors()
42 | assert {
43 | 'username': ['This field is required.'],
44 | 'password1': ['This field is required.'],
45 | 'password2': ['This field is required.'],
46 | } == form.errors
47 | assert not form.is_valid()
48 |
49 | def test_post__passwords_mismatch(self) -> None:
50 | response: HttpResponse = self.client.post(self.path, data=dict(
51 | username='buza-user-12',
52 | password1='password',
53 | password2='mismatch',
54 | ))
55 | assert HTTPStatus.OK == response.status_code
56 | assert self.assertTemplateUsed('accounts/register.html')
57 |
58 | form: ModelForm = response.context['user_form'] # noqa: E701
59 | assert [] == form.non_field_errors()
60 | assert {
61 | 'password2': ["The two password fields didn't match."],
62 | } == form.errors
63 | assert not form.is_valid()
64 |
65 | def test_post__valid_form(self) -> None:
66 | response = self.client.post(self.path, data=dict(
67 | username='buza-user-12',
68 | password1='secret',
69 | password2='secret',
70 | ))
71 | assert HTTPStatus.OK == response.status_code
72 | self.assertTemplateUsed('accounts/register_done.html')
73 | new_user: models.User = models.User.objects.get()
74 | assert new_user == response.context['new_user']
75 |
76 | assert {
77 | 'bio': None,
78 | 'date_joined': new_user.date_joined,
79 | 'email': '',
80 | 'first_name': '',
81 | 'grade': 7,
82 | 'id': new_user.pk,
83 | 'is_active': True,
84 | 'is_staff': False,
85 | 'is_superuser': False,
86 | 'last_login': None,
87 | 'last_name': '',
88 | 'password': new_user.password,
89 | 'phone': '',
90 | 'photo': '',
91 | 'school': None,
92 | 'school_address': None,
93 | 'username': 'buza-user-12',
94 | } == models.User.objects.filter(pk=new_user.pk).values().get()
95 |
96 |
97 | class TestUserUpdate(TestCase):
98 |
99 | def _authenticated_user(self) -> models.User:
100 | """
101 | Create and return an authenticated user.
102 | """
103 | user: models.User = models.User.objects.create()
104 | self.client.force_login(user)
105 | return user
106 |
107 | def test_get__anonymous(self) -> None:
108 | user: models.User = models.User.objects.create()
109 | response = self.client.get(reverse('user-update', kwargs=dict(pk=user.pk)))
110 | self.assertRedirects(response, f'/auth/login/?next=/users/{user.pk}/update/')
111 |
112 | def test_post__anonymous(self) -> None:
113 | user: models.User = models.User.objects.create()
114 | response = self.client.post(reverse('user-update', kwargs=dict(pk=user.pk)))
115 | self.assertRedirects(response, f'/auth/login/?next=/users/{user.pk}/update/')
116 |
117 | def test_get__authenticated(self) -> None:
118 | user = self._authenticated_user()
119 | response = self.client.get(reverse('user-update', kwargs=dict(pk=user.pk)))
120 | assert HTTPStatus.OK == response.status_code
121 | self.assertTemplateUsed(response, 'accounts/edit.html')
122 |
123 | assert 'form' in response.context
124 | form: ModelForm = response.context['form'] # noqa: E701
125 | assert user == form.instance
126 | assert not form.is_bound
127 |
128 | def test_post__empty(self) -> None:
129 | user = self._authenticated_user()
130 | response = self.client.post(reverse('user-update', kwargs=dict(pk=user.pk)))
131 | self.assertRedirects(response, reverse('user-detail', kwargs=dict(pk=user.pk)))
132 |
133 | def test_post__blank(self) -> None:
134 | user = self._authenticated_user()
135 | response = self.client.post(
136 | path=reverse('user-update', kwargs=dict(pk=user.pk)),
137 | data={
138 | 'email': '',
139 | 'phone': '',
140 | 'photo': '',
141 | 'first_name': '',
142 | 'last_name': '',
143 | 'school': '',
144 | 'school_address': '',
145 | 'grade': '',
146 | 'bio': '',
147 | },
148 | )
149 | self.assertRedirects(response, reverse('user-detail', kwargs=dict(pk=user.pk)))
150 |
151 |
152 | class TestUserDetail(TestCase):
153 |
154 | def test_not_found(self) -> None:
155 | response = self.client.get(reverse('user-detail', kwargs=dict(pk=404)))
156 | assert HTTPStatus.NOT_FOUND == response.status_code
157 |
158 | def test_get(self) -> None:
159 | user = models.User.objects.create(
160 | first_name='Test',
161 | last_name='User',
162 | photo='example.jpeg',
163 | email='tester@example.com',
164 | bio='Example bio.',
165 | )
166 | response = self.client.get(reverse('user-detail', kwargs=dict(pk=user.pk)))
167 | assert HTTPStatus.OK == response.status_code
168 | self.assertTemplateUsed(response, 'buza/user_detail.html')
169 |
170 | self.assertContains(response, 'Test User', count=2)
171 | self.assertContains(response, 'Example bio.', count=1)
172 |
173 | def test_get__user_with_question(self) -> None:
174 | user = models.User.objects.create(
175 | first_name='Test',
176 | last_name='User',
177 | username='newuser',
178 | email='tester@example.com',
179 | bio='Example bio.',
180 | )
181 | subject: models.Subject = models.Subject.objects.create(title="maths")
182 | question = models.Question.objects.create(
183 | author=user,
184 | title='Example question?',
185 | body='A question.',
186 | subject=subject,
187 | grade=7,
188 | )
189 | response = self.client.get(reverse('user-detail', kwargs=dict(pk=user.pk)))
190 | assert HTTPStatus.OK == response.status_code
191 | self.assertTemplateUsed(response, 'buza/user_detail.html')
192 |
193 | self.assertContains(response, user.get_full_name(), count=2)
194 | self.assertContains(response, "@" + user.username, count=2)
195 | self.assertContains(response, user.bio, count=1)
196 | self.assertContains(response, question.title, count=1)
197 | self.assertContains(response, question.subject, count=1)
198 |
199 |
200 | class TestQuestionDetail(TestCase):
201 |
202 | def test_not_found(self) -> None:
203 | response = self.client.get(reverse('user-detail', kwargs=dict(pk=404)))
204 | assert HTTPStatus.NOT_FOUND == response.status_code
205 |
206 | def test_get(self) -> None:
207 | user = models.User.objects.create(username="username")
208 | user2 = models.User.objects.create(username="username2")
209 | subject: models.Subject = models.Subject.objects.create(title="maths")
210 | question = models.Question.objects.create(
211 | author=user,
212 | title='Example question?',
213 | body='A question.',
214 | subject=subject,
215 | grade=7,
216 | )
217 | answer: models.Answer = models.Answer.objects.create(
218 | body='An answer',
219 | question=question,
220 | author=user2,
221 | )
222 | path = reverse('question-detail', kwargs=dict(pk=question.pk))
223 | response = self.client.get(path)
224 | assert HTTPStatus.OK == response.status_code
225 | self.assertTemplateUsed(response, 'buza/question_detail.html')
226 |
227 | assert question == response.context['question']
228 | self.assertContains(response, question.title, count=2)
229 | self.assertContains(response, question.body, count=0)
230 | self.assertContains(response, subject.title, count=1)
231 | self.assertContains(response, answer.author, count=1)
232 | self.assertContains(response, "now", count=2)
233 | self.assertContains(response, "answers")
234 | self.assertNotContains(response, "edit answer")
235 |
236 | def test_get__authenticated(self) -> None:
237 | user = models.User.objects.create(username="username")
238 | subject: models.Subject = models.Subject.objects.create(title="maths")
239 | self.client.force_login(user)
240 | question = models.Question.objects.create(
241 | author=user,
242 | title='Example question?',
243 | body='A question.',
244 | subject=subject,
245 | grade=7,
246 | )
247 | path = reverse('question-detail', kwargs=dict(pk=question.pk))
248 | response = self.client.get(path)
249 | assert HTTPStatus.OK == response.status_code
250 | self.assertTemplateUsed(response, 'buza/question_detail.html')
251 |
252 | assert question == response.context['question']
253 | self.assertContains(response, question.title, count=2)
254 | self.assertContains(response, question.body, count=0)
255 | self.assertContains(response, subject.title, count=1)
256 | self.assertContains(response, "now", count=1)
257 | self.assertContains(response, "answers")
258 | print(response.content)
259 | self.assertContains(response, "Edit question")
260 | self.assertContains(response, "No answers yet")
261 |
262 |
263 | class TestQuestionList(TestCase):
264 | def setUp(self) -> None:
265 | self.user: models.User = models.User.objects.create()
266 | self.maths: models.Subject = models.Subject.objects.create(
267 | title='Mathematics',
268 | short_title='maths',
269 | )
270 | self.biology: models.Subject = models.Subject.objects.create(
271 | title='Biology',
272 | short_title='bio',
273 | )
274 | self.question: models.Question = models.Question.objects.create(
275 | title="this is a queston",
276 | author=self.user,
277 | subject=self.maths,
278 | grade=7,
279 | )
280 | self.answer: models.Answer = models.Answer.objects.create(
281 | question=self.question,
282 | author=self.user,
283 | )
284 | self.path = reverse('question-list')
285 |
286 | def test_get__one_question(self) -> None:
287 | """
288 | Test Question list view with one question
289 | """
290 | response = self.client.get(reverse('question-list'))
291 | assert HTTPStatus.OK == response.status_code
292 | self.assertTemplateUsed(response, 'buza/question_list.html')
293 |
294 | self.assertNotContains(response, "Follow")
295 | self.assertNotContains(response, "Following")
296 | self.assertQuerysetEqual(response.context['subject_list'], [
297 | '
',
298 | '',
299 | ])
300 | self.assertContains(response, self.maths.title, count=2)
301 | self.assertContains(response, self.biology.title, count=1)
302 | self.assertContains(response, "@" + self.user.username)
303 | self.assertContains(response, self.question.title)
304 | self.assertContains(response, "now", count=1)
305 | self.assertContains(response, self.answer.body)
306 |
307 | def test_get__unauthenticated(self) -> None:
308 | """
309 | Unauthenticated users can view but not follow subjects
310 | :return:
311 | """
312 | response = self.client.get(self.path)
313 | assert HTTPStatus.OK == response.status_code
314 | self.assertNotContains(response, "Follow")
315 | self.assertNotContains(response, "Following")
316 | self.assertContains(response, self.maths.title, count=2)
317 | self.assertContains(response, self.biology.title, count=1)
318 | self.assertContains(response, "@" + self.user.username)
319 | self.assertContains(response, self.answer.body)
320 | # test the humanizer
321 | self.assertContains(response, "now")
322 |
323 | # Listed by title.
324 | self.assertQuerysetEqual(response.context['subject_list'], [
325 | '',
326 | '',
327 | ])
328 |
329 | def test_get__no_followed_subjects(self) -> None:
330 | """
331 | Logged in users can view the list of subjects and follow them
332 | """
333 | self.client.force_login(self.user)
334 | response = self.client.get(self.path)
335 | assert HTTPStatus.OK == response.status_code
336 | self.assertContains(response, "follow")
337 | self.assertContains(response, "★")
338 | self.assertContains(response, "following", 0)
339 | self.assertContains(response, self.maths.title, count=2)
340 | self.assertContains(response, self.biology.title, count=1)
341 | self.assertContains(response, "@" + self.user.username)
342 | self.assertContains(response, self.question.title)
343 | self.assertContains(response, "now")
344 | self.assertContains(response, self.answer.body)
345 |
346 | # Listed by title.
347 | self.assertQuerysetEqual(response.context['subject_list'], [
348 | '',
349 | '',
350 | ])
351 |
352 | def test_get__followed_subjects(self) -> None:
353 | """
354 | When follow a question, the UI updates
355 | :return:
356 | """
357 | self.client.force_login(self.user)
358 | self.user.subjects.add(self.maths)
359 | response = self.client.get(self.path)
360 | self.assertTemplateUsed(response, 'buza/question_list.html')
361 | assert HTTPStatus.OK == response.status_code
362 | self.assertContains(response, "following")
363 | self.assertContains(response, "follow")
364 | self.assertContains(response, "@" + self.user.username)
365 | self.assertContains(response, self.question.title)
366 | self.assertContains(response, "now")
367 | self.assertContains(response, self.answer.body)
368 | # Maths (followed) listed first.
369 | self.assertQuerysetEqual(response.context['subject_list'], [
370 | '',
371 | '',
372 | ])
373 |
374 | def test_get__long_subject_names(self) -> None:
375 | """
376 | When follow a question, the UI updates
377 | :return:
378 | """
379 | self.client.force_login(self.user)
380 | self.user.subjects.add(self.maths)
381 |
382 | ems: models.Subject = models.Subject.objects.create(
383 | title='Economics and Management Sciences',
384 | short_title='EMS',
385 | )
386 | response = self.client.get(self.path)
387 | self.assertTemplateUsed(response, 'buza/question_list.html')
388 | assert HTTPStatus.OK == response.status_code
389 | # test that EMS is truncated
390 | self.assertNotContains(response, ems.title)
391 | self.assertContains(response, 'Economics and Manage...')
392 |
393 | # regular question list tests
394 | self.assertContains(response, "@" + self.user.username)
395 | self.assertContains(response, self.question.title)
396 | self.assertContains(response, "now")
397 |
398 | def test_post_unauthenticated(self) -> None:
399 | """Redirect unauthenticated user's posts """
400 |
401 | response: HttpResponse = self.client.post(self.path, data={
402 | 'follow-subject': self.maths.pk,
403 | })
404 |
405 | assert HTTPStatus.FOUND == response.status_code
406 | self.assertRedirects(response, f'/auth/login/?next=/')
407 |
408 | def test_post__follow_subject(self) -> None:
409 | """Redirect unauthenticated user's posts """
410 | self.client.force_login(self.user)
411 | path = reverse('subject-detail', kwargs=dict(pk=self.maths.pk))
412 | response: HttpResponse = self.client.post(path, data={
413 | 'follow-subject': self.maths.pk,
414 | })
415 |
416 | assert HTTPStatus.FOUND == response.status_code
417 | self.assertRedirects(response, f'/subjects/{self.maths.pk}/')
418 | self.assertEqual(self.user.subjects.all().count(), 1)
419 |
420 | response = self.client.get(response.url)
421 | self.assertContains(response, "following")
422 | self.assertContains(response, "follow")
423 | # Maths (followed) listed first.
424 | self.assertQuerysetEqual(response.context['subject_list'], [
425 | '',
426 | '',
427 | ])
428 |
429 | self.assertContains(response, "@" + self.user.username)
430 | self.assertContains(response, self.question.title)
431 | self.assertContains(response, "now")
432 | self.assertContains(response, self.answer.body)
433 |
434 | def test_post__unfollow_in_subjectlist(self) -> None:
435 | """Redirect unauthenticated user's posts """
436 | self.client.force_login(self.user)
437 |
438 | # follow and unfollow a subject
439 | path = reverse('subject-detail', kwargs=dict(pk=self.maths.pk))
440 | self.client.post(path, data={
441 | 'follow-subject': self.maths.pk,
442 |
443 | })
444 | response: HttpResponse = self.client.post(self.path, data={
445 | 'following-subject': self.maths.pk,
446 |
447 | })
448 |
449 | assert HTTPStatus.FOUND == response.status_code
450 | self.assertRedirects(response, f'/subjects/{self.maths.pk}/')
451 | self.assertEqual(self.user.subjects.all().count(), 0)
452 |
453 | response = self.client.get(response.url)
454 | self.assertNotContains(response, "following")
455 | self.assertContains(response, "follow")
456 | # Maths (followed) listed first.
457 | self.assertQuerysetEqual(response.context['subject_list'], [
458 | '',
459 | '',
460 | ])
461 |
462 | self.assertContains(response, "@" + self.user.username)
463 | self.assertContains(response, self.question.title)
464 | self.assertContains(response, "now")
465 | self.assertContains(response, self.answer.body)
466 |
467 |
468 | class TestQuestionCreate(TestCase):
469 |
470 | def setUp(self) -> None:
471 | self.subject: models.Subject = models.Subject.objects.create(
472 | title='mathematics',
473 | short_title='maths',
474 | )
475 | self.user: models.User = models.User.objects.create()
476 |
477 | def test_get__anonymous(self) -> None:
478 | response = self.client.get(reverse(
479 | 'question-create',
480 | kwargs=dict(subject_pk=self.subject.pk),
481 | ))
482 | self.assertRedirects(
483 | response,
484 | f'/auth/login/?next=/questions/{self.subject.pk}/ask/',
485 | )
486 |
487 | def test_post__anonymous(self) -> None:
488 | response = self.client.post(reverse(
489 | 'question-create',
490 | kwargs=dict(subject_pk=self.subject.pk),
491 | ))
492 | self.assertRedirects(
493 | response,
494 | f'/auth/login/?next=/questions/{self.subject.pk}/ask/',
495 | )
496 |
497 | def test_get__authenticated(self) -> None:
498 | self.client.force_login(self.user)
499 | response = self.client.get(reverse(
500 | 'question-create',
501 | kwargs=dict(subject_pk=self.subject.pk),
502 | ))
503 | assert HTTPStatus.OK == response.status_code
504 | self.assertTemplateUsed(response, 'buza/question_form.html')
505 | self.assertContains(response, 'Question Summary', count=1)
506 | self.assertContains(
507 | response,
508 | 'Give a detailed description of your question',
509 | count=1,
510 | )
511 |
512 | def test_post__empty(self) -> None:
513 | self.client.force_login(self.user)
514 | response = self.client.post(reverse(
515 | 'question-create',
516 | kwargs=dict(subject_pk=self.subject.pk),
517 | ))
518 | assert HTTPStatus.OK == response.status_code
519 |
520 | assert 'form' in response.context
521 | form: ModelForm = response.context['form'] # noqa: E701
522 | assert [] == form.non_field_errors()
523 | assert {
524 | 'title': ['This field is required.'],
525 | } == form.errors
526 | assert not form.is_valid()
527 |
528 | def test_post__success(self) -> None:
529 | """
530 | Question post redirects to question view
531 | """
532 | self.client.force_login(self.user)
533 | response = self.client.post(reverse(
534 | 'question-create',
535 | kwargs=dict(subject_pk=self.subject.pk)),
536 | data=dict(
537 | title='This is a title',
538 | body='This is a body',
539 | grade=7,
540 | ))
541 | question: models.Question = models.Question.objects.get()
542 | assert {
543 | 'author_id': self.user.pk,
544 | 'body': 'This is a body',
545 | 'created': question.created,
546 | 'id': question.pk,
547 | 'modified': question.modified,
548 | 'title': 'This is a title',
549 | 'subject_id': self.subject.pk,
550 | 'grade': question.grade,
551 | } == models.Question.objects.filter(pk=question.pk).values().get()
552 | self.assertRedirects(response, f'/questions/{question.pk}/')
553 |
554 |
555 | class TestQuestionUpdate(TestCase):
556 | def setUp(self) -> None:
557 | super().setUp()
558 | self.author = models.User.objects.create(username='author')
559 | self.subject: models.Subject = models.Subject.objects.create(title="maths")
560 | self.other_user = models.User.objects.create(username='otheruser')
561 | self.question = models.Question.objects.create(
562 | author=self.author,
563 | title='question',
564 | subject=self.subject,
565 | grade=7,
566 | )
567 |
568 | def test_get__anonymous(self) -> None:
569 | response = self.client.get(
570 | reverse('question-update', kwargs=dict(pk=self.question.pk)),
571 | )
572 | self.assertRedirects(
573 | response,
574 | f'/auth/login/?next=/questions/{self.question.pk}/edit/',
575 | )
576 |
577 | def test_post__anonymous(self) -> None:
578 | response = self.client.get(
579 | reverse('question-update', kwargs=dict(pk=self.question.pk)),
580 | )
581 | self.assertRedirects(
582 | response,
583 | f'/auth/login/?next=/questions/{self.question.pk}/edit/',
584 | )
585 |
586 | def test_get__not_author(self) -> None:
587 | """
588 | Users can only edit questions they own
589 | """
590 | self.client.force_login(self.other_user)
591 | response = self.client.get(
592 | reverse('question-update', kwargs=dict(pk=self.question.pk)),
593 | )
594 | assert HTTPStatus.FORBIDDEN == response.status_code
595 |
596 | def test_post__not_author(self) -> None:
597 | """
598 | Only authors can post questions changes
599 | """
600 | self.client.force_login(self.other_user)
601 | response = self.client.post(reverse(
602 | 'question-update',
603 | kwargs=dict(pk=self.question.pk)), data=dict(
604 | title='This is a title updated',
605 | body='This is an updated body',
606 | ))
607 | assert HTTPStatus.FORBIDDEN == response.status_code
608 |
609 | def test_post__author_update(self) -> None:
610 | """
611 | Question update allows author to update the question
612 |
613 | """
614 | self.client.force_login(self.author)
615 | path = reverse('question-update', kwargs=dict(pk=self.question.pk))
616 | response = self.client.post(path, data=dict(
617 | title='This is a title updated',
618 | body='This is an updated body',
619 | subject=self.subject.pk,
620 | grade=7,
621 | ))
622 |
623 | question: models.Question = models.Question.objects.get()
624 | assert {
625 | 'author_id': self.author.pk,
626 | 'body': 'This is an updated body',
627 | 'created': question.created,
628 | 'id': question.pk,
629 | 'modified': question.modified,
630 | 'title': 'This is a title updated',
631 | 'subject_id': question.subject.pk,
632 | 'grade': question.grade,
633 | } == models.Question.objects.filter(pk=question.pk).values().get()
634 | self.assertRedirects(response, f'/questions/{self.question.pk}/')
635 |
636 |
637 | class TestAnswerCreate(TestCase):
638 |
639 | def setUp(self) -> None:
640 | super().setUp()
641 | self.user = models.User.objects.create()
642 | self.subject: models.Subject = models.Subject.objects.create(title="maths")
643 | self.question = models.Question.objects.create(
644 | author=self.user,
645 | title='question',
646 | subject=self.subject,
647 | grade=7,
648 | )
649 | self.path = reverse('answer-create', kwargs=dict(question_pk=self.question.pk))
650 |
651 | def test__not_found(self) -> None:
652 | path = reverse('answer-create', kwargs=dict(question_pk=404))
653 | # Anonymous:
654 | assert HTTPStatus.NOT_FOUND == self.client.get(path).status_code
655 | assert HTTPStatus.NOT_FOUND == self.client.post(path).status_code
656 | # Authenticated:
657 | self.client.force_login(self.user)
658 | assert HTTPStatus.NOT_FOUND == self.client.get(path).status_code
659 | assert HTTPStatus.NOT_FOUND == self.client.post(path).status_code
660 |
661 | def test___anonymous(self) -> None:
662 | """
663 | Test that when an unauthenticated user tries to answer a question
664 | they are redirected to the home page
665 | """
666 | expected_url = f'/auth/login/?next=/questions/{self.question.pk}/answer/'
667 | self.assertRedirects(self.client.get(self.path), expected_url)
668 | self.assertRedirects(self.client.post(self.path), expected_url)
669 |
670 | def test_get__authenticated(self) -> None:
671 | self.client.force_login(self.user)
672 | response: HttpResponse = self.client.get(self.path)
673 | assert HTTPStatus.OK == response.status_code
674 | assert self.assertTemplateUsed('buza/question_form.html')
675 | assert self.question == response.context['question']
676 |
677 | def test_post__empty(self) -> None:
678 | """
679 | Test that when an authenticated user submits an empty answer
680 | the answer is not posted
681 | """
682 | self.client.force_login(self.user)
683 | response: HttpResponse = self.client.post(self.path)
684 | assert HTTPStatus.OK == response.status_code
685 | assert self.assertTemplateUsed('buza/question_form.html')
686 | assert self.question == response.context['question']
687 |
688 | form: ModelForm = response.context['form'] # noqa: E701
689 | assert [] == form.non_field_errors()
690 | assert {'body': ['This field is required.']} == form.errors
691 | assert not form.is_valid()
692 |
693 | def test_post__valid(self) -> None:
694 | """
695 | Test that when an authenticated user submits a valid answer
696 | the answer is posted
697 | """
698 | self.client.force_login(self.user)
699 | response: HttpResponse = self.client.post(self.path, data={
700 | 'body': 'An example answer',
701 | })
702 | answer: models.Answer = models.Answer.objects.get()
703 | assert {
704 | 'author_id': self.user.pk,
705 | 'created': answer.created,
706 | 'id': answer.pk,
707 | 'modified': answer.modified,
708 | 'body': 'An example answer',
709 | 'question_id': 1,
710 | } == models.Answer.objects.filter(pk=answer.pk).values().get()
711 | self.assertRedirects(response, f'/questions/{answer.question.pk}/')
712 |
713 |
714 | class TestAnswerUpdate(TestCase):
715 | def setUp(self) -> None:
716 | super().setUp()
717 | self.author: models.User = models.User.objects.create()
718 | self.answer_author: models.User = \
719 | models.User.objects.create(username='answer_author')
720 | self.subject: models.Subject = models.Subject.objects.create(title="maths")
721 | self.question: models.Question = models.Question.objects.create(
722 | author=self.author,
723 | title='question',
724 | subject=self.subject,
725 | grade=12,
726 | )
727 | self.answer: models.Answer = models.Answer.objects.create(
728 | author=self.answer_author,
729 | body='This is an answer',
730 | question=self.question,
731 | )
732 | self.path = reverse('answer-update', kwargs=dict(pk=self.answer.pk))
733 |
734 | def test_get__anonymous(self) -> None:
735 | response = self.client.get(self.path)
736 | self.assertRedirects(
737 | response,
738 | f'/auth/login/?next=/answers/{self.answer.pk}/edit/',
739 | )
740 |
741 | def test_get__authenticated(self) -> None:
742 | self.client.force_login(self.answer_author)
743 | response = self.client.get(self.path)
744 | self.assertTemplateUsed(response, 'buza/answer_form.html')
745 | assert self.question == response.context['question']
746 | assert self.answer == response.context['answer']
747 |
748 | def test_post__anonymous(self) -> None:
749 | response = self.client.post(self.path)
750 | self.assertRedirects(
751 | response,
752 | f'/auth/login/?next=/answers/{self.answer.pk}/edit/',
753 | )
754 |
755 | def test_post__authenticated(self) -> None:
756 | self.client.force_login(self.answer_author)
757 | response = self.client.post(
758 | self.path,
759 | data=dict(
760 | body='This is an updated answer',
761 | ),
762 | )
763 | assert \
764 | 'This is an updated answer' == \
765 | models.Answer.objects.filter(pk=self.answer.pk).get().body
766 | self.assertRedirects(response, f'/questions/{self.question.pk}/')
767 |
768 | def test_post__authenticated__not_owner(self) -> None:
769 | """
770 | Only the question authors are allowed to edit the question
771 | """
772 | self.client.force_login(self.author)
773 | response = self.client.post(self.path, data=dict(
774 | body='This is an updated answer',
775 | ))
776 | assert HTTPStatus.FORBIDDEN == response.status_code
777 |
778 |
779 | class TestSubjectDetails(TestCase):
780 |
781 | def setUp(self) -> None:
782 | self.user: models.User = models.User.objects.create()
783 | self.maths: models.Subject = models.Subject.objects.create(
784 | title="Mathematics",
785 | description="the study of numbers",
786 | )
787 |
788 | self.biology: models.Subject = models.Subject.objects.create(
789 | title='Biology',
790 | short_title='bio',
791 | )
792 | self.question: models.Question = models.Question.objects.create(
793 | author=self.user,
794 | title='Example question?',
795 | body='A question.',
796 | subject=self.maths,
797 | grade=7,
798 | )
799 | self.path = reverse('subject-detail', kwargs=dict(pk=self.maths.pk))
800 |
801 | def test_not_found(self) -> None:
802 | response = self.client.get(reverse('subject-detail', kwargs=dict(pk=404)))
803 | assert HTTPStatus.NOT_FOUND == response.status_code
804 | self.assertTemplateUsed(response, '404.html')
805 |
806 | def test_get_authenticated(self) -> None:
807 | response = self.client.get(self.path)
808 | assert HTTPStatus.OK == response.status_code
809 | self.assertTemplateUsed(response, 'buza/subject_detail.html')
810 |
811 | self.assertContains(response, self.maths.title)
812 | self.assertContains(response, "Ask New Question")
813 | self.assertContains(response, self.biology.title, count=1)
814 | self.assertContains(response, self.question.title, count=1)
815 | self.assertNotContains(response, "Follow")
816 | self.assertNotContains(response, "Following")
817 | # Listed subjects by title.
818 | self.assertQuerysetEqual(response.context['subject_list'], [
819 | '',
820 | '',
821 | ])
822 |
823 | def test_get__authenticated__subject_short_title(self) -> None:
824 | self.maths: models.Subject = models.Subject.objects.create(
825 | title="mathematics",
826 | short_title="maths",
827 | description="the study of numbers",
828 | )
829 | self.client.force_login(self.user)
830 | path = reverse('subject-detail', kwargs=dict(pk=self.maths.pk))
831 | response = self.client.get(path)
832 |
833 | assert HTTPStatus.OK == response.status_code
834 | self.assertContains(response, self.maths.title)
835 | self.assertContains(
836 | response,
837 | "Ask New " + self.maths.short_title + " Question",
838 | )
839 |
840 | def test_post_unauthenticated(self) -> None:
841 | """Redirect unauthenticated user's posts """
842 |
843 | response: HttpResponse = self.client.post(self.path, data={
844 | 'follow-subject': self.maths.pk,
845 | })
846 |
847 | assert HTTPStatus.FOUND == response.status_code
848 | self.assertRedirects(response, f'/auth/login/?next=/subjects/{self.maths.pk}/')
849 |
850 | def test_post__follow_subject(self) -> None:
851 | """Redirect unauthenticated user's posts """
852 | self.client.force_login(self.user)
853 | response: HttpResponse = self.client.post(self.path, data={
854 | 'follow-subject': self.maths.pk,
855 |
856 | })
857 |
858 | assert HTTPStatus.FOUND == response.status_code
859 | self.assertRedirects(response, f'/subjects/{self.maths.pk}/')
860 | self.assertEqual(self.user.subjects.all().count(), 1)
861 |
862 | response = self.client.get(response.url)
863 | self.assertContains(response, "following")
864 | self.assertContains(response, "follow")
865 | # Maths (followed) listed first.
866 | self.assertContains(response, self.maths.title, count=3)
867 | self.assertQuerysetEqual(response.context['subject_list'], [
868 | '',
869 | '',
870 | ])
871 |
872 | def test_get__no_followed_subjects(self) -> None:
873 | """
874 | Test subject list and question list
875 | """
876 | self.client.force_login(self.user)
877 | response = self.client.get(self.path)
878 |
879 | assert HTTPStatus.OK == response.status_code
880 | # Question list and button
881 | self.assertContains(response, "Ask New Question")
882 | self.assertContains(response, self.question.title, count=1)
883 | # Subjects listing
884 | self.assertContains(response, "follow")
885 | self.assertContains(response, "★")
886 | self.assertContains(response, "following", 0)
887 | self.assertContains(response, self.maths.title, count=3)
888 | self.assertContains(response, self.biology.title, count=1)
889 |
890 | # Listed subjects by title.
891 | self.assertQuerysetEqual(response.context['subject_list'], [
892 | '',
893 | '',
894 | ])
895 |
896 | def test_get__followed_subjects(self) -> None:
897 | """
898 | When follow a question, the UI updates
899 | :return:
900 | """
901 | self.client.force_login(self.user)
902 | self.user.subjects.add(self.maths)
903 | response = self.client.get(self.path)
904 | self.assertTemplateUsed(response, 'buza/subject_detail.html')
905 | assert HTTPStatus.OK == response.status_code
906 | self.assertContains(response, "following")
907 | self.assertContains(response, "follow")
908 |
909 | # Maths (followed) listed first.
910 | self.assertQuerysetEqual(response.context['subject_list'], [
911 | '',
912 | '',
913 | ])
914 |
915 | def test_post__unfollow_subject(self) -> None:
916 | """Redirect unauthenticated user's posts """
917 | self.client.force_login(self.user)
918 |
919 | # follow and unfollow a subject
920 | self.client.post(self.path, data={
921 | 'follow-subject': self.maths.pk,
922 |
923 | })
924 | response: HttpResponse = self.client.post(self.path, data={
925 | 'following-subject': self.maths.pk,
926 |
927 | })
928 |
929 | assert HTTPStatus.FOUND == response.status_code
930 | self.assertRedirects(response, f'/subjects/{self.maths.pk}/')
931 | self.assertEqual(self.user.subjects.all().count(), 0)
932 |
933 | response = self.client.get(response.url)
934 | self.assertNotContains(response, "following")
935 | self.assertContains(response, "follow")
936 | # Maths (followed) listed first.
937 | self.assertQuerysetEqual(response.context['subject_list'], [
938 | '',
939 | '',
940 | ])
941 |
942 |
943 | class Test404PageNotFound(TestCase):
944 |
945 | def test_url_not_found(self):
946 | response = self.client.get('404/not-found/test')
947 | self.assertTemplateUsed(response, '404.html')
948 | self.assertContains(
949 | response,
950 | 'We could not find the page you were looking for',
951 | status_code=HTTPStatus.NOT_FOUND,
952 | )
953 | self.assertContains(response, 'Take me home', status_code=HTTPStatus.NOT_FOUND)
954 |
955 |
956 | class TestPrivacyPolicy(TestCase):
957 |
958 | def test_privacy_policy(self) -> None:
959 | response = self.client.get(reverse("privacy-policy"))
960 | self.assertTemplateUsed(response, "accounts/privacy_policy.html")
961 | self.assertContains(
962 | response,
963 | "Privacy Policy for",
964 | )
965 |
966 |
967 | class TestTermsOfService(TestCase):
968 |
969 | def test_privacy_policy(self) -> None:
970 | response = self.client.get(reverse("terms-of-service"))
971 | self.assertTemplateUsed(response, "accounts/terms_of_service.html")
972 | self.assertContains(
973 | response,
974 | "Welcome to Buza Answers",
975 | )
976 |
977 |
978 | class TestHomagePageView(TestCase):
979 |
980 | def test__get__unauthenticated(self) -> None:
981 | """Unauthenticated users are directed to the login page
982 | """
983 | response = self.client.get(reverse('home'))
984 | expected_url = f'/auth/login/'
985 | self.assertRedirects(response, expected_url)
986 |
987 | def test__get__authenticated(self) -> None:
988 | """Authenticated users are directed to their profile
989 | """
990 | self.user: models.User = models.User.objects.create()
991 | self.client.force_login(self.user)
992 | response = self.client.get(reverse('home'))
993 | expected_url = f'/users/{self.user.pk}/'
994 | self.assertRedirects(response, expected_url)
995 |
--------------------------------------------------------------------------------