Basic user management skeleton #8

Merged
benjaoming merged 2 commits from benjaoming/membersystem:master into master 2019-08-31 18:56:19 +00:00
22 changed files with 428 additions and 19 deletions

View file

@ -14,7 +14,6 @@ import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
@ -55,7 +54,7 @@ ROOT_URLCONF = 'project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "project", "templates")],
'DIRS': [os.path.join("project", "templates")],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -106,6 +105,8 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
AUTH_USER_MODEL = 'users.User'
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
@ -126,6 +127,8 @@ USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
SITE_ID = 1
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

View file

@ -13,11 +13,11 @@
</h1>
<ul>
{% if user.is_authenticated %}
<li><a href="{% url 'account_email' %}">Change e-mail</a></li>
<li><a href="{% url 'account_logout' %}">Sign out</a></li>
<li><a href="{% url 'users:email' %}">Change e-mail</a></li>
<li><a href="{% url 'users:logout' %}">Sign out</a></li>
{% else %}
<li><a href="{% url 'account_login' %}">Sign in</a></li>
<li><a href="{% url 'account_signup' %}">Sign up</a></li>
<li><a href="{% url 'users:login' %}">Sign in</a></li>
<li><a href="{% url 'users:signup' %}">Sign up</a></li>
{% endif %}
</ul>
</header>

View file

@ -0,0 +1 @@
{% extends "base.html" %}

View file

@ -1,7 +1,12 @@
"""URLs for the membersystem"""
from django.contrib import admin
from django.urls import include
from django.urls import path
from . import views
urlpatterns = [
path('', views.index),
path("users/", include("users.urls")),
path('admin/', admin.site.urls),
]

5
project/views.py Normal file
View file

@ -0,0 +1,5 @@
from django.shortcuts import render_to_response
def index(request):
return render_to_response("index.html")

View file

@ -1,3 +1,2 @@
Django==2.0.6
django-money==0.14
django-extensions==2.0.7
Django>=2.2,<2.3
django-money==0.15

19
users/forms.py Normal file
View file

@ -0,0 +1,19 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.tokens import default_token_generator
from django.utils.translation import gettext_lazy as _
from . import models
def get_confirm_code(email):
return default_token_generator(email)[:7]
class SignupForm(UserCreationForm):
username = forms.EmailField(label=_("Email"))
class Meta:
model = models.User
fields = ("username",)

View file

@ -1,6 +1,6 @@
# Generated by Django 2.0.6 on 2018-06-23 19:45
import django.db.models.deletion
from django.conf import settings
# Generated by Django 2.2.4 on 2019-08-31 18:44
import uuid
from django.db import migrations
from django.db import models
@ -10,15 +10,27 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.CreateModel(
name='Profile',
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('nick', models.CharField(blank=True, max_length=60, null=True)),
('email', models.EmailField(help_text='Your email address will be used for password resets and notification about your event/submissions.', max_length=254, unique=True, verbose_name='E-Mail')),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)),
('token_uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('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',
},
),
]

View file

@ -1,5 +1,60 @@
import uuid
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils.translation import gettext_lazy as _
class Profile(models.Model):
user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
class UserManager(BaseUserManager):
"""The user manager class."""
def create_user(self, password: str = None, **kwargs):
user = self.model(**kwargs)
user.set_password(password)
user.save()
return user
def create_superuser(self, password: str, **kwargs):
user = self.create_user(password=password, **kwargs)
user.is_staff = True
user.is_superuser = True
user.save(update_fields=['is_staff', 'is_superuser'])
return user
class User(PermissionsMixin, AbstractBaseUser):
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'email'
objects = UserManager()
nick = models.CharField(max_length=60, null=True, blank=True)
email = models.EmailField(
unique=True,
verbose_name=_('E-Mail'),
help_text=_(
'Your email address will be used for password resets and notification about your event/submissions.'
),
)
is_active = models.BooleanField(default=True)
# For the Django admin...
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
# Used for confirmations and password reminders to NOT disclose email in URL
token_uuid = models.UUIDField(default=uuid.uuid4, editable=False)
def __str__(self) -> str:
"""Use a useful string representation."""
return self.get_display_name()
def get_display_name(self) -> str:
return self.nick if self.nick else str(_('Unnamed user'))
class Meta:
verbose_name = _("User")

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Thanks for spending some quality time with the Web site today." %}</p>
<p><a href="{% url 'admin:index' %}">{% trans 'Log in again' %}</a></p>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'users:login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'users:password_reset' %}">Lost password?</a></p>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans 'Your password was changed.' %}</p>
{% endblock %}

View file

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}<div id="content-main">
<form method="post">{% csrf_token %}
<div>
{% if form.errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
<fieldset class="module aligned wide">
<div class="form-row">
{{ form.old_password.errors }}
{{ form.old_password.label_tag }} {{ form.old_password }}
</div>
<div class="form-row">
{{ form.new_password1.errors }}
{{ form.new_password1.label_tag }} {{ form.new_password1 }}
{% if form.new_password1.help_text %}
<div class="help">{{ form.new_password1.help_text|safe }}</div>
{% endif %}
</div>
<div class="form-row">
{{ form.new_password2.errors }}
{{ form.new_password2.label_tag }} {{ form.new_password2 }}
{% if form.new_password2.help_text %}
<div class="help">{{ form.new_password2.help_text|safe }}</div>
{% endif %}
</div>
</fieldset>
<div class="submit-row">
<input type="submit" value="{% trans 'Change my password' %}" class="default">
</div>
</div>
</form></div>
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
{% if validlink %}
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form method="post">{% csrf_token %}
<fieldset class="module aligned">
<div class="form-row field-password1">
{{ form.new_password1.errors }}
<label for="id_new_password1">{% trans 'New password:' %}</label>
{{ form.new_password1 }}
</div>
<div class="form-row field-password2">
{{ form.new_password2.errors }}
<label for="id_new_password2">{% trans 'Confirm password:' %}</label>
{{ form.new_password2 }}
</div>
<input type="submit" value="{% trans 'Change my password' %}">
</fieldset>
</form>
{% else %}
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

View file

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<h1>{% trans "Forgotten password?" %}</h1>
<p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p>
<form method="post">{% csrf_token %}
{{ form.email.errors }}
<label for="id_email">{% trans 'Email address:' %}</label>
{{ form.email }}
<input type="submit" value="{% trans 'Reset my password' %}">
</form>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Sign up" %}</h1>
{% if form.errors %}
{% endif %}
<form method="post" action="{% url 'users:signup' %}">
{% csrf_token %}
{{ form.as_p }}
<p><button type="submit">{% trans "Confirm email..." %}</button></p>
</form>
<p><a href="{% url 'users:login' %}">{% trans "Already have an account? Log in..." %}</a></p>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Confirm your email" %}</h1>
<p>{% trans "You've got mail - click the link or copy paste it to this browser session and you'll be logged in." %}</p>
{% endblock %}

22
users/urls.py Normal file
View file

@ -0,0 +1,22 @@
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
path('signup/', views.SignupView.as_view(), name='signup'),
path('signup/confirm/', views.SignupConfirmView.as_view(), name='signup_confirm'),
path('login/', auth_views.LoginView.as_view(template_name="users/login.html"), name='login'),
path('logout/', auth_views.LogoutView.as_view(template_name="users/logged_out.html"), name='logout'),
path('password_change/', views.PasswordChangeView.as_view(template_name="users/password_change_form.html"), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name="users/password_change_done.html"), name='password_change_done'),
path('password_reset/', views.PasswordResetView.as_view(template_name="users/password_reset_form.html"), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(template_name="users/password_reset_done.html"), name='password_reset_done'),
path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(template_name="users/password_reset_confirm.html"), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(template_name="users/password_reset_complete.html"), name='password_reset_complete'),
]

View file

@ -1,3 +1,59 @@
from django.shortcuts import render
from django.contrib.auth import views as auth_views
from django.shortcuts import redirect
from django.urls.base import reverse_lazy
from django.views.generic.base import RedirectView
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
# Create your views here.
from . import forms
# from . import email
class PasswordResetView(auth_views.PasswordResetView):
email_template_name = 'users/password_reset_email.html'
success_url = reverse_lazy('users:password_reset_done')
class PasswordResetConfirmView(auth_views.PasswordResetConfirmView):
success_url = reverse_lazy('users:password_reset_complete')
class PasswordChangeView(auth_views.PasswordChangeView):
success_url = reverse_lazy('users:password_change_done')
class SignupView(FormView):
template_name = "users/signup.html"
form_class = forms.SignupForm
def form_valid(self, form):
user = form.save(commit=False)
user.is_active = False
user.set_password(form.cleaned_data['password1'])
user.save()
# mail = email.UserConfirm(user=user)
# mail.send_with_feedback(success_msg=_("An email was sent with a confirmation link"))
self.request.session["user_confirm_pending_id"] = user.id
return redirect("users:signup_confirm")
class SignupConfirmView(TemplateView):
template_name = "users/signup_confirm.html"
class SignupConfirmRedirectView(RedirectView):
def get_redirect_url(self):
uuid = self.kwargs['uuid']
if self.kwargs["token"] == forms.get_confirm_code(uuid):
redirect("users:confirmed") # TODO
redirect("users:confirm_nope") # TODO