mirror of
https://expo.survex.com/repositories/troggle/.git
synced 2025-12-15 12:57:07 +00:00
New User registration form
This commit is contained in:
@@ -6,9 +6,11 @@ from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
|
||||
from troggle.core.models.troggle import DataIssue, Person
|
||||
from troggle.parsers.users import register_user, get_encryptor, ENCRYPTED_DIR, USERS_FILE
|
||||
from troggle.parsers.people import troggle_slugify
|
||||
from troggle.core.utils import (
|
||||
add_commit,
|
||||
)
|
||||
@@ -23,6 +25,23 @@ todo = """
|
||||
- login automatically, and redirect to control panel ?
|
||||
"""
|
||||
|
||||
class ExpoPasswordResetForm(PasswordResetForm):
|
||||
"""Because we are using the Django-standard django.contrib.auth mechanisms, the way Django wants us to
|
||||
modify them is to subclass their stuff and insert our extras in teh subclasses. This is completely
|
||||
unlike how the rest of troggle works because we avoid Class-based views.
|
||||
We avoid them because they are very hard to debug for newcomer programmers who don't know Django:
|
||||
the logic is spread out up a tree of preceding ancestor classes.
|
||||
|
||||
This is where we would override the template so make the form look visually like the rest of troggle
|
||||
"""
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
# Add custom validation logic etc.
|
||||
print(f" * ExpoPasswordResetForm PASSWORD reset email posted '{email=}'")
|
||||
# method_list = [attribute for attribute in dir(PasswordResetForm) if callable(getattr(PasswordResetForm, attribute)) and attribute.startswith('__') is False]
|
||||
# print(method_list)
|
||||
return email
|
||||
|
||||
def reset_done(request):
|
||||
"""This page is called when a password reset has successively occured
|
||||
Unfortunately by this point, we do not know the name of the user who initiated the
|
||||
@@ -38,8 +57,43 @@ def reset_done(request):
|
||||
"""
|
||||
current_user = request.user
|
||||
save_users(request, current_user)
|
||||
return HttpResponseRedirect("/accounts/login/")
|
||||
if updated_user.is_anonymous:
|
||||
# What we expect, for a completely new user
|
||||
return HttpResponseRedirect("/accounts/login/?next=/handbook/troggle/training/trogbegin.html")
|
||||
else:
|
||||
# This would be for someone already looged in "expo" for example
|
||||
return HttpResponseRedirect("/handbook/troggle/training/trogbegin.html")
|
||||
|
||||
def newregister(request, username=None):
|
||||
"""To register a COMPLETELY new user on the troggle system,
|
||||
WITHOUT any previous expo attendance.
|
||||
"""
|
||||
current_user = request.user # if not logged in, this is 'AnonymousUser'
|
||||
warning = ""
|
||||
|
||||
if request.method == "POST":
|
||||
form = newregister_form(request.POST)
|
||||
if form.is_valid():
|
||||
fullname = form.cleaned_data["fullname"]
|
||||
email = form.cleaned_data["email"]
|
||||
|
||||
nameslug = troggle_slugify(fullname)
|
||||
print(f"NEW user slug {nameslug}")
|
||||
|
||||
expoers = User.objects.filter(username=nameslug)
|
||||
if len(expoers) != 0:
|
||||
# Disallow a name which already exists, use the other form.
|
||||
return HttpResponseRedirect(f"/accounts/register/{nameslug}")
|
||||
# create User in the system and refresh stored encrypted user list and git commit it:
|
||||
updated_user = register_user(nameslug, email, password=None, pwhash=None, fullname=fullname)
|
||||
save_users(request, updated_user, email)
|
||||
return HttpResponseRedirect("/accounts/password_reset/")
|
||||
else: # GET
|
||||
form = newregister_form(initial={"visible": "True"})
|
||||
|
||||
return render(request, "login/register.html", {"form": form, "warning": warning, "newuser": True})
|
||||
|
||||
|
||||
def register(request, username=None):
|
||||
"""To register a new user on the troggle system, similar to the "expo" user
|
||||
(with cavey:beery password) but specific to an individual
|
||||
@@ -65,8 +119,8 @@ def register(request, username=None):
|
||||
save_users(request, updated_user, email)
|
||||
# to do, login automatically, and redirect to control panel ?
|
||||
return HttpResponseRedirect("/accounts/login/")
|
||||
else:
|
||||
if username:
|
||||
else: # GET
|
||||
if username: # if provided in URL
|
||||
if not current_user.is_anonymous:
|
||||
warning = f"WARNING - you are logged-in as someone else '{current_user}'. You must logout and login again as '{username}' "
|
||||
print(f"REGISTER: {warning}")
|
||||
@@ -126,6 +180,34 @@ def write_users(registered_users, encryptedfile, git_string):
|
||||
raise
|
||||
return True
|
||||
|
||||
class newregister_form(forms.Form): # not a model-form, just a form-form
|
||||
fullname = forms.CharField(strip=True, required=True,
|
||||
label="Forename Surname",
|
||||
widget=forms.TextInput(
|
||||
attrs={"size": 35, "placeholder": "e.g. Anathema Device",
|
||||
"style": "vertical-align: text-top;"}
|
||||
))
|
||||
email = forms.CharField(strip=True, required=True,
|
||||
label="email",
|
||||
widget=forms.TextInput(
|
||||
attrs={"size": 35, "placeholder": "e.g. anathema@tackle_store.expo",
|
||||
"style": "vertical-align: text-top;"}
|
||||
))
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
un = cleaned_data.get("fullname")
|
||||
|
||||
# expoers = Person.objects.filter(slug=un)
|
||||
# if len(expoers) == 0:
|
||||
# raise ValidationError(
|
||||
# "Sorry, we are not registering new people yet. Try again next week. We are still getting the bugs out of this.."
|
||||
# )
|
||||
# if len(expoers) != 1:
|
||||
# raise ValidationError(
|
||||
# "Sorry, that troggle identifier has duplicates. Contact a nerd on the Nerd email list, or (better) the Matrix website chat."
|
||||
# )
|
||||
|
||||
class register_form(forms.Form): # not a model-form, just a form-form
|
||||
username = forms.CharField(strip=True, required=True,
|
||||
label="Username",
|
||||
@@ -170,7 +252,7 @@ class register_form(forms.Form): # not a model-form, just a form-form
|
||||
expoers = Person.objects.filter(slug=un)
|
||||
if len(expoers) == 0:
|
||||
raise ValidationError(
|
||||
"Sorry, we are not registering new people yet. Try again next week. We are still getting the bugs out of this.."
|
||||
"Sorry, this is the form for people who have already been to expo. Use the New User registration form (link above)."
|
||||
)
|
||||
if len(expoers) != 1:
|
||||
raise ValidationError(
|
||||
|
||||
@@ -79,7 +79,8 @@ def troggle_slugify(longname):
|
||||
slug = slug.replace('ä', 'a')
|
||||
slug = slug.replace('&', '') # otherwise just remove the &
|
||||
slug = slug.replace(';', '') # otherwise just remove the ;
|
||||
slug = re.sub(r'<[^>]*>','',slug) # remove <span-lang = "hu">
|
||||
slug = slug.replace("'", "") # otherwise just remove the ', no O'Reilly problem # NEW
|
||||
slug = re.sub(r'<[^>]*>','',slug) # remove <span-lang = "hu"> and any HTML tags
|
||||
slug=slug.strip("-") # remove spare hyphens
|
||||
|
||||
if len(slug) > 40: # slugfield is 50 chars
|
||||
@@ -120,9 +121,11 @@ def load_people_expos():
|
||||
e = Expedition.objects.create(**otherAttribs, **coUniqueAttribs)
|
||||
|
||||
print(" - Loading personexpeditions")
|
||||
|
||||
|
||||
for personline in personreader:
|
||||
# This is all horrible: refactor it.
|
||||
# CSV: Name,Lastname,Guest,VfHO member,Mugshot,..
|
||||
# e.g: Olly Betts (Ol),Betts,,,l/ollybetts.htm,
|
||||
name = personline[header["Name"]]
|
||||
plainname = re.sub(r"<.*?>", "", name) # now in slugify
|
||||
|
||||
|
||||
@@ -25,20 +25,25 @@ todo = """
|
||||
USERS_FILE = "users.json"
|
||||
ENCRYPTED_DIR = "encrypted"
|
||||
|
||||
def register_user(u, email, password=None, pwhash=None):
|
||||
def register_user(u, email, password=None, pwhash=None, fullname=None):
|
||||
"""Create User and we may not have a Person to tie it to if it is a future caver.
|
||||
Do not use the lastname field, put the whole free text identification into firstname
|
||||
as this saves hassle and works with Wookey too
|
||||
"""
|
||||
try:
|
||||
if existing_user := User.objects.filter(username=u): # WALRUS
|
||||
print(f" - deleting existing user '{existing_user[0]}' before importing")
|
||||
existing_user[0].delete()
|
||||
user = User.objects.create_user(u, email)
|
||||
user = User.objects.create_user(u, email, first_name=fullname)
|
||||
if pwhash:
|
||||
user.password = pwhash
|
||||
elif password:
|
||||
user.set_password(password) # function creates hash and stores hash
|
||||
print(f" # hashing provided clear-text password {password} to make pwhash for user {u}")
|
||||
else:
|
||||
user.set_password('secret') # function creates hash and stores hash
|
||||
print(f" # hashing 'secret' password to make pwhash for user {u}")
|
||||
# user.set_password(None) # use Django special setting for invalid password, but then FAILS to send password reset email
|
||||
user.set_password("secret") # Why is the Django logic broken. Hmph.
|
||||
print(f" # setting INVALID password for user {u}, must be reset by password_reset")
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
user.save()
|
||||
|
||||
@@ -37,21 +37,30 @@ li {color:red}
|
||||
</style>
|
||||
|
||||
<div class='middle'>
|
||||
<h2>User registration - for a personal login to Troggle</h2>
|
||||
<h2>{% if newuser %}
|
||||
New User Registration <br />for someone who has never attended Expo
|
||||
{% else %}
|
||||
User Registration - for a personal login to Troggle
|
||||
{%endif %}</h2>
|
||||
<!--using template login/register.html -->
|
||||
</div>
|
||||
|
||||
<h3>Register a password and your email address</h3>
|
||||
<h3>Register your email address</h3>
|
||||
{% if unauthorized %}
|
||||
<span style="color:red">
|
||||
UNAUTHORIZED attempt to change password or email address. <br />
|
||||
You are not logged in as the user you are attempting to re-register.
|
||||
</span>{% endif %}
|
||||
|
||||
{% if newuser %}
|
||||
<p>You need to register before you can fill out the 'signup' form to request to attend Expo.
|
||||
{% else %}
|
||||
<p>For previous expoers, your username must be your 'troggle id' as listed on the <a href='/people_ids'>past expoers list</a>
|
||||
<p>For new people wanting to come to expo for the first time, please use the <a href="/accounts/newregister/">New User</a> registration form
|
||||
{%endif %}
|
||||
<p>This will eventually sign you up automatically to the
|
||||
<a href="https://lists.wookware.org/cgi-bin/mailman/roster/expo">expo email list</a>.
|
||||
So type in the same email address that you use there.
|
||||
So type in the same email address that you use there if you have already signed up to that.
|
||||
<p>
|
||||
<span style="color:red">
|
||||
{{ warning }}
|
||||
@@ -62,9 +71,18 @@ So type in the same email address that you use there.
|
||||
{{form.as_p}}
|
||||
|
||||
<div class='align-right'>
|
||||
{% if newuser %}
|
||||
{% else %}
|
||||
<input type="checkbox" checked name="visible" onclick="myFunction()">Make Passwords visible (on this form only)
|
||||
|
||||
{%endif %}
|
||||
<br /><br />
|
||||
{% if newuser %}
|
||||
|
||||
<button class="fancybutton" style="padding: 0.5em 25px; font-size: 100%;"
|
||||
onclick="window.location.href='/accounts/password_reset/'" value = "Go to" >
|
||||
Get login token by email →
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="fancybutton" style="padding: 0.5em 25px; font-size: 100%;"
|
||||
onclick="window.location.href='/accounts/password_reset/'" value = "Go to" >
|
||||
Reset password
|
||||
@@ -73,11 +91,20 @@ So type in the same email address that you use there.
|
||||
<button class="fancybutton" style="padding: 0.5em 25px; font-size: 100%;" type = "submit" >
|
||||
Register →
|
||||
</button>
|
||||
{%endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div style='width: 50em' align="left">
|
||||
|
||||
<h3>Your name</h3>
|
||||
{% if newuser %}
|
||||
<p>Use the name you are usually known by "officially": this is usually the name on your passport.
|
||||
But if you habitually use your second forename, not the first forename, use that.
|
||||
Yes, you can put in any 'de', 'van' or 'von' or whatever 'nobiliary particle' you have, or a hyphenated surname; but this must be your real name.
|
||||
Nicknames and fun names are done later.
|
||||
{% else %}
|
||||
<p>For previous expoers, your username must be your 'troggle id' as listed on the <a href='/people_ids'>past expoers list</a>
|
||||
{%endif %}
|
||||
<p>Unfortunately cavers tend to use weird and playful names when signing up for things,
|
||||
so we can't automatically connect the troggle names and ids with the email addresses
|
||||
on the email list. And we don't believe in signing people up for things without their
|
||||
@@ -93,20 +120,33 @@ right now.
|
||||
|
||||
<h3>Students !</h3>
|
||||
Please do not use an email address which will expire when you leave your current institution.
|
||||
This will happen much sooner than you realise. If you realise that you have done this on the email list,
|
||||
This will happen much sooner than you can possibly believe. If you realise that you have done this on the email list,
|
||||
you can change it at the bottom of <a href="https://lists.wookware.org/cgi-bin/mailman/listinfo/expo">this page</a>.
|
||||
|
||||
{% if newuser %}
|
||||
<h3>What happens next</h3>
|
||||
<p>Clicking the big blue button will send you an email which will contain a login token.
|
||||
Click on the link in the email and you will be able to set your own login password.
|
||||
Use this to login to troggle and go to the Expo Signup form.
|
||||
{% else %}
|
||||
{%endif %}
|
||||
|
||||
<h3>Security note</h3>
|
||||
We never store passwords at all, we only store a cryptographic hash.
|
||||
We do store your email address but only 'in clear' inside the live database online
|
||||
where it is accessible only to the database administrators. There is no troggle report
|
||||
which publishes your email address.
|
||||
For permanent storage all email addresses are encrypted. Your troggle
|
||||
For permanent storage all email addresses are encrypted. Your real name and troggle
|
||||
username is public however, and we do not have anonymous people attending expo.
|
||||
<p>The password we are asking for is used only to log on to troggle to keep track of
|
||||
<p>The password we
|
||||
{% if newuser %}will be{% else %}are{%endif %}
|
||||
asking for is used only to log on to troggle to keep track of
|
||||
who is editing the current expo records, website content, historic survey data and
|
||||
when using the expo kanban software. It is not the same as the password to access your email
|
||||
and it is not the same as the password you use to interact with the expo email list.
|
||||
with your email provider
|
||||
and it is not the same as the password you use to interact with the expo
|
||||
<a href="https://lists.wookware.org/cgi-bin/mailman/roster/expo">email list</a>.
|
||||
|
||||
<span style="color:red">
|
||||
{{ form.non_field_errors }} <!-- form validation errors appear here, and also at the top of the form-->
|
||||
</span>
|
||||
|
||||
43
urls.py
43
urls.py
@@ -1,6 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import PasswordResetView # class-based view
|
||||
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from troggle.core.views import statistics, survex
|
||||
@@ -53,7 +55,7 @@ from troggle.core.views.logbooks import (
|
||||
)
|
||||
from troggle.core.views.other import controlpanel, exportlogbook, frontpage, todos
|
||||
from troggle.core.views.prospect import prospecting
|
||||
from troggle.core.views.user_registration import register, reset_done
|
||||
from troggle.core.views.user_registration import register, newregister, reset_done, ExpoPasswordResetForm
|
||||
from troggle.core.views.scans import allscans, cavewallets, scansingle, walletslistperson, walletslistyear
|
||||
from troggle.core.views.signup import signup
|
||||
from troggle.core.views.uploads import dwgupload, expofilerename, gpxupload, photoupload
|
||||
@@ -106,24 +108,6 @@ else:
|
||||
path('<path:filepath>', expofilessingle, name="single"), # local copy of EXPOFILES
|
||||
]
|
||||
|
||||
# see https://docs.djangoproject.com/en/dev/topics/auth/default/tiny
|
||||
# The URLs provided by include('django.contrib.auth.urls') are:
|
||||
#
|
||||
# accounts/login/ [name='login']
|
||||
# accounts/logout/ [name='logout']
|
||||
# accounts/password_change/ [name='password_change']
|
||||
# accounts/password_change/done/ [name='password_change_done']
|
||||
# accounts/password_reset/ [name='password_reset']
|
||||
# accounts/password_reset/done/ [name='password_reset_done']
|
||||
# accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
|
||||
# accounts/reset/done/ [name='password_reset_complete']
|
||||
|
||||
# BUT many of these are set up by opinionated Django even if 'django.contrib.auth.urls' is NOT included.
|
||||
# Some overlap with 'admin.site.urls' needs to be investigated.
|
||||
|
||||
# admin.site.urls is urls() which maps to get_urls() which is a function declared
|
||||
# in django/contrib/admin/sites.py which for me is
|
||||
# /home/philip/expo/troggle/.venv/lib/python3.xx/site-packages/django/contrib/admin/sites.py
|
||||
|
||||
trogglepatterns = [
|
||||
path('pubs.htm', pubspage, name="pubspage"), # ~165 hrefs to this url in expoweb files
|
||||
@@ -164,13 +148,34 @@ trogglepatterns = [
|
||||
# Renaming an uploaded file
|
||||
path('expofilerename/<path:filepath>', expofilerename, name='expofilerename'),
|
||||
|
||||
# see https://docs.djangoproject.com/en/dev/topics/auth/default/tiny
|
||||
# The URLs provided by include('django.contrib.auth.urls') are:
|
||||
#
|
||||
# accounts/login/ [name='login']
|
||||
# accounts/logout/ [name='logout']
|
||||
# accounts/password_change/ [name='password_change']
|
||||
# accounts/password_change/done/ [name='password_change_done']
|
||||
# accounts/password_reset/ [name='password_reset']
|
||||
# accounts/password_reset/done/ [name='password_reset_done']
|
||||
# accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
|
||||
# accounts/reset/done/ [name='password_reset_complete']
|
||||
|
||||
# BUT many of these are set up by opinionated Django even if 'django.contrib.auth.urls' is NOT included.
|
||||
# Some overlap with 'admin.site.urls' needs to be investigated.
|
||||
|
||||
# admin.site.urls is urls() which maps to get_urls() which is a function declared
|
||||
# in django/contrib/admin/sites.py which for me is
|
||||
# /home/philip/expo/troggle/.venv/lib/python3.xx/site-packages/django/contrib/admin/sites.py
|
||||
|
||||
# setting LOGIN_URL = '/accounts/login/' is default.
|
||||
# NB setting url pattern name to 'login' instea dof 'expologin' with override Django, see https://docs.djangoproject.com/en/dev/topics/http/urls/#naming-url-patterns
|
||||
path('accounts/logout/', expologout, name='expologout'), # same as in django.contrib.auth.urls
|
||||
path('accounts/login/', expologin, name='expologin'), # same as in django.contrib.auth.urls
|
||||
path("accounts/register/<slug:username>", register, name="re_register"), # overriding django.contrib.auth.urls
|
||||
path("accounts/register/", register, name="register"), # overriding django.contrib.auth.urls
|
||||
path("accounts/newregister/", newregister, name="newregister"),
|
||||
path("accounts/reset/done/", reset_done, name="password_reset_done"), # overriding django.contrib.auth.urls
|
||||
path('accounts/password_reset/', PasswordResetView.as_view(form_class=ExpoPasswordResetForm), name='password_reset'),
|
||||
path('accounts/', include('django.contrib.auth.urls')), # see line 109 in this file NB initial "/accounts/" in URL
|
||||
|
||||
path('person/<slug:slug>', person, name="person"),
|
||||
|
||||
Reference in New Issue
Block a user