initial commit
This commit is contained in:
5
benchcoach/__init__.py
Normal file
5
benchcoach/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
__version__ = "0.1.0"
|
||||
__version_info__ = tuple(
|
||||
int(num) if num.isdigit() else num
|
||||
for num in __version__.replace("-", ".", 1).split(".")
|
||||
)
|
||||
14
benchcoach/conftest.py
Normal file
14
benchcoach/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from benchcoach.users.models import User
|
||||
from benchcoach.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()
|
||||
5
benchcoach/contrib/__init__.py
Normal file
5
benchcoach/contrib/__init__.py
Normal file
@@ -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
|
||||
"""
|
||||
5
benchcoach/contrib/sites/__init__.py
Normal file
5
benchcoach/contrib/sites/__init__.py
Normal file
@@ -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
|
||||
"""
|
||||
42
benchcoach/contrib/sites/migrations/0001_initial.py
Normal file
42
benchcoach/contrib/sites/migrations/0001_initial.py
Normal file
@@ -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())],
|
||||
)
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
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_or_create_site_with_sequence(site_model, connection, domain, name):
|
||||
"""Update or create the site with default ID and keep the DB sequence in sync."""
|
||||
site, created = site_model.objects.update_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
"domain": domain,
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
# We provided the ID explicitly when creating the Site entry, therefore the DB
|
||||
# sequence to auto-generate them wasn't used and is now out of sync. If we
|
||||
# don't do anything, we'll get a unique constraint violation the next time a
|
||||
# site is created.
|
||||
# To avoid this, we need to manually update DB sequence and make sure it's
|
||||
# greater than the maximum value.
|
||||
max_id = site_model.objects.order_by('-id').first().id
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT last_value from django_site_id_seq")
|
||||
(current_id,) = cursor.fetchone()
|
||||
if current_id <= max_id:
|
||||
cursor.execute(
|
||||
"alter sequence django_site_id_seq restart with %s",
|
||||
[max_id + 1],
|
||||
)
|
||||
|
||||
|
||||
def update_site_forward(apps, schema_editor):
|
||||
"""Set site domain and name."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"benchcoach.ascorrea.com",
|
||||
"BenchCoach",
|
||||
)
|
||||
|
||||
|
||||
def update_site_backward(apps, schema_editor):
|
||||
"""Revert site domain and name to default."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"example.com",
|
||||
"example.com",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("sites", "0002_alter_domain_unique")]
|
||||
|
||||
operations = [migrations.RunPython(update_site_forward, update_site_backward)]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-02-04 14:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("sites", "0003_set_site_domain_and_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="site",
|
||||
options={
|
||||
"ordering": ["domain"],
|
||||
"verbose_name": "site",
|
||||
"verbose_name_plural": "sites",
|
||||
},
|
||||
),
|
||||
]
|
||||
5
benchcoach/contrib/sites/migrations/__init__.py
Normal file
5
benchcoach/contrib/sites/migrations/__init__.py
Normal file
@@ -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
|
||||
"""
|
||||
13
benchcoach/static/css/project.css
Normal file
13
benchcoach/static/css/project.css
Normal file
@@ -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;
|
||||
}
|
||||
0
benchcoach/static/fonts/.gitkeep
Normal file
0
benchcoach/static/fonts/.gitkeep
Normal file
BIN
benchcoach/static/images/favicons/favicon.ico
Normal file
BIN
benchcoach/static/images/favicons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
1
benchcoach/static/js/project.js
Normal file
1
benchcoach/static/js/project.js
Normal file
@@ -0,0 +1 @@
|
||||
/* Project specific Javascript goes here. */
|
||||
9
benchcoach/templates/403.html
Normal file
9
benchcoach/templates/403.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Forbidden (403){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Forbidden (403)</h1>
|
||||
|
||||
<p>{% if exception %}{{ exception }}{% else %}You're not allowed to access this page.{% endif %}</p>
|
||||
{% endblock content %}
|
||||
9
benchcoach/templates/404.html
Normal file
9
benchcoach/templates/404.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page not found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Page not found</h1>
|
||||
|
||||
<p>{% if exception %}{{ exception }}{% else %}This is not the page you were looking for.{% endif %}</p>
|
||||
{% endblock content %}
|
||||
11
benchcoach/templates/500.html
Normal file
11
benchcoach/templates/500.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% 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 %}
|
||||
11
benchcoach/templates/account/account_inactive.html
Normal file
11
benchcoach/templates/account/account_inactive.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% translate "Account Inactive" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Account Inactive" %}</h1>
|
||||
|
||||
<p>{% translate "This account is inactive." %}</p>
|
||||
{% endblock %}
|
||||
10
benchcoach/templates/account/base.html
Normal file
10
benchcoach/templates/account/base.html
Normal file
@@ -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 %}
|
||||
78
benchcoach/templates/account/email.html
Normal file
78
benchcoach/templates/account/email.html
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Account" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "E-mail Addresses" %}</h1>
|
||||
|
||||
{% if user.emailaddress_set.all %}
|
||||
<p>{% translate '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">{% translate "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="unverified">{% translate "Unverified" %}</span>
|
||||
{% endif %}
|
||||
{% if emailaddress.primary %}<span class="primary">{% translate "Primary" %}</span>{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<button class="secondaryAction btn btn-primary" type="submit" name="action_primary" >{% translate 'Make Primary' %}</button>
|
||||
<button class="secondaryAction btn btn-primary" type="submit" name="action_send" >{% translate 'Re-send Verification' %}</button>
|
||||
<button class="primaryAction btn btn-primary" type="submit" name="action_remove" >{% translate 'Remove' %}</button>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
<p><strong>{% translate 'Warning:'%}</strong> {% translate "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>{% translate "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">{% translate "Add E-mail" %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block inline_javascript %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded',function() {
|
||||
const message = "{% translate 'Do you really want to remove the selected e-mail address?' %}";
|
||||
const actions = document.getElementsByName('action_remove');
|
||||
if (actions.length) {
|
||||
actions[0].addEventListener("click",function(e) {
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
Array.from(document.getElementsByClassName('form-group')).forEach(x => x.classList.remove('row'));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
31
benchcoach/templates/account/email_confirm.html
Normal file
31
benchcoach/templates/account/email_confirm.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
|
||||
{% block head_title %}{% translate "Confirm E-mail Address" %}{% endblock %}
|
||||
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Confirm E-mail Address" %}</h1>
|
||||
|
||||
{% if confirmation %}
|
||||
|
||||
{% user_display confirmation.email_address.user as user_display %}
|
||||
|
||||
<p>{% blocktranslate 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 }}.{% endblocktranslate %}</p>
|
||||
|
||||
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary" type="submit">{% translate 'Confirm' %}</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% url 'account_email' as email_url %}
|
||||
|
||||
<p>{% blocktranslate %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktranslate %}</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
59
benchcoach/templates/account/login.html
Normal file
59
benchcoach/templates/account/login.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account socialaccount %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Sign In" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
|
||||
<h1>{% translate "Sign In" %}</h1>
|
||||
|
||||
{% get_providers as socialaccount_providers %}
|
||||
|
||||
{% if socialaccount_providers %}
|
||||
<p>
|
||||
{% translate "Please sign in with one of your existing third party accounts:" %}
|
||||
{% if ACCOUNT_ALLOW_REGISTRATION %}
|
||||
{% blocktranslate trimmed %}
|
||||
Or, <a href="{{ signup_url }}">sign up</a>
|
||||
for a {{ site_name }} account and sign in below:
|
||||
{% endblocktranslate %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="socialaccount_ballot">
|
||||
|
||||
<ul class="socialaccount_providers">
|
||||
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
|
||||
</ul>
|
||||
|
||||
<div class="login-or">{% translate "or" %}</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% include "socialaccount/snippets/login_extra.html" %}
|
||||
|
||||
{% else %}
|
||||
{% if ACCOUNT_ALLOW_REGISTRATION %}
|
||||
<p>
|
||||
{% blocktranslate trimmed %}
|
||||
If you have not created an account yet, then please
|
||||
<a href="{{ signup_url }}">sign up</a> first.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% 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' %}">{% translate "Forgot Password?" %}</a>
|
||||
<button class="primaryAction btn btn-primary" type="submit">{% translate "Sign In" %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
19
benchcoach/templates/account/logout.html
Normal file
19
benchcoach/templates/account/logout.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% translate "Sign Out" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Sign Out" %}</h1>
|
||||
|
||||
<p>{% translate '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">{% translate 'Sign Out' %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
16
benchcoach/templates/account/password_change.html
Normal file
16
benchcoach/templates/account/password_change.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Change Password" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "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">{% translate "Change Password" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
25
benchcoach/templates/account/password_reset.html
Normal file
25
benchcoach/templates/account/password_reset.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Password Reset" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
|
||||
<h1>{% translate "Password Reset" %}</h1>
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
|
||||
<p>{% translate "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="{% translate 'Reset My Password' %}" />
|
||||
</form>
|
||||
|
||||
<p>{% blocktranslate %}Please contact us if you have any trouble resetting your password.{% endblocktranslate %}</p>
|
||||
{% endblock %}
|
||||
16
benchcoach/templates/account/password_reset_done.html
Normal file
16
benchcoach/templates/account/password_reset_done.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
|
||||
{% block head_title %}{% translate "Password Reset" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Password Reset" %}</h1>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
|
||||
<p>{% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}</p>
|
||||
{% endblock %}
|
||||
24
benchcoach/templates/account/password_reset_from_key.html
Normal file
24
benchcoach/templates/account/password_reset_from_key.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block head_title %}{% translate "Change Password" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% if token_fail %}{% translate "Bad Token" %}{% else %}{% translate "Change Password" %}{% endif %}</h1>
|
||||
|
||||
{% if token_fail %}
|
||||
{% url 'account_reset_password' as passwd_reset_url %}
|
||||
<p>{% blocktranslate %}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>.{% endblocktranslate %}</p>
|
||||
{% else %}
|
||||
{% if form %}
|
||||
<form method="POST" action=".">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input class="btn btn-primary" type="submit" name="action" value="{% translate 'change password' %}"/>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% translate 'Your password is now changed.' %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block head_title %}{% translate "Change Password" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Change Password" %}</h1>
|
||||
<p>{% translate 'Your password is now changed.' %}</p>
|
||||
{% endblock %}
|
||||
16
benchcoach/templates/account/password_set.html
Normal file
16
benchcoach/templates/account/password_set.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Set Password" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "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="{% translate 'Set Password' %}"/>
|
||||
</form>
|
||||
{% endblock %}
|
||||
22
benchcoach/templates/account/signup.html
Normal file
22
benchcoach/templates/account/signup.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block head_title %}{% translate "Signup" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Sign Up" %}</h1>
|
||||
|
||||
<p>{% blocktranslate %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktranslate %}</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">{% translate "Sign Up" %} »</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
11
benchcoach/templates/account/signup_closed.html
Normal file
11
benchcoach/templates/account/signup_closed.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% translate "Sign Up Closed" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Sign Up Closed" %}</h1>
|
||||
|
||||
<p>{% translate "We are sorry, but the sign up is currently closed." %}</p>
|
||||
{% endblock %}
|
||||
12
benchcoach/templates/account/verification_sent.html
Normal file
12
benchcoach/templates/account/verification_sent.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Verify Your E-mail Address" %}</h1>
|
||||
|
||||
<p>{% blocktranslate %}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.{% endblocktranslate %}</p>
|
||||
|
||||
{% endblock %}
|
||||
21
benchcoach/templates/account/verified_email_required.html
Normal file
21
benchcoach/templates/account/verified_email_required.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %}
|
||||
|
||||
{% block inner %}
|
||||
<h1>{% translate "Verify Your E-mail Address" %}</h1>
|
||||
|
||||
{% url 'account_email' as email_url %}
|
||||
|
||||
<p>{% blocktranslate %}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. {% endblocktranslate %}</p>
|
||||
|
||||
<p>{% blocktranslate %}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.{% endblocktranslate %}</p>
|
||||
|
||||
<p>{% blocktranslate %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>.{% endblocktranslate %}</p>
|
||||
{% endblock %}
|
||||
112
benchcoach/templates/base.html
Normal file
112
benchcoach/templates/base.html
Normal file
@@ -0,0 +1,112 @@
|
||||
{% load static i18n %}<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>{% block title %}BenchCoach{% endblock title %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="An assistant coach for TeamSnap">
|
||||
<meta name="author" content="Anthony Correa">
|
||||
|
||||
<link rel="icon" href="{% static 'images/favicons/favicon.ico' %}">
|
||||
|
||||
{% block css %}
|
||||
<!-- Latest compiled and minified Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- Your stuff: Third-party CSS libraries go here -->
|
||||
|
||||
<!-- This file stores project-specific CSS -->
|
||||
<link href="{% static 'css/project.css' %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
<!-- Le javascript
|
||||
================================================== -->
|
||||
{# Placed at the top of the document so pages load faster with defer #}
|
||||
{% block javascript %}
|
||||
<!-- Bootstrap JS -->
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.min.js" integrity="sha512-OvBgP9A2JBgiRad/mM36mkzXSXaJE9BEIENnVEmeZdITvwT09xnxLtT4twkCa8m/loMbPHsvPl0T8lRGVBwjlQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<!-- Your stuff: Third-party javascript libraries go here -->
|
||||
|
||||
<!-- place project specific Javascript in this file -->
|
||||
<script defer src="{% static 'js/project.js' %}"></script>
|
||||
|
||||
{% endblock javascript %}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="mb-1">
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-bs-toggle="collapse" data-bs-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' %}">BenchCoach</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="visually-hidden">(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 %}">{% translate "My Profile" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if ACCOUNT_ALLOW_REGISTRATION %}
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% translate "Sign Up" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible {% if message.tags %}alert-{{ message.tags }}{% endif %}">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></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 %}
|
||||
|
||||
{% block inline_javascript %}
|
||||
{% comment %}
|
||||
Script tags with only code, no src (defer by default). To run
|
||||
with a "defer" so that you run inline code:
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {/* Run whatever you want */});
|
||||
</script>
|
||||
{% endcomment %}
|
||||
{% endblock inline_javascript %}
|
||||
</body>
|
||||
</html>
|
||||
1
benchcoach/templates/pages/about.html
Normal file
1
benchcoach/templates/pages/about.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "base.html" %}
|
||||
1
benchcoach/templates/pages/home.html
Normal file
1
benchcoach/templates/pages/home.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "base.html" %}
|
||||
34
benchcoach/templates/users/user_detail.html
Normal file
34
benchcoach/templates/users/user_detail.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% 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 %}
|
||||
17
benchcoach/templates/users/user_form.html
Normal file
17
benchcoach/templates/users/user_form.html
Normal file
@@ -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 %}
|
||||
0
benchcoach/users/__init__.py
Normal file
0
benchcoach/users/__init__.py
Normal file
16
benchcoach/users/adapters.py
Normal file
16
benchcoach/users/adapters.py
Normal file
@@ -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)
|
||||
34
benchcoach/users/admin.py
Normal file
34
benchcoach/users/admin.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from benchcoach.users.forms import UserAdminChangeForm, UserAdminCreationForm
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(auth_admin.UserAdmin):
|
||||
|
||||
form = UserAdminChangeForm
|
||||
add_form = UserAdminCreationForm
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(_("Personal info"), {"fields": ("name", "email")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
"fields": (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
),
|
||||
},
|
||||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
list_display = ["username", "name", "is_superuser"]
|
||||
search_fields = ["name"]
|
||||
13
benchcoach/users/apps.py
Normal file
13
benchcoach/users/apps.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = "benchcoach.users"
|
||||
verbose_name = _("Users")
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
import benchcoach.users.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
||||
8
benchcoach/users/context_processors.py
Normal file
8
benchcoach/users/context_processors.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def allauth_settings(request):
|
||||
"""Expose some settings from django-allauth in templates."""
|
||||
return {
|
||||
"ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION,
|
||||
}
|
||||
42
benchcoach/users/forms.py
Normal file
42
benchcoach/users/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from allauth.account.forms import SignupForm
|
||||
from allauth.socialaccount.forms import SignupForm as SocialSignupForm
|
||||
from django.contrib.auth import forms as admin_forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserAdminChangeForm(admin_forms.UserChangeForm):
|
||||
class Meta(admin_forms.UserChangeForm.Meta):
|
||||
model = User
|
||||
|
||||
|
||||
class UserAdminCreationForm(admin_forms.UserCreationForm):
|
||||
"""
|
||||
Form for User Creation in the Admin Area.
|
||||
To change user signup, see UserSignupForm and UserSocialSignupForm.
|
||||
"""
|
||||
|
||||
class Meta(admin_forms.UserCreationForm.Meta):
|
||||
model = User
|
||||
|
||||
error_messages = {
|
||||
"username": {"unique": _("This username has already been taken.")}
|
||||
}
|
||||
|
||||
|
||||
class UserSignupForm(SignupForm):
|
||||
"""
|
||||
Form that will be rendered on a user sign up section/screen.
|
||||
Default fields will be added automatically.
|
||||
Check UserSocialSignupForm for accounts created from social.
|
||||
"""
|
||||
|
||||
|
||||
class UserSocialSignupForm(SocialSignupForm):
|
||||
"""
|
||||
Renders the form when user has signed up using social accounts.
|
||||
Default fields will be added automatically.
|
||||
See UserSignupForm otherwise.
|
||||
"""
|
||||
125
benchcoach/users/migrations/0001_initial.py
Normal file
125
benchcoach/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Generated by Django 3.2.9 on 2021-11-20 11:23
|
||||
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", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
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",
|
||||
),
|
||||
),
|
||||
(
|
||||
"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": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
benchcoach/users/migrations/__init__.py
Normal file
0
benchcoach/users/migrations/__init__.py
Normal file
26
benchcoach/users/models.py
Normal file
26
benchcoach/users/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db.models import CharField
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Default custom user model for BenchCoach.
|
||||
If adding fields that need to be filled at user signup,
|
||||
check forms.SignupForm and forms.SocialSignupForms accordingly.
|
||||
"""
|
||||
|
||||
#: First and last name do not cover name patterns around the globe
|
||||
name = CharField(_("Name of User"), blank=True, max_length=255)
|
||||
first_name = None # type: ignore
|
||||
last_name = None # type: ignore
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get url for user's detail view.
|
||||
|
||||
Returns:
|
||||
str: URL for user detail.
|
||||
|
||||
"""
|
||||
return reverse("users:detail", kwargs={"username": self.username})
|
||||
0
benchcoach/users/tests/__init__.py
Normal file
0
benchcoach/users/tests/__init__.py
Normal file
32
benchcoach/users/tests/factories.py
Normal file
32
benchcoach/users/tests/factories.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Any, Sequence
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from factory import Faker, post_generation
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
|
||||
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 = (
|
||||
extracted
|
||||
if extracted
|
||||
else Faker(
|
||||
"password",
|
||||
length=42,
|
||||
special_chars=True,
|
||||
digits=True,
|
||||
upper_case=True,
|
||||
lower_case=True,
|
||||
).evaluate(None, None, extra={"locale": None})
|
||||
)
|
||||
self.set_password(password)
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
django_get_or_create = ["username"]
|
||||
40
benchcoach/users/tests/test_admin.py
Normal file
40
benchcoach/users/tests/test_admin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from benchcoach.users.models import User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class TestUserAdmin:
|
||||
def test_changelist(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_search(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url, data={"q": "test"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_add(self, admin_client):
|
||||
url = reverse("admin:users_user_add")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = admin_client.post(
|
||||
url,
|
||||
data={
|
||||
"username": "test",
|
||||
"password1": "My_R@ndom-P@ssw0rd",
|
||||
"password2": "My_R@ndom-P@ssw0rd",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert User.objects.filter(username="test").exists()
|
||||
|
||||
def test_view_user(self, admin_client):
|
||||
user = User.objects.get(username="admin")
|
||||
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == 200
|
||||
39
benchcoach/users/tests/test_forms.py
Normal file
39
benchcoach/users/tests/test_forms.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Module for all Form Tests.
|
||||
"""
|
||||
import pytest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from benchcoach.users.forms import UserAdminCreationForm
|
||||
from benchcoach.users.models import User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class TestUserAdminCreationForm:
|
||||
"""
|
||||
Test class for all tests related to the UserAdminCreationForm
|
||||
"""
|
||||
|
||||
def test_username_validation_error_msg(self, user: User):
|
||||
"""
|
||||
Tests UserAdminCreation Form's unique validator functions correctly by testing:
|
||||
1) A new user with an existing username cannot be added.
|
||||
2) Only 1 error is raised by the UserCreation Form
|
||||
3) The desired error message is raised
|
||||
"""
|
||||
|
||||
# The user already exists,
|
||||
# hence cannot be created.
|
||||
form = UserAdminCreationForm(
|
||||
{
|
||||
"username": user.username,
|
||||
"password1": user.password,
|
||||
"password2": user.password,
|
||||
}
|
||||
)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert len(form.errors) == 1
|
||||
assert "username" in form.errors
|
||||
assert form.errors["username"][0] == _("This username has already been taken.")
|
||||
9
benchcoach/users/tests/test_models.py
Normal file
9
benchcoach/users/tests/test_models.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from benchcoach.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}/"
|
||||
24
benchcoach/users/tests/test_urls.py
Normal file
24
benchcoach/users/tests/test_urls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from django.urls import resolve, reverse
|
||||
|
||||
from benchcoach.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"
|
||||
98
benchcoach/users/tests/test_views.py
Normal file
98
benchcoach/users/tests/test_views.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from benchcoach.users.forms import UserAdminChangeForm
|
||||
from benchcoach.users.models import User
|
||||
from benchcoach.users.tests.factories import UserFactory
|
||||
from benchcoach.users.views import UserRedirectView, UserUpdateView, user_detail_view
|
||||
|
||||
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 dummy_get_response(self, request: HttpRequest):
|
||||
return None
|
||||
|
||||
def test_get_success_url(self, user: User, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.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, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
|
||||
assert view.get_object() == user
|
||||
|
||||
def test_form_valid(self, user: User, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.get("/fake-url/")
|
||||
|
||||
# Add the session/message middleware to the request
|
||||
SessionMiddleware(self.dummy_get_response).process_request(request)
|
||||
MessageMiddleware(self.dummy_get_response).process_request(request)
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
|
||||
# Initialize the form
|
||||
form = UserAdminChangeForm()
|
||||
form.cleaned_data = []
|
||||
view.form_valid(form)
|
||||
|
||||
messages_sent = [m.message for m in messages.get_messages(request)]
|
||||
assert messages_sent == ["Information successfully updated"]
|
||||
|
||||
|
||||
class TestUserRedirectView:
|
||||
def test_get_redirect_url(self, user: User, rf: RequestFactory):
|
||||
view = UserRedirectView()
|
||||
request = rf.get("/fake-url")
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
|
||||
assert view.get_redirect_url() == f"/users/{user.username}/"
|
||||
|
||||
|
||||
class TestUserDetailView:
|
||||
def test_authenticated(self, user: User, rf: RequestFactory):
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = UserFactory()
|
||||
|
||||
response = user_detail_view(request, username=user.username)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_not_authenticated(self, user: User, rf: RequestFactory):
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = AnonymousUser()
|
||||
|
||||
response = user_detail_view(request, username=user.username)
|
||||
login_url = reverse(settings.LOGIN_URL)
|
||||
|
||||
assert isinstance(response, HttpResponseRedirect)
|
||||
assert response.status_code == 302
|
||||
assert response.url == f"{login_url}?next=/fake-url/"
|
||||
14
benchcoach/users/urls.py
Normal file
14
benchcoach/users/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from benchcoach.users.views import (
|
||||
user_detail_view,
|
||||
user_redirect_view,
|
||||
user_update_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"),
|
||||
]
|
||||
48
benchcoach/users/views.py
Normal file
48
benchcoach/users/views.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, RedirectView, UpdateView
|
||||
|
||||
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, SuccessMessageMixin, UpdateView):
|
||||
|
||||
model = User
|
||||
fields = ["name"]
|
||||
success_message = _("Information successfully updated")
|
||||
|
||||
def get_success_url(self):
|
||||
assert (
|
||||
self.request.user.is_authenticated
|
||||
) # for mypy to know that the user is authenticated
|
||||
return self.request.user.get_absolute_url()
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
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()
|
||||
0
benchcoach/utils/__init__.py
Normal file
0
benchcoach/utils/__init__.py
Normal file
Reference in New Issue
Block a user