diff options
author | Philip Sargent <philip.sargent@gmail.com> | 2025-01-23 23:38:06 +0000 |
---|---|---|
committer | Philip Sargent <philip.sargent@gmail.com> | 2025-01-23 23:38:06 +0000 |
commit | a5d0ad3e4f92147ea07a13e236389914ab4a57b7 (patch) | |
tree | 6b6cb4e23aecdd90da9c2f09b8ab730dab6de380 | |
parent | f842dab12a8bd807a0bb61a6eb35600b789564b0 (diff) | |
download | troggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.tar.gz troggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.tar.bz2 troggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.zip |
New User registration form
-rw-r--r-- | core/views/user_registration.py | 90 | ||||
-rw-r--r-- | parsers/people.py | 7 | ||||
-rw-r--r-- | parsers/users.py | 13 | ||||
-rw-r--r-- | templates/login/register.html | 58 | ||||
-rw-r--r-- | urls.py | 43 |
5 files changed, 173 insertions, 38 deletions
diff --git a/core/views/user_registration.py b/core/views/user_registration.py index 82381ad..f54e6da 100644 --- a/core/views/user_registration.py +++ b/core/views/user_registration.py @@ -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(
diff --git a/parsers/people.py b/parsers/people.py index 96bda41..4a4512c 100644 --- a/parsers/people.py +++ b/parsers/people.py @@ -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 diff --git a/parsers/users.py b/parsers/users.py index fe11bdf..cf721ba 100644 --- a/parsers/users.py +++ b/parsers/users.py @@ -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() diff --git a/templates/login/register.html b/templates/login/register.html index bc3fe81..c7b4702 100644 --- a/templates/login/register.html +++ b/templates/login/register.html @@ -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> @@ -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"), |