summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Sargent <philip.sargent@gmail.com>2025-01-23 23:38:06 +0000
committerPhilip Sargent <philip.sargent@gmail.com>2025-01-23 23:38:06 +0000
commita5d0ad3e4f92147ea07a13e236389914ab4a57b7 (patch)
tree6b6cb4e23aecdd90da9c2f09b8ab730dab6de380
parentf842dab12a8bd807a0bb61a6eb35600b789564b0 (diff)
downloadtroggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.tar.gz
troggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.tar.bz2
troggle-a5d0ad3e4f92147ea07a13e236389914ab4a57b7.zip
New User registration form
-rw-r--r--core/views/user_registration.py90
-rw-r--r--parsers/people.py7
-rw-r--r--parsers/users.py13
-rw-r--r--templates/login/register.html58
-rw-r--r--urls.py43
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('&auml;', '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 &rarr;
+ </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 &rarr;
</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>
diff --git a/urls.py b/urls.py
index 88bbb71..8841093 100644
--- a/urls.py
+++ b/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"),