diff --git a/project/settings/base.py b/project/settings/base.py index 158923b..513b05d 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -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' diff --git a/project/templates/base.html b/project/templates/base.html index 21f240c..6e3343c 100644 --- a/project/templates/base.html +++ b/project/templates/base.html @@ -13,11 +13,11 @@ diff --git a/project/templates/index.html b/project/templates/index.html new file mode 100644 index 0000000..94d9808 --- /dev/null +++ b/project/templates/index.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/project/urls.py b/project/urls.py index 0e3265b..41148c9 100644 --- a/project/urls.py +++ b/project/urls.py @@ -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), ] diff --git a/project/views.py b/project/views.py new file mode 100644 index 0000000..b5bf757 --- /dev/null +++ b/project/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render_to_response + + +def index(request): + return render_to_response("index.html") diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..bcbeaca --- /dev/null +++ b/users/forms.py @@ -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",) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 16d03e3..b9ab905 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -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', + }, ), ] diff --git a/users/models.py b/users/models.py index 275e626..5c96291 100644 --- a/users/models.py +++ b/users/models.py @@ -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") diff --git a/users/templates/users/logged_out.html b/users/templates/users/logged_out.html new file mode 100644 index 0000000..9466a31 --- /dev/null +++ b/users/templates/users/logged_out.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +

{% trans "Thanks for spending some quality time with the Web site today." %}

+ +

{% trans 'Log in again' %}

+ +{% endblock %} diff --git a/users/templates/users/login.html b/users/templates/users/login.html new file mode 100644 index 0000000..7f034b4 --- /dev/null +++ b/users/templates/users/login.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} + +{% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} +{% endif %} + +
+{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{# Assumes you setup the password_reset view in your URLconf #} +

Lost password?

+ +{% endblock %} diff --git a/users/templates/users/password_change_done.html b/users/templates/users/password_change_done.html new file mode 100644 index 0000000..d167eed --- /dev/null +++ b/users/templates/users/password_change_done.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} +{% block content %} +

{% trans 'Your password was changed.' %}

+{% endblock %} diff --git a/users/templates/users/password_change_form.html b/users/templates/users/password_change_form.html new file mode 100644 index 0000000..bc002b3 --- /dev/null +++ b/users/templates/users/password_change_form.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} + +{% block content %}
+ +
{% csrf_token %} +
+{% if form.errors %} +

+ {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

+{% endif %} + + +

{% 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." %}

+ +
+ +
+ {{ form.old_password.errors }} + {{ form.old_password.label_tag }} {{ form.old_password }} +
+ +
+ {{ form.new_password1.errors }} + {{ form.new_password1.label_tag }} {{ form.new_password1 }} + {% if form.new_password1.help_text %} +
{{ form.new_password1.help_text|safe }}
+ {% endif %} +
+ +
+{{ form.new_password2.errors }} + {{ form.new_password2.label_tag }} {{ form.new_password2 }} + {% if form.new_password2.help_text %} +
{{ form.new_password2.help_text|safe }}
+ {% endif %} +
+ +
+ +
+ +
+ +
+
+ +{% endblock %} diff --git a/users/templates/users/password_reset_complete.html b/users/templates/users/password_reset_complete.html new file mode 100644 index 0000000..f633333 --- /dev/null +++ b/users/templates/users/password_reset_complete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} + +{% block content %} + +

{% trans "Your password has been set. You may go ahead and log in now." %}

+ +

{% trans 'Log in' %}

+ +{% endblock %} diff --git a/users/templates/users/password_reset_confirm.html b/users/templates/users/password_reset_confirm.html new file mode 100644 index 0000000..990be75 --- /dev/null +++ b/users/templates/users/password_reset_confirm.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} +{% block content %} + +{% if validlink %} + +

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

+ +
{% csrf_token %} +
+
+ {{ form.new_password1.errors }} + + {{ form.new_password1 }} +
+
+ {{ form.new_password2.errors }} + + {{ form.new_password2 }} +
+ +
+
+ +{% else %} + +

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

+ +{% endif %} + +{% endblock %} diff --git a/users/templates/users/password_reset_done.html b/users/templates/users/password_reset_done.html new file mode 100644 index 0000000..c097748 --- /dev/null +++ b/users/templates/users/password_reset_done.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} +{% block content %} + +

{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}

+ +

{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}

+ +{% endblock %} diff --git a/users/templates/users/password_reset_email.html b/users/templates/users/password_reset_email.html new file mode 100644 index 0000000..f1afa47 --- /dev/null +++ b/users/templates/users/password_reset_email.html @@ -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 %} diff --git a/users/templates/users/password_reset_form.html b/users/templates/users/password_reset_form.html new file mode 100644 index 0000000..6f55d5c --- /dev/null +++ b/users/templates/users/password_reset_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

{{ title }}

{% endblock %} +{% block content %} + +

{% trans "Forgotten password?" %}

+ +

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

+ +
{% csrf_token %} + {{ form.email.errors }} + + {{ form.email }} + +
+ +{% endblock %} diff --git a/users/templates/users/signup.html b/users/templates/users/signup.html new file mode 100644 index 0000000..b47dd9b --- /dev/null +++ b/users/templates/users/signup.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +

{% trans "Sign up" %}

+ + {% if form.errors %} + {% endif %} + +
+ {% csrf_token %} + {{ form.as_p }} + +

+ +
+ +

{% trans "Already have an account? Log in..." %}

+ +{% endblock %} diff --git a/users/templates/users/signup_confirm.html b/users/templates/users/signup_confirm.html new file mode 100644 index 0000000..ab0eb7d --- /dev/null +++ b/users/templates/users/signup_confirm.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +

{% trans "Confirm your email" %}

+ +

{% trans "You've got mail - click the link or copy paste it to this browser session and you'll be logged in." %}

+ +{% endblock %} diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..5651d02 --- /dev/null +++ b/users/urls.py @@ -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///', 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'), +] diff --git a/users/views.py b/users/views.py index 91ea44a..631f57e 100644 --- a/users/views.py +++ b/users/views.py @@ -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