aboutsummaryrefslogtreecommitdiffstats
path: root/ctrack
diff options
context:
space:
mode:
authorMatthew Lemon <lemon@matthewlemon.com>2020-01-19 15:57:06 +0000
committerMatthew Lemon <lemon@matthewlemon.com>2020-01-19 15:57:06 +0000
commit9d76a3c52b8310726ec09e0262813f0438c21df6 (patch)
tree4acf47dce6c3aa75f8ad7c5cb56fe6486c2d64a7 /ctrack
init commit - from cookiecutter
Diffstat (limited to 'ctrack')
-rw-r--r--ctrack/__init__.py7
-rw-r--r--ctrack/conftest.py20
-rw-r--r--ctrack/contrib/__init__.py5
-rw-r--r--ctrack/contrib/sites/__init__.py5
-rw-r--r--ctrack/contrib/sites/migrations/0001_initial.py42
-rw-r--r--ctrack/contrib/sites/migrations/0002_alter_domain_unique.py20
-rw-r--r--ctrack/contrib/sites/migrations/0003_set_site_domain_and_name.py34
-rw-r--r--ctrack/contrib/sites/migrations/__init__.py5
-rw-r--r--ctrack/static/css/project.css13
-rw-r--r--ctrack/static/fonts/.gitkeep0
-rw-r--r--ctrack/static/images/favicons/favicon.icobin0 -> 8348 bytes
-rw-r--r--ctrack/static/js/project.js1
-rw-r--r--ctrack/static/sass/custom_bootstrap_vars.scss0
-rw-r--r--ctrack/static/sass/project.scss37
-rw-r--r--ctrack/templates/403.html9
-rw-r--r--ctrack/templates/404.html9
-rw-r--r--ctrack/templates/500.html13
-rw-r--r--ctrack/templates/account/account_inactive.html12
-rw-r--r--ctrack/templates/account/base.html10
-rw-r--r--ctrack/templates/account/email.html80
-rw-r--r--ctrack/templates/account/email_confirm.html32
-rw-r--r--ctrack/templates/account/login.html48
-rw-r--r--ctrack/templates/account/logout.html22
-rw-r--r--ctrack/templates/account/password_change.html17
-rw-r--r--ctrack/templates/account/password_reset.html26
-rw-r--r--ctrack/templates/account/password_reset_done.html17
-rw-r--r--ctrack/templates/account/password_reset_from_key.html25
-rw-r--r--ctrack/templates/account/password_reset_from_key_done.html10
-rw-r--r--ctrack/templates/account/password_set.html17
-rw-r--r--ctrack/templates/account/signup.html23
-rw-r--r--ctrack/templates/account/signup_closed.html12
-rw-r--r--ctrack/templates/account/verification_sent.html13
-rw-r--r--ctrack/templates/account/verified_email_required.html24
-rw-r--r--ctrack/templates/base.html114
-rw-r--r--ctrack/templates/pages/about.html1
-rw-r--r--ctrack/templates/pages/home.html1
-rw-r--r--ctrack/templates/users/user_detail.html36
-rw-r--r--ctrack/templates/users/user_form.html17
-rw-r--r--ctrack/users/__init__.py0
-rw-r--r--ctrack/users/adapters.py16
-rw-r--r--ctrack/users/admin.py17
-rw-r--r--ctrack/users/apps.py13
-rw-r--r--ctrack/users/forms.py30
-rw-r--r--ctrack/users/migrations/0001_initial.py132
-rw-r--r--ctrack/users/migrations/__init__.py0
-rw-r--r--ctrack/users/models.py14
-rw-r--r--ctrack/users/tests/__init__.py0
-rw-r--r--ctrack/users/tests/factories.py27
-rw-r--r--ctrack/users/tests/test_forms.py40
-rw-r--r--ctrack/users/tests/test_models.py9
-rw-r--r--ctrack/users/tests/test_urls.py24
-rw-r--r--ctrack/users/tests/test_views.py46
-rw-r--r--ctrack/users/urls.py14
-rw-r--r--ctrack/users/views.py50
-rw-r--r--ctrack/utils/__init__.py0
-rw-r--r--ctrack/utils/context_processors.py5
56 files changed, 1214 insertions, 0 deletions
diff --git a/ctrack/__init__.py b/ctrack/__init__.py
new file mode 100644
index 0000000..e1d8615
--- /dev/null
+++ b/ctrack/__init__.py
@@ -0,0 +1,7 @@
+__version__ = "0.1.0"
+__version_info__ = tuple(
+ [
+ int(num) if num.isdigit() else num
+ for num in __version__.replace("-", ".", 1).split(".")
+ ]
+)
diff --git a/ctrack/conftest.py b/ctrack/conftest.py
new file mode 100644
index 0000000..b994f24
--- /dev/null
+++ b/ctrack/conftest.py
@@ -0,0 +1,20 @@
+import pytest
+from django.test import RequestFactory
+
+from ctrack.users.models import User
+from ctrack.users.tests.factories import UserFactory
+
+
+@pytest.fixture(autouse=True)
+def media_storage(settings, tmpdir):
+ settings.MEDIA_ROOT = tmpdir.strpath
+
+
+@pytest.fixture
+def user() -> User:
+ return UserFactory()
+
+
+@pytest.fixture
+def request_factory() -> RequestFactory:
+ return RequestFactory()
diff --git a/ctrack/contrib/__init__.py b/ctrack/contrib/__init__.py
new file mode 100644
index 0000000..1c7ecc8
--- /dev/null
+++ b/ctrack/contrib/__init__.py
@@ -0,0 +1,5 @@
+"""
+To understand why this file is here, please read:
+
+http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
+"""
diff --git a/ctrack/contrib/sites/__init__.py b/ctrack/contrib/sites/__init__.py
new file mode 100644
index 0000000..1c7ecc8
--- /dev/null
+++ b/ctrack/contrib/sites/__init__.py
@@ -0,0 +1,5 @@
+"""
+To understand why this file is here, please read:
+
+http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
+"""
diff --git a/ctrack/contrib/sites/migrations/0001_initial.py b/ctrack/contrib/sites/migrations/0001_initial.py
new file mode 100644
index 0000000..304cd6d
--- /dev/null
+++ b/ctrack/contrib/sites/migrations/0001_initial.py
@@ -0,0 +1,42 @@
+import django.contrib.sites.models
+from django.contrib.sites.models import _simple_domain_name_validator
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = []
+
+ operations = [
+ migrations.CreateModel(
+ name="Site",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ verbose_name="ID",
+ serialize=False,
+ auto_created=True,
+ primary_key=True,
+ ),
+ ),
+ (
+ "domain",
+ models.CharField(
+ max_length=100,
+ verbose_name="domain name",
+ validators=[_simple_domain_name_validator],
+ ),
+ ),
+ ("name", models.CharField(max_length=50, verbose_name="display name")),
+ ],
+ options={
+ "ordering": ("domain",),
+ "db_table": "django_site",
+ "verbose_name": "site",
+ "verbose_name_plural": "sites",
+ },
+ bases=(models.Model,),
+ managers=[("objects", django.contrib.sites.models.SiteManager())],
+ )
+ ]
diff --git a/ctrack/contrib/sites/migrations/0002_alter_domain_unique.py b/ctrack/contrib/sites/migrations/0002_alter_domain_unique.py
new file mode 100644
index 0000000..2c8d6da
--- /dev/null
+++ b/ctrack/contrib/sites/migrations/0002_alter_domain_unique.py
@@ -0,0 +1,20 @@
+import django.contrib.sites.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("sites", "0001_initial")]
+
+ operations = [
+ migrations.AlterField(
+ model_name="site",
+ name="domain",
+ field=models.CharField(
+ max_length=100,
+ unique=True,
+ validators=[django.contrib.sites.models._simple_domain_name_validator],
+ verbose_name="domain name",
+ ),
+ )
+ ]
diff --git a/ctrack/contrib/sites/migrations/0003_set_site_domain_and_name.py b/ctrack/contrib/sites/migrations/0003_set_site_domain_and_name.py
new file mode 100644
index 0000000..e81e91e
--- /dev/null
+++ b/ctrack/contrib/sites/migrations/0003_set_site_domain_and_name.py
@@ -0,0 +1,34 @@
+"""
+To understand why this file is here, please read:
+
+http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
+"""
+from django.conf import settings
+from django.db import migrations
+
+
+def update_site_forward(apps, schema_editor):
+ """Set site domain and name."""
+ Site = apps.get_model("sites", "Site")
+ Site.objects.update_or_create(
+ id=settings.SITE_ID,
+ defaults={
+ "domain": "ctrackdft.net",
+ "name": "ctrack",
+ },
+ )
+
+
+def update_site_backward(apps, schema_editor):
+ """Revert site domain and name to default."""
+ Site = apps.get_model("sites", "Site")
+ Site.objects.update_or_create(
+ id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("sites", "0002_alter_domain_unique")]
+
+ operations = [migrations.RunPython(update_site_forward, update_site_backward)]
diff --git a/ctrack/contrib/sites/migrations/__init__.py b/ctrack/contrib/sites/migrations/__init__.py
new file mode 100644
index 0000000..1c7ecc8
--- /dev/null
+++ b/ctrack/contrib/sites/migrations/__init__.py
@@ -0,0 +1,5 @@
+"""
+To understand why this file is here, please read:
+
+http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
+"""
diff --git a/ctrack/static/css/project.css b/ctrack/static/css/project.css
new file mode 100644
index 0000000..f1d543d
--- /dev/null
+++ b/ctrack/static/css/project.css
@@ -0,0 +1,13 @@
+/* These styles are generated from project.scss. */
+
+.alert-debug {
+ color: black;
+ background-color: white;
+ border-color: #d6e9c6;
+}
+
+.alert-error {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #eed3d7;
+}
diff --git a/ctrack/static/fonts/.gitkeep b/ctrack/static/fonts/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/static/fonts/.gitkeep
diff --git a/ctrack/static/images/favicons/favicon.ico b/ctrack/static/images/favicons/favicon.ico
new file mode 100644
index 0000000..e1c1dd1
--- /dev/null
+++ b/ctrack/static/images/favicons/favicon.ico
Binary files differ
diff --git a/ctrack/static/js/project.js b/ctrack/static/js/project.js
new file mode 100644
index 0000000..d26d23b
--- /dev/null
+++ b/ctrack/static/js/project.js
@@ -0,0 +1 @@
+/* Project specific Javascript goes here. */
diff --git a/ctrack/static/sass/custom_bootstrap_vars.scss b/ctrack/static/sass/custom_bootstrap_vars.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/static/sass/custom_bootstrap_vars.scss
diff --git a/ctrack/static/sass/project.scss b/ctrack/static/sass/project.scss
new file mode 100644
index 0000000..3c8f261
--- /dev/null
+++ b/ctrack/static/sass/project.scss
@@ -0,0 +1,37 @@
+
+
+
+
+// project specific CSS goes here
+
+////////////////////////////////
+ //Variables//
+////////////////////////////////
+
+// Alert colors
+
+$white: #fff;
+$mint-green: #d6e9c6;
+$black: #000;
+$pink: #f2dede;
+$dark-pink: #eed3d7;
+$red: #b94a48;
+
+////////////////////////////////
+ //Alerts//
+////////////////////////////////
+
+// bootstrap alert CSS, translated to the django-standard levels of
+// debug, info, success, warning, error
+
+.alert-debug {
+ background-color: $white;
+ border-color: $mint-green;
+ color: $black;
+}
+
+.alert-error {
+ background-color: $pink;
+ border-color: $dark-pink;
+ color: $red;
+}
diff --git a/ctrack/templates/403.html b/ctrack/templates/403.html
new file mode 100644
index 0000000..77db8ae
--- /dev/null
+++ b/ctrack/templates/403.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Forbidden (403){% endblock %}
+
+{% block content %}
+<h1>Forbidden (403)</h1>
+
+<p>CSRF verification failed. Request aborted.</p>
+{% endblock content %}
diff --git a/ctrack/templates/404.html b/ctrack/templates/404.html
new file mode 100644
index 0000000..98327cd
--- /dev/null
+++ b/ctrack/templates/404.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Page not found{% endblock %}
+
+{% block content %}
+<h1>Page not found</h1>
+
+<p>This is not the page you were looking for.</p>
+{% endblock content %}
diff --git a/ctrack/templates/500.html b/ctrack/templates/500.html
new file mode 100644
index 0000000..21df606
--- /dev/null
+++ b/ctrack/templates/500.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Server Error{% endblock %}
+
+{% block content %}
+<h1>Ooops!!! 500</h1>
+
+<h3>Looks like something went wrong!</h3>
+
+<p>We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.</p>
+{% endblock content %}
+
+
diff --git a/ctrack/templates/account/account_inactive.html b/ctrack/templates/account/account_inactive.html
new file mode 100644
index 0000000..17c2157
--- /dev/null
+++ b/ctrack/templates/account/account_inactive.html
@@ -0,0 +1,12 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+
+{% block head_title %}{% trans "Account Inactive" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "Account Inactive" %}</h1>
+
+<p>{% trans "This account is inactive." %}</p>
+{% endblock %}
+
diff --git a/ctrack/templates/account/base.html b/ctrack/templates/account/base.html
new file mode 100644
index 0000000..8e1f260
--- /dev/null
+++ b/ctrack/templates/account/base.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}
+
+{% block content %}
+<div class="row">
+ <div class="col-md-6 offset-md-3">
+ {% block inner %}{% endblock %}
+ </div>
+</div>
+{% endblock %}
diff --git a/ctrack/templates/account/email.html b/ctrack/templates/account/email.html
new file mode 100644
index 0000000..0dc8d14
--- /dev/null
+++ b/ctrack/templates/account/email.html
@@ -0,0 +1,80 @@
+
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Account" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "E-mail Addresses" %}</h1>
+
+{% if user.emailaddress_set.all %}
+<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
+
+<form action="{% url 'account_email' %}" class="email_list" method="post">
+{% csrf_token %}
+<fieldset class="blockLabels">
+
+ {% for emailaddress in user.emailaddress_set.all %}
+<div class="radio">
+ <label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
+
+ <input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
+
+ {{ emailaddress.email }}
+ {% if emailaddress.verified %}
+ <span class="verified">{% trans "Verified" %}</span>
+ {% else %}
+ <span class="unverified">{% trans "Unverified" %}</span>
+ {% endif %}
+ {% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
+ </label>
+</div>
+ {% endfor %}
+
+<div class="form-group">
+ <button class="secondaryAction btn btn-primary" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
+ <button class="secondaryAction btn btn-primary" type="submit" name="action_send" >{% trans 'Re-send Verification' %}</button>
+ <button class="primaryAction btn btn-primary" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
+</div>
+
+</fieldset>
+</form>
+
+{% else %}
+<p><strong>{% trans 'Warning:'%}</strong> {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}</p>
+
+{% endif %}
+
+
+ <h2>{% trans "Add E-mail Address" %}</h2>
+
+ <form method="post" action="{% url 'account_email' %}" class="add_email">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <button class="btn btn-primary" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
+ </form>
+
+{% endblock %}
+
+
+{% block javascript %}
+{{ block.super }}
+<script type="text/javascript">
+(function() {
+ var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
+ var actions = document.getElementsByName('action_remove');
+ if (actions.length) {
+ actions[0].addEventListener("click", function(e) {
+ if (! confirm(message)) {
+ e.preventDefault();
+ }
+ });
+ }
+})();
+
+$('.form-group').removeClass('row');
+</script>
+{% endblock %}
+
diff --git a/ctrack/templates/account/email_confirm.html b/ctrack/templates/account/email_confirm.html
new file mode 100644
index 0000000..46c7812
--- /dev/null
+++ b/ctrack/templates/account/email_confirm.html
@@ -0,0 +1,32 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load account %}
+
+{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
+
+
+{% block inner %}
+<h1>{% trans "Confirm E-mail Address" %}</h1>
+
+{% if confirmation %}
+
+{% user_display confirmation.email_address.user as user_display %}
+
+<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
+
+<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
+{% csrf_token %}
+ <button class="btn btn-primary" type="submit">{% trans 'Confirm' %}</button>
+</form>
+
+{% else %}
+
+{% url 'account_email' as email_url %}
+
+<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
+
+{% endif %}
+
+{% endblock %}
+
diff --git a/ctrack/templates/account/login.html b/ctrack/templates/account/login.html
new file mode 100644
index 0000000..2cadea6
--- /dev/null
+++ b/ctrack/templates/account/login.html
@@ -0,0 +1,48 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load account socialaccount %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Sign In" %}{% endblock %}
+
+{% block inner %}
+
+<h1>{% trans "Sign In" %}</h1>
+
+{% get_providers as socialaccount_providers %}
+
+{% if socialaccount_providers %}
+<p>{% blocktrans with site.name as site_name %}Please sign in with one
+of your existing third party accounts. Or, <a href="{{ signup_url }}">sign up</a>
+for a {{ site_name }} account and sign in below:{% endblocktrans %}</p>
+
+<div class="socialaccount_ballot">
+
+ <ul class="socialaccount_providers">
+ {% include "socialaccount/snippets/provider_list.html" with process="login" %}
+ </ul>
+
+ <div class="login-or">{% trans 'or' %}</div>
+
+</div>
+
+{% include "socialaccount/snippets/login_extra.html" %}
+
+{% else %}
+<p>{% blocktrans %}If you have not created an account yet, then please
+<a href="{{ signup_url }}">sign up</a> first.{% endblocktrans %}</p>
+{% endif %}
+
+<form class="login" method="POST" action="{% url 'account_login' %}">
+ {% csrf_token %}
+ {{ form|crispy }}
+ {% if redirect_field_value %}
+ <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
+ {% endif %}
+ <a class="button secondaryAction" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
+ <button class="primaryAction btn btn-primary" type="submit">{% trans "Sign In" %}</button>
+</form>
+
+{% endblock %}
+
diff --git a/ctrack/templates/account/logout.html b/ctrack/templates/account/logout.html
new file mode 100644
index 0000000..8e2e675
--- /dev/null
+++ b/ctrack/templates/account/logout.html
@@ -0,0 +1,22 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+
+{% block head_title %}{% trans "Sign Out" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "Sign Out" %}</h1>
+
+<p>{% trans 'Are you sure you want to sign out?' %}</p>
+
+<form method="post" action="{% url 'account_logout' %}">
+ {% csrf_token %}
+ {% if redirect_field_value %}
+ <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
+ {% endif %}
+ <button class="btn btn-danger" type="submit">{% trans 'Sign Out' %}</button>
+</form>
+
+
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_change.html b/ctrack/templates/account/password_change.html
new file mode 100644
index 0000000..b72ca06
--- /dev/null
+++ b/ctrack/templates/account/password_change.html
@@ -0,0 +1,17 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Change Password" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% trans "Change Password" %}</h1>
+
+ <form method="POST" action="{% url 'account_change_password' %}" class="password_change">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <button class="btn btn-primary" type="submit" name="action">{% trans "Change Password" %}</button>
+ </form>
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_reset.html b/ctrack/templates/account/password_reset.html
new file mode 100644
index 0000000..845bbda
--- /dev/null
+++ b/ctrack/templates/account/password_reset.html
@@ -0,0 +1,26 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load account %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Password Reset" %}{% endblock %}
+
+{% block inner %}
+
+ <h1>{% trans "Password Reset" %}</h1>
+ {% if user.is_authenticated %}
+ {% include "account/snippets/already_logged_in.html" %}
+ {% endif %}
+
+ <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
+
+ <form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <input class="btn btn-primary" type="submit" value="{% trans 'Reset My Password' %}" />
+ </form>
+
+ <p>{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}</p>
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_reset_done.html b/ctrack/templates/account/password_reset_done.html
new file mode 100644
index 0000000..c59534a
--- /dev/null
+++ b/ctrack/templates/account/password_reset_done.html
@@ -0,0 +1,17 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load account %}
+
+{% block head_title %}{% trans "Password Reset" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% trans "Password Reset" %}</h1>
+
+ {% if user.is_authenticated %}
+ {% include "account/snippets/already_logged_in.html" %}
+ {% endif %}
+
+ <p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_reset_from_key.html b/ctrack/templates/account/password_reset_from_key.html
new file mode 100644
index 0000000..4abdb56
--- /dev/null
+++ b/ctrack/templates/account/password_reset_from_key.html
@@ -0,0 +1,25 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+{% block head_title %}{% trans "Change Password" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h1>
+
+ {% if token_fail %}
+ {% url 'account_reset_password' as passwd_reset_url %}
+ <p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
+ {% else %}
+ {% if form %}
+ <form method="POST" action=".">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <input class="btn btn-primary" type="submit" name="action" value="{% trans 'change password' %}"/>
+ </form>
+ {% else %}
+ <p>{% trans 'Your password is now changed.' %}</p>
+ {% endif %}
+ {% endif %}
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_reset_from_key_done.html b/ctrack/templates/account/password_reset_from_key_done.html
new file mode 100644
index 0000000..89be086
--- /dev/null
+++ b/ctrack/templates/account/password_reset_from_key_done.html
@@ -0,0 +1,10 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% block head_title %}{% trans "Change Password" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% trans "Change Password" %}</h1>
+ <p>{% trans 'Your password is now changed.' %}</p>
+{% endblock %}
+
diff --git a/ctrack/templates/account/password_set.html b/ctrack/templates/account/password_set.html
new file mode 100644
index 0000000..2232223
--- /dev/null
+++ b/ctrack/templates/account/password_set.html
@@ -0,0 +1,17 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Set Password" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% trans "Set Password" %}</h1>
+
+ <form method="POST" action="{% url 'account_set_password' %}" class="password_set">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <input class="btn btn-primary" type="submit" name="action" value="{% trans 'Set Password' %}"/>
+ </form>
+{% endblock %}
+
diff --git a/ctrack/templates/account/signup.html b/ctrack/templates/account/signup.html
new file mode 100644
index 0000000..6a2954e
--- /dev/null
+++ b/ctrack/templates/account/signup.html
@@ -0,0 +1,23 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}{% trans "Signup" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "Sign Up" %}</h1>
+
+<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>
+
+<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
+ {% csrf_token %}
+ {{ form|crispy }}
+ {% if redirect_field_value %}
+ <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
+ {% endif %}
+ <button class="btn btn-primary" type="submit">{% trans "Sign Up" %} &raquo;</button>
+</form>
+
+{% endblock %}
+
diff --git a/ctrack/templates/account/signup_closed.html b/ctrack/templates/account/signup_closed.html
new file mode 100644
index 0000000..2322f17
--- /dev/null
+++ b/ctrack/templates/account/signup_closed.html
@@ -0,0 +1,12 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+
+{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "Sign Up Closed" %}</h1>
+
+<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
+{% endblock %}
+
diff --git a/ctrack/templates/account/verification_sent.html b/ctrack/templates/account/verification_sent.html
new file mode 100644
index 0000000..ad093fd
--- /dev/null
+++ b/ctrack/templates/account/verification_sent.html
@@ -0,0 +1,13 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+
+{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
+
+{% block inner %}
+ <h1>{% trans "Verify Your E-mail Address" %}</h1>
+
+ <p>{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
+
+{% endblock %}
+
diff --git a/ctrack/templates/account/verified_email_required.html b/ctrack/templates/account/verified_email_required.html
new file mode 100644
index 0000000..09d4fde
--- /dev/null
+++ b/ctrack/templates/account/verified_email_required.html
@@ -0,0 +1,24 @@
+{% extends "account/base.html" %}
+
+{% load i18n %}
+
+{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
+
+{% block inner %}
+<h1>{% trans "Verify Your E-mail Address" %}</h1>
+
+{% url 'account_email' as email_url %}
+
+<p>{% blocktrans %}This part of the site requires us to verify that
+you are who you claim to be. For this purpose, we require that you
+verify ownership of your e-mail address. {% endblocktrans %}</p>
+
+<p>{% blocktrans %}We have sent an e-mail to you for
+verification. Please click on the link inside this e-mail. Please
+contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
+
+<p>{% blocktrans %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>.{% endblocktrans %}</p>
+
+
+{% endblock %}
+
diff --git a/ctrack/templates/base.html b/ctrack/templates/base.html
new file mode 100644
index 0000000..fbe9d86
--- /dev/null
+++ b/ctrack/templates/base.html
@@ -0,0 +1,114 @@
+{% load static i18n %}<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <title>{% block title %}ctrack{% endblock title %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
+ <!--[if lt IE 9]>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
+ <![endif]-->
+
+ <link rel="icon" href="{% static 'images/favicons/favicon.ico' %}">
+
+ {% block css %}
+
+ <!-- Latest compiled and minified Bootstrap CSS -->
+ <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+
+
+ <!-- Your stuff: Third-party CSS libraries go here -->
+
+ <!-- This file stores project-specific CSS -->
+
+ <link href="{% static 'css/project.css' %}" rel="stylesheet">
+
+
+ {% endblock %}
+
+ </head>
+
+ <body>
+
+ <div class="mb-1">
+ <nav class="navbar navbar-expand-md navbar-light bg-light">
+ <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <a class="navbar-brand" href="{% url 'home' %}">ctrack</a>
+
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
+ <ul class="navbar-nav mr-auto">
+ <li class="nav-item active">
+ <a class="nav-link" href="{% url 'home' %}">Home <span class="sr-only">(current)</span></a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{% url 'about' %}">About</a>
+ </li>
+ {% if request.user.is_authenticated %}
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a class="nav-link" href="{% url 'users:detail' request.user.username %}">{% trans "My Profile" %}</a>
+ </li>
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a class="nav-link" href="{% url 'account_logout' %}">{% trans "Sign Out" %}</a>
+ </li>
+ {% else %}
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
+ </li>
+ <li class="nav-item">
+ {# URL provided by django-allauth/account/urls.py #}
+ <a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+ </nav>
+
+ </div>
+
+ <div class="container">
+
+ {% if messages %}
+ {% for message in messages %}
+ <div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">{{ message }}<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>
+ {% endfor %}
+ {% endif %}
+
+ {% block content %}
+ <p>Use this document as a way to quick start any new project.</p>
+ {% endblock content %}
+
+ </div> <!-- /container -->
+
+ {% block modal %}{% endblock modal %}
+
+ <!-- Le javascript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ {% block javascript %}
+
+ <!-- Bootstrap JS and its dependencies-->
+ <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+
+ <!-- Your stuff: Third-party javascript libraries go here -->
+
+
+ <!-- place project specific Javascript in this file -->
+
+ <script src="{% static 'js/project.js' %}"></script>
+
+
+ {% endblock javascript %}
+ </body>
+</html>
+
diff --git a/ctrack/templates/pages/about.html b/ctrack/templates/pages/about.html
new file mode 100644
index 0000000..63913c1
--- /dev/null
+++ b/ctrack/templates/pages/about.html
@@ -0,0 +1 @@
+{% extends "base.html" %} \ No newline at end of file
diff --git a/ctrack/templates/pages/home.html b/ctrack/templates/pages/home.html
new file mode 100644
index 0000000..63913c1
--- /dev/null
+++ b/ctrack/templates/pages/home.html
@@ -0,0 +1 @@
+{% extends "base.html" %} \ No newline at end of file
diff --git a/ctrack/templates/users/user_detail.html b/ctrack/templates/users/user_detail.html
new file mode 100644
index 0000000..e86eda1
--- /dev/null
+++ b/ctrack/templates/users/user_detail.html
@@ -0,0 +1,36 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}User: {{ object.username }}{% endblock %}
+
+{% block content %}
+<div class="container">
+
+ <div class="row">
+ <div class="col-sm-12">
+
+ <h2>{{ object.username }}</h2>
+ {% if object.name %}
+ <p>{{ object.name }}</p>
+ {% endif %}
+ </div>
+ </div>
+
+{% if object == request.user %}
+<!-- Action buttons -->
+<div class="row">
+
+ <div class="col-sm-12">
+ <a class="btn btn-primary" href="{% url 'users:update' %}" role="button">My Info</a>
+ <a class="btn btn-primary" href="{% url 'account_email' %}" role="button">E-Mail</a>
+ <!-- Your Stuff: Custom user template urls -->
+ </div>
+
+</div>
+<!-- End Action buttons -->
+{% endif %}
+
+
+</div>
+{% endblock content %}
+
diff --git a/ctrack/templates/users/user_form.html b/ctrack/templates/users/user_form.html
new file mode 100644
index 0000000..467357a
--- /dev/null
+++ b/ctrack/templates/users/user_form.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% load crispy_forms_tags %}
+
+{% block title %}{{ user.username }}{% endblock %}
+
+{% block content %}
+ <h1>{{ user.username }}</h1>
+ <form class="form-horizontal" method="post" action="{% url 'users:update' %}">
+ {% csrf_token %}
+ {{ form|crispy }}
+ <div class="control-group">
+ <div class="controls">
+ <button type="submit" class="btn btn-primary">Update</button>
+ </div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/ctrack/users/__init__.py b/ctrack/users/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/users/__init__.py
diff --git a/ctrack/users/adapters.py b/ctrack/users/adapters.py
new file mode 100644
index 0000000..0d206fa
--- /dev/null
+++ b/ctrack/users/adapters.py
@@ -0,0 +1,16 @@
+from typing import Any
+
+from allauth.account.adapter import DefaultAccountAdapter
+from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from django.conf import settings
+from django.http import HttpRequest
+
+
+class AccountAdapter(DefaultAccountAdapter):
+ def is_open_for_signup(self, request: HttpRequest):
+ return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
+
+
+class SocialAccountAdapter(DefaultSocialAccountAdapter):
+ def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
+ return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
diff --git a/ctrack/users/admin.py b/ctrack/users/admin.py
new file mode 100644
index 0000000..120cc64
--- /dev/null
+++ b/ctrack/users/admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+from django.contrib.auth import admin as auth_admin
+from django.contrib.auth import get_user_model
+
+from ctrack.users.forms import UserChangeForm, UserCreationForm
+
+User = get_user_model()
+
+
+@admin.register(User)
+class UserAdmin(auth_admin.UserAdmin):
+
+ form = UserChangeForm
+ add_form = UserCreationForm
+ fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets
+ list_display = ["username", "name", "is_superuser"]
+ search_fields = ["name"]
diff --git a/ctrack/users/apps.py b/ctrack/users/apps.py
new file mode 100644
index 0000000..9f81f1d
--- /dev/null
+++ b/ctrack/users/apps.py
@@ -0,0 +1,13 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class UsersConfig(AppConfig):
+ name = "ctrack.users"
+ verbose_name = _("Users")
+
+ def ready(self):
+ try:
+ import ctrack.users.signals # noqa F401
+ except ImportError:
+ pass
diff --git a/ctrack/users/forms.py b/ctrack/users/forms.py
new file mode 100644
index 0000000..250cc90
--- /dev/null
+++ b/ctrack/users/forms.py
@@ -0,0 +1,30 @@
+from django.contrib.auth import get_user_model, forms
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+User = get_user_model()
+
+
+class UserChangeForm(forms.UserChangeForm):
+ class Meta(forms.UserChangeForm.Meta):
+ model = User
+
+
+class UserCreationForm(forms.UserCreationForm):
+
+ error_message = forms.UserCreationForm.error_messages.update(
+ {"duplicate_username": _("This username has already been taken.")}
+ )
+
+ class Meta(forms.UserCreationForm.Meta):
+ model = User
+
+ def clean_username(self):
+ username = self.cleaned_data["username"]
+
+ try:
+ User.objects.get(username=username)
+ except User.DoesNotExist:
+ return username
+
+ raise ValidationError(self.error_messages["duplicate_username"])
diff --git a/ctrack/users/migrations/0001_initial.py b/ctrack/users/migrations/0001_initial.py
new file mode 100644
index 0000000..c9d8905
--- /dev/null
+++ b/ctrack/users/migrations/0001_initial.py
@@ -0,0 +1,132 @@
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [("auth", "0008_alter_user_username_max_length")]
+
+ operations = [
+ migrations.CreateModel(
+ name="User",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("password", models.CharField(max_length=128, verbose_name="password")),
+ (
+ "last_login",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="last login"
+ ),
+ ),
+ (
+ "is_superuser",
+ models.BooleanField(
+ default=False,
+ help_text="Designates that this user has all permissions without explicitly assigning them.",
+ verbose_name="superuser status",
+ ),
+ ),
+ (
+ "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",
+ ),
+ ),
+ (
+ "first_name",
+ models.CharField(
+ blank=True, max_length=30, verbose_name="first name"
+ ),
+ ),
+ (
+ "last_name",
+ models.CharField(
+ blank=True, max_length=150, verbose_name="last name"
+ ),
+ ),
+ (
+ "email",
+ models.EmailField(
+ blank=True, max_length=254, verbose_name="email address"
+ ),
+ ),
+ (
+ "is_staff",
+ models.BooleanField(
+ default=False,
+ help_text="Designates whether the user can log into this admin site.",
+ verbose_name="staff status",
+ ),
+ ),
+ (
+ "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",
+ ),
+ ),
+ (
+ "date_joined",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date joined"
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="Name of User"
+ ),
+ ),
+ (
+ "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",
+ ),
+ ),
+ (
+ "user_permissions",
+ 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",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name_plural": "users",
+ "verbose_name": "user",
+ "abstract": False,
+ },
+ managers=[("objects", django.contrib.auth.models.UserManager())],
+ )
+ ]
diff --git a/ctrack/users/migrations/__init__.py b/ctrack/users/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/users/migrations/__init__.py
diff --git a/ctrack/users/models.py b/ctrack/users/models.py
new file mode 100644
index 0000000..8f07b15
--- /dev/null
+++ b/ctrack/users/models.py
@@ -0,0 +1,14 @@
+from django.contrib.auth.models import AbstractUser
+from django.db.models import CharField
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+
+
+class User(AbstractUser):
+
+ # First Name and Last Name do not cover name patterns
+ # around the globe.
+ name = CharField(_("Name of User"), blank=True, max_length=255)
+
+ def get_absolute_url(self):
+ return reverse("users:detail", kwargs={"username": self.username})
diff --git a/ctrack/users/tests/__init__.py b/ctrack/users/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/users/tests/__init__.py
diff --git a/ctrack/users/tests/factories.py b/ctrack/users/tests/factories.py
new file mode 100644
index 0000000..b537136
--- /dev/null
+++ b/ctrack/users/tests/factories.py
@@ -0,0 +1,27 @@
+from typing import Any, Sequence
+
+from django.contrib.auth import get_user_model
+from factory import DjangoModelFactory, Faker, post_generation
+
+
+class UserFactory(DjangoModelFactory):
+
+ username = Faker("user_name")
+ email = Faker("email")
+ name = Faker("name")
+
+ @post_generation
+ def password(self, create: bool, extracted: Sequence[Any], **kwargs):
+ password = Faker(
+ "password",
+ length=42,
+ special_chars=True,
+ digits=True,
+ upper_case=True,
+ lower_case=True,
+ ).generate(extra_kwargs={})
+ self.set_password(password)
+
+ class Meta:
+ model = get_user_model()
+ django_get_or_create = ["username"]
diff --git a/ctrack/users/tests/test_forms.py b/ctrack/users/tests/test_forms.py
new file mode 100644
index 0000000..11ef251
--- /dev/null
+++ b/ctrack/users/tests/test_forms.py
@@ -0,0 +1,40 @@
+import pytest
+
+from ctrack.users.forms import UserCreationForm
+from ctrack.users.tests.factories import UserFactory
+
+pytestmark = pytest.mark.django_db
+
+
+class TestUserCreationForm:
+ def test_clean_username(self):
+ # A user with proto_user params does not exist yet.
+ proto_user = UserFactory.build()
+
+ form = UserCreationForm(
+ {
+ "username": proto_user.username,
+ "password1": proto_user._password,
+ "password2": proto_user._password,
+ }
+ )
+
+ assert form.is_valid()
+ assert form.clean_username() == proto_user.username
+
+ # Creating a user.
+ form.save()
+
+ # The user with proto_user params already exists,
+ # hence cannot be created.
+ form = UserCreationForm(
+ {
+ "username": proto_user.username,
+ "password1": proto_user._password,
+ "password2": proto_user._password,
+ }
+ )
+
+ assert not form.is_valid()
+ assert len(form.errors) == 1
+ assert "username" in form.errors
diff --git a/ctrack/users/tests/test_models.py b/ctrack/users/tests/test_models.py
new file mode 100644
index 0000000..1b33961
--- /dev/null
+++ b/ctrack/users/tests/test_models.py
@@ -0,0 +1,9 @@
+import pytest
+
+from ctrack.users.models import User
+
+pytestmark = pytest.mark.django_db
+
+
+def test_user_get_absolute_url(user: User):
+ assert user.get_absolute_url() == f"/users/{user.username}/"
diff --git a/ctrack/users/tests/test_urls.py b/ctrack/users/tests/test_urls.py
new file mode 100644
index 0000000..383a649
--- /dev/null
+++ b/ctrack/users/tests/test_urls.py
@@ -0,0 +1,24 @@
+import pytest
+from django.urls import reverse, resolve
+
+from ctrack.users.models import User
+
+pytestmark = pytest.mark.django_db
+
+
+def test_detail(user: User):
+ assert (
+ reverse("users:detail", kwargs={"username": user.username})
+ == f"/users/{user.username}/"
+ )
+ assert resolve(f"/users/{user.username}/").view_name == "users:detail"
+
+
+def test_update():
+ assert reverse("users:update") == "/users/~update/"
+ assert resolve("/users/~update/").view_name == "users:update"
+
+
+def test_redirect():
+ assert reverse("users:redirect") == "/users/~redirect/"
+ assert resolve("/users/~redirect/").view_name == "users:redirect"
diff --git a/ctrack/users/tests/test_views.py b/ctrack/users/tests/test_views.py
new file mode 100644
index 0000000..3e7fbf7
--- /dev/null
+++ b/ctrack/users/tests/test_views.py
@@ -0,0 +1,46 @@
+import pytest
+from django.test import RequestFactory
+
+from ctrack.users.models import User
+from ctrack.users.views import UserRedirectView, UserUpdateView
+
+pytestmark = pytest.mark.django_db
+
+
+class TestUserUpdateView:
+ """
+ TODO:
+ extracting view initialization code as class-scoped fixture
+ would be great if only pytest-django supported non-function-scoped
+ fixture db access -- this is a work-in-progress for now:
+ https://github.com/pytest-dev/pytest-django/pull/258
+ """
+
+ def test_get_success_url(self, user: User, request_factory: RequestFactory):
+ view = UserUpdateView()
+ request = request_factory.get("/fake-url/")
+ request.user = user
+
+ view.request = request
+
+ assert view.get_success_url() == f"/users/{user.username}/"
+
+ def test_get_object(self, user: User, request_factory: RequestFactory):
+ view = UserUpdateView()
+ request = request_factory.get("/fake-url/")
+ request.user = user
+
+ view.request = request
+
+ assert view.get_object() == user
+
+
+class TestUserRedirectView:
+ def test_get_redirect_url(self, user: User, request_factory: RequestFactory):
+ view = UserRedirectView()
+ request = request_factory.get("/fake-url")
+ request.user = user
+
+ view.request = request
+
+ assert view.get_redirect_url() == f"/users/{user.username}/"
diff --git a/ctrack/users/urls.py b/ctrack/users/urls.py
new file mode 100644
index 0000000..cc1d04c
--- /dev/null
+++ b/ctrack/users/urls.py
@@ -0,0 +1,14 @@
+from django.urls import path
+
+from ctrack.users.views import (
+ user_redirect_view,
+ user_update_view,
+ user_detail_view,
+)
+
+app_name = "users"
+urlpatterns = [
+ path("~redirect/", view=user_redirect_view, name="redirect"),
+ path("~update/", view=user_update_view, name="update"),
+ path("<str:username>/", view=user_detail_view, name="detail"),
+]
diff --git a/ctrack/users/views.py b/ctrack/users/views.py
new file mode 100644
index 0000000..5c0d5b5
--- /dev/null
+++ b/ctrack/users/views.py
@@ -0,0 +1,50 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.urls import reverse
+from django.views.generic import DetailView, RedirectView, UpdateView
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
+
+User = get_user_model()
+
+
+class UserDetailView(LoginRequiredMixin, DetailView):
+
+ model = User
+ slug_field = "username"
+ slug_url_kwarg = "username"
+
+
+user_detail_view = UserDetailView.as_view()
+
+
+class UserUpdateView(LoginRequiredMixin, UpdateView):
+
+ model = User
+ fields = ["name"]
+
+ def get_success_url(self):
+ return reverse("users:detail", kwargs={"username": self.request.user.username})
+
+ def get_object(self):
+ return User.objects.get(username=self.request.user.username)
+
+ def form_valid(self, form):
+ messages.add_message(
+ self.request, messages.INFO, _("Infos successfully updated")
+ )
+ return super().form_valid(form)
+
+
+user_update_view = UserUpdateView.as_view()
+
+
+class UserRedirectView(LoginRequiredMixin, RedirectView):
+
+ permanent = False
+
+ def get_redirect_url(self):
+ return reverse("users:detail", kwargs={"username": self.request.user.username})
+
+
+user_redirect_view = UserRedirectView.as_view()
diff --git a/ctrack/utils/__init__.py b/ctrack/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ctrack/utils/__init__.py
diff --git a/ctrack/utils/context_processors.py b/ctrack/utils/context_processors.py
new file mode 100644
index 0000000..de40507
--- /dev/null
+++ b/ctrack/utils/context_processors.py
@@ -0,0 +1,5 @@
+from django.conf import settings
+
+
+def settings_context(_request):
+ return {"settings": settings}