diff options
Diffstat (limited to 'ctrack')
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 Binary files differnew file mode 100644 index 0000000..e1c1dd1 --- /dev/null +++ b/ctrack/static/images/favicons/favicon.ico 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" %} »</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">×</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} |