summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Sargent <philip.sargent@gmail.com>2025-02-19 16:00:53 +0200
committerPhilip Sargent <philip.sargent@gmail.com>2025-02-19 16:00:53 +0200
commita950cc60d9400c553b853f3a3308edaa8ea645cc (patch)
tree2d9ba95d09d74355fc329b4e3bd093ea4ac30de8
parentcc06e2e1f4bdfbf354d79055595980a1bdef495c (diff)
downloadtroggle-a950cc60d9400c553b853f3a3308edaa8ea645cc.tar.gz
troggle-a950cc60d9400c553b853f3a3308edaa8ea645cc.tar.bz2
troggle-a950cc60d9400c553b853f3a3308edaa8ea645cc.zip
tidy trailing slash everywhere & fix tests
-rw-r--r--_test_response_photo_upload.html63
-rw-r--r--core/TESTS/test_imports.py9
-rw-r--r--core/TESTS/test_logins.py8
-rw-r--r--core/TESTS/test_parsers.py2
-rw-r--r--core/TESTS/test_urls.py7
-rw-r--r--core/TESTS/tests.py2
-rw-r--r--core/middleware.py6
-rw-r--r--core/views/signup.py9
-rw-r--r--settings.py9
-rw-r--r--templates/pagenotfound.html3
-rw-r--r--templates/svxcaves.html2
-rw-r--r--templates/wallet_new.html2
-rw-r--r--templates/wallet_table.html2
-rw-r--r--urls.py62
14 files changed, 139 insertions, 47 deletions
diff --git a/_test_response_photo_upload.html b/_test_response_photo_upload.html
new file mode 100644
index 0000000..9160762
--- /dev/null
+++ b/_test_response_photo_upload.html
@@ -0,0 +1,63 @@
+<!-- expobase.html - this text visible because this template has been included -->
+
+<html lang="en">
+<head>
+<script>document.interestCohort = null;</script> <!-- Turn off Google FLoC -->
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Directory not found photoupload/</title>
+<link rel="stylesheet" type="text/css" href="/css/main2.css" />
+
+</head>
+
+
+
+<body >
+
+<h1>Directory not found 'photoupload/'</h1>
+<h3>Click here: <a href="/photoupload">/photoupload</a> </h3>
+<p>i.e. without the final '/'
+
+
+ <!-- the year now -->
+
+
+<div id="menu">
+<ul id="menulinks">
+<li><a href="/index.htm">Home</a></li>
+<li><a href="/handbook/index.htm">Handbook</a>
+</li>
+<li><a href="/handbook/computing/onlinesystems.html">Online systems</a></li>
+<li><a href="/handbook/logbooks.html#form">Make Logbook Entry</a></li>
+
+
+<li><a href="/caves">Caves</a>
+</li>
+<li><a href="/infodx.htm">Site index</a></li>
+<li><a href="/pubs.htm">Reports</a></li>
+
+
+<li><a href="https://expo.survex.com/kanboard/board/2">Kanboard</a></li>
+
+<li><a href="/handbook/troggle/training/trogbegin.html">Troggle</a></li>
+
+<li><form name=P method=get
+
+action="https://expo.survex.com/search"
+
+
+target="_top">
+ <input id="omega-autofocus" type=search name=P size=8 autofocus>
+ <input type=submit value="Search"></form></li>
+
+
+
+<li> <b style="color:red">RUNNING ON LOCALSERVER</b> <br>slug:<br>newslug:<br>url:
+</ul>
+</div>
+
+
+
+
+</body>
+</html>
+
diff --git a/core/TESTS/test_imports.py b/core/TESTS/test_imports.py
index 322fc9d..f288951 100644
--- a/core/TESTS/test_imports.py
+++ b/core/TESTS/test_imports.py
@@ -96,6 +96,7 @@ class SimpleTest(SimpleTestCase):
import troggle.parsers.QMs
import troggle.parsers.scans
import troggle.parsers.survex
+ import troggle.parsers.users
import troggle.settings
from troggle.parsers.logbooks import GetCaveLookup
@@ -106,6 +107,14 @@ class SimpleTest(SimpleTestCase):
from django.http import HttpResponse
from django.urls import reverse
+ def test_import_users_urls(self):
+ import base64
+ import json
+ import os
+ from cryptography.fernet import Fernet
+ from pathlib import Path
+ from django.contrib.auth.models import User
+
def test_import_urls(self):
from django.conf import settings
diff --git a/core/TESTS/test_logins.py b/core/TESTS/test_logins.py
index 1a153d2..9602b94 100644
--- a/core/TESTS/test_logins.py
+++ b/core/TESTS/test_logins.py
@@ -187,7 +187,7 @@ class PostTests(TestCase):
with open("core/fixtures/test_upload_file.txt", "r") as testf:
response = self.client.post(
- "/photoupload/", data={"name": "test_upload_file.txt", "renameto": "", "uploadfiles": testf}
+ "/photoupload", data={"name": "test_upload_file.txt", "renameto": "", "uploadfiles": testf}
)
content = response.content.decode()
self.assertEqual(response.status_code, HTTPStatus.OK)
@@ -224,7 +224,7 @@ class PostTests(TestCase):
rename = "RENAMED-FILE.JPG"
with open("core/fixtures/test_upload_file.txt", "r") as testf:
response = self.client.post(
- "/photoupload/", data={"name": "test_upload_file.txt", "renameto": rename, "uploadfiles": testf}
+ "/photoupload", data={"name": "test_upload_file.txt", "renameto": rename, "uploadfiles": testf}
)
content = response.content.decode()
self.assertEqual(response.status_code, HTTPStatus.OK)
@@ -255,11 +255,11 @@ class PostTests(TestCase):
self.assertTrue(u.is_active, "User '" + u.username + "' is INACTIVE")
c.login(username=u.username, password="secretword")
- response = self.client.post("/photoupload/", data={"photographer": "GussieFinkNottle"})
+ response = self.client.post("/photoupload", data={"photographer": "GussieFinkNottle"})
content = response.content.decode()
self.assertEqual(response.status_code, HTTPStatus.OK)
# with open('_test_response.html', 'w') as f:
- # f.write(content)
+ # f.write(content)
for ph in [r"Create new Photographer folder", r"/GussieFinkNottle/"]:
phmatch = re.search(ph, content)
self.assertIsNotNone(phmatch, "Failed to find expected text: '" + ph + "'")
diff --git a/core/TESTS/test_parsers.py b/core/TESTS/test_parsers.py
index 66e058d..549095e 100644
--- a/core/TESTS/test_parsers.py
+++ b/core/TESTS/test_parsers.py
@@ -206,7 +206,7 @@ class ImportTest(TestCase):
def test_survexfiles(self):
# Needs another test with test data
- response = self.client.get("/survexfile/caves/")
+ response = self.client.get("/survexfile/caves")
self.assertEqual(response.status_code, HTTPStatus.OK)
content = response.content.decode()
# with open('_test_response.html', 'w') as f:
diff --git a/core/TESTS/test_urls.py b/core/TESTS/test_urls.py
index e7a30cf..861ae2c 100644
--- a/core/TESTS/test_urls.py
+++ b/core/TESTS/test_urls.py
@@ -141,18 +141,17 @@ class URLTests(TestCase):
def test_url_allscans(self):
"""Test the {% url "allscans" %} reverse resolution
- path('survey_scans/', allscans, name="allscans"), # all the scans in all wallets
+ path('survey_scans', allscans, name="allscans"), # all the scans in all wallets
"""
reversed_url = reverse('allscans') # NB _ must be written as - if present in name
- self.assertEqual(reversed_url, "/survey_scans/")
+ self.assertEqual(reversed_url, "/survey_scans")
def test_url_survexcaveslist(self):
"""Test the {% url "allscans" %} reverse resolution
path('survexfile/caves', survex.survexcaveslist, name="survexcaveslist"),
- path('survexfile/caves/', survex.survexcaveslist, name="survexcaveslist"), # auto slash not working
"""
reversed_url = reverse('survexcaveslist') # NB _ must be written as - if present in name
- self.assertEqual(reversed_url, "/survexfile/caves/")
+ self.assertEqual(reversed_url, "/survexfile/caves")
def test_url_threed(self):
"""Test the {% url "threed" %} reverse resolution
diff --git a/core/TESTS/tests.py b/core/TESTS/tests.py
index d13e1b0..4c4638a 100644
--- a/core/TESTS/tests.py
+++ b/core/TESTS/tests.py
@@ -457,7 +457,7 @@ class PageTests(TestCase):
def test_page_dwgallfiles_empty_slash(self):
# this gets an empty page as the database has not been loaded
- response = self.client.get("/dwgfiles/")
+ response = self.client.get("/dwgfiles")
self.assertEqual(response.status_code, HTTPStatus.OK)
content = response.content.decode()
for ph in [
diff --git a/core/middleware.py b/core/middleware.py
index 2b280a1..fe68fb2 100644
--- a/core/middleware.py
+++ b/core/middleware.py
@@ -36,6 +36,12 @@ class TroggleAppendSlashMiddleware(MiddlewareMixin):
do NOT include
troggle.core.middleware.TroggleAppendSlashMiddleware
in settings.py
+
+ FURTHER WARNING
+ If playing about with this, the 301 redirects that it creates will be cached INDEFINITELY by any browser you
+ used to test it, e.g. /css/main.css with be permanetly redirected to /css/main2.css/ with dreadful
+ consequences, similarly for any images visited. You have to go into your browser settings and delete all cached
+ files to recover from this.
"""
def process_request(self, request):
diff --git a/core/views/signup.py b/core/views/signup.py
index 7536556..bb85e03 100644
--- a/core/views/signup.py
+++ b/core/views/signup.py
@@ -23,7 +23,7 @@ from troggle.core.utils import (
write_and_commit,
)
from troggle.parsers.users import get_encryptor, ENCRYPTED_DIR, how_many_previous_expos
-
+from .auth import login_required_if_public
"""The new user signup form and expo user management system in 2025.
"""
@@ -50,7 +50,7 @@ def signupok(request):
{"year": SIGNUP_YEAR, "dates": SIGNUP_DATES, "signup_user": signup_user, "signedup_people": signedup_people},
)
-
+@login_required_if_public
def signup(request):
"""Displays and processes the applicant signup form for the forthcoming expo
The user must be logged-on as a personal login and that is
@@ -75,11 +75,12 @@ def signup(request):
if len(people) == 1:
signup_person = people[0]
form_read_only = False
- # else:
+ experience = how_many_previous_expos(signup_person)
+ else:
+ experience = 0
# No, just make the form read-only.
# return HttpResponseRedirect("/accounts/login")
- experience = how_many_previous_expos(signup_person)
if request.method == "POST": # If the form has been submitted...
pageform = ExpoSignupForm(request.POST) # A form bound to the POST data
diff --git a/settings.py b/settings.py
index 486d494..e8a0022 100644
--- a/settings.py
+++ b/settings.py
@@ -113,7 +113,7 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting" # Required to customi
# see https://docs.djangoproject.com/en/dev/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware
# Seriously, read this: https://www.webforefront.com/django/middlewaredjango.html which is MUCH BETTER than the docs
-# We are NOT using the home-built SmartAppendSlashMiddleware
+# We are NOT using the home-built TroggleAppendSlashMiddleware, NOR are we using the Django system append_slash
MIDDLEWARE = [
#'django.middleware.security.SecurityMiddleware', # SECURE_SSL_REDIRECT and SECURE_SSL_HOST # we don't use this
"django.middleware.gzip.GZipMiddleware", # not needed when expofiles and photos served by apache
@@ -128,11 +128,12 @@ MIDDLEWARE = [
#"troggle.core.middleware.TroggleAppendSlashMiddleware", # modified Feb.2025
]
-WSGI_APPLICATION = "troggle.wsgi.application" # change to asgi as soon as we upgrade to Django 3.0
+WSGI_APPLICATION = "troggle.wsgi.application" # change to asgi as soon as we upgrade to Django 5 ?
# Append slash can't work if we have a universal catchall URL rule, and we do because all the handbook files
# do not have simple prefix. This is why we used to have an /expoweb/ prefix for everything in the website.
-# APPEND_SLASH = True # using django.middleware.common.CommonMiddleware. Pointless, never happens if there is a catchall.
-# TROGGLE_APPEND_SLASH = True # this is our middleware: see the code in troggle/core/middleware.py for why we do NOT use it.
+# Also site-media etc.
+APPEND_SLASH = False # using django.middleware.common.CommonMiddleware which we need for other things (I think).
+
QM_PATTERN = r"\[\[\s*[Qq][Mm]:([ABC]?)(\d{4})-(\d*)-(\d*)\]\]"
# Re-enable TinyMCE when Dj upgraded to v3. Also templates/editexpopage.html
diff --git a/templates/pagenotfound.html b/templates/pagenotfound.html
index 913c092..891674e 100644
--- a/templates/pagenotfound.html
+++ b/templates/pagenotfound.html
@@ -41,7 +41,8 @@ Did you mistype the URL '<b>{{ path }}</b>' ?
{% endif %}
<p>
<p>
- <p>Did you get lost ?</p>
+ <h3>Did you get lost on the plateau?</h3>
+<p>
<img align=center src="/handbook/i/204-area.png">
{% include "menu.html" %}
diff --git a/templates/svxcaves.html b/templates/svxcaves.html
index 1bea274..7c481b6 100644
--- a/templates/svxcaves.html
+++ b/templates/svxcaves.html
@@ -18,7 +18,7 @@
&nbsp;
{% empty %}
<p>If you were expecting to see a list of survex files here and a summary table of who did what and when, perhaps
- because you followed a link from <a href="/survexfile/caves/">the master caves' survex list</a> page which showed that such survex files clearly existed, and yet there is nothing here but a blank; then this will be because <br>
+ because you followed a link from <a href="/survexfile/caves">the master caves' survex list</a> page which showed that such survex files clearly existed, and yet there is nothing here but a blank; then this will be because <br>
[a] - this cave has no survex survey files at all, or <br>
[b] - you have run a 'caves' parsing import after running a 'survex' import and the survex data has been blanked out, or <br>
[c] - the survex (.svx) files have been stored on the server in the
diff --git a/templates/wallet_new.html b/templates/wallet_new.html
index 619e025..448249f 100644
--- a/templates/wallet_new.html
+++ b/templates/wallet_new.html
@@ -6,7 +6,7 @@ Upload the survexfile using e.g.
(for a survex file for cave 1623-290). You will cut and paste the survex file data into the window on the form.
-<p>While still logged-in, go to this page <a href="/walletedit/">Create Wallet</a> which will take you to the next unused wallet number page, and click the 'Create' button. This will not actually complete
+<p>While still logged-in, go to this page <a href="/walletedit">Create Wallet</a> which will take you to the next unused wallet number page, and click the 'Create' button. This will not actually complete
the creation of the wallet until you have also set the date for the wallet in the wallet edit form (which will appear when you press 'Create').
<p>
While editing the wallet you should enter in the form the url of the survexfile which you have just created at e.g. <a href="/survexfile/caves-1623/290/mynewsurvex.svx">/survexfile/caves-1623/290/mynewsurvex.svx</a> (see above).
diff --git a/templates/wallet_table.html b/templates/wallet_table.html
index 3f90152..ba02e14 100644
--- a/templates/wallet_table.html
+++ b/templates/wallet_table.html
@@ -55,7 +55,7 @@ It lists things like "1984AndysNotebook" instead of a wallet identifier, but if
<a href="/survey_scans/1984AndysNotebook/">"1984AndysNotebook"</a> you
will see some of the the notes and sketches scanned from it.
<p>Or look at all the scanned files,
-their wallet names and the drawings that were created using them at <a href="/dwgfiles/">Drawings</a>
+their wallet names and the drawings that were created using them at <a href="/dwgfiles">Drawings</a>
which is probably more useful.
{% endfor %}
</table>
diff --git a/urls.py b/urls.py
index 695b66c..4803171 100644
--- a/urls.py
+++ b/urls.py
@@ -73,27 +73,44 @@ re_path( <regular expression that matches the thing in the web browser>,
<reference to python function in 'core' folder>, <optional name>)
Django also provides the reverse function: given an an object, provide the URL
-which is vital to writing code for the webapp. So the URL dispatch is declarative.
+which is important to writing code for the webapp. So the URL dispatch is declarative.
But this means that two URLs should NOT go to the same python target function,
(or only if the target name is different)
The API urls return TSV or JSON and are new in July 2020.
-"""
-todo = '''
-- Replace more re_path() with modern and simpler path(). Careful: some have to stay as re_path()
+CONVENTION
+Unlike most DJango projects, we have the convention that we do NOT have a terminal slash
+on the URL of a page which is generated. (Generated pages are neither files not directories.)
+So the url is "/dwgfiles" not "/dwgfiles/" everywhere, and similarly for "/walletedit" etc.
+This is important in troggle because we do NOT universally use the reverse() function
+or the url() function to get the URL from the declarations in this url.py file.
+The reason is that url() requires quite a wide knowledge of troggle, whereas explicit
+urls can be done by beginner maintainers and work (but do add to future maintenance).
+NOTE
- The admin and logout paths need to stay using re_path() as they
have to be locked to the start.
+
+- admin and login forms are provided by Django so we have to fit in to their conventions.
- The final _edit and CATCHALL also have to use re_path().
+
+Many of these patterns do not work because troggle spent many years broken and we have
+not yet restored all the functions. Some may have never been fully implemented in
+the first place and what they were intended to provide is obscure.
+
+Some short names such as indxal.htm date from the Archimedes-era when filenames had to be less
+than 10-chars long.
+"""
+
+todo = '''
+- Replace more re_path() with modern and simpler path(). Careful: some have to stay as re_path()
+
- Test VERY CAREFULLY for each change. It is fragile.
'''
-# Many of these patterns do not work because troggle spent many years broken and we have
-# not yet restored all the functions. Some may have never been fully implemented in
-# the first place and what they were intended to provide is obscure.
# WHen running on the server, apache intercepts all the /expofiles/ files so troggle never sees them,
@@ -134,18 +151,18 @@ trogglepatterns = [
path('entrances', entranceindex, name="entranceindex"),
re_path(r'^admin/doc/', include('django.contrib.admindocs.urls')), # needs docutils Python module (http://docutils.sf.net/).
- path('admin/', admin.site.urls), # includes admin login & logout urls & /admin/jsi18n/
+ path('admin/', admin.site.urls), # includes admin login & logout urls & /admin/jsi18n/ NOTE TERMINAL SLASH
# Uploads - uploading a file
- path('walletedit/', walletedit, name='walletedit'), # not just an upload, also edit metadata
+ path('walletedit', walletedit, name='walletedit'), # not just an upload, also edit metadata
path('walletedit/<path:path>', walletedit, name='walletedit'), # path=2020#01
- path('photoupload/', photoupload, name='photoupload'), # restricted to current year
+ path('photoupload', photoupload, name='photoupload'), # restricted to current year
path('photoupload/<path:folder>', photoupload, name='photoupload'), # restricted to current year
- path('gpxupload/', gpxupload, name='gpxupload'), # restricted to current year
+ path('gpxupload', gpxupload, name='gpxupload'), # restricted to current year
path('gpxupload/<path:folder>', gpxupload, name='gpxupload'), # restricted to current year
path('dwgupload/<path:folder>', dwgupload, name='dwgupload'),
- path('dwgupload/', dwgupload, name='dwgupload'),
- path('dwguploadnogit/', dwgupload, {'gitdisable': 'yes'}, name='dwguploadnogit'), # used in testing
+ path('dwgupload', dwgupload, name='dwgupload'),
+ path('dwguploadnogit', dwgupload, {'gitdisable': 'yes'}, name='dwguploadnogit'), # used in testing
path('dwguploadnogit/<path:folder>', dwgupload, {'gitdisable': 'yes'}, name='dwguploadnogit'), # used in testing
path('logbookedit/', logbookedit, name='logbookedit'),
path('logbookedit/<slug:slug>', logbookedit, name='logbookedit'),
@@ -175,7 +192,7 @@ trogglepatterns = [
# /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
+# NB setting url pattern name to 'login' instead of '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:url_username>", register, name="re_register"), # overriding django.contrib.auth.urls
@@ -204,14 +221,14 @@ trogglepatterns = [
# Internal. editfile.html template uses these internally
re_path(r'^getPeople/(?P<expeditionslug>.*)', get_people, name = "get_people"),
re_path(r'^getLogBookEntries/(?P<expeditionslug>.*)', get_logbook_entries, name = "get_logbook_entries"),
- re_path(r'^getEntrances/(?P<caveslug>.*)', get_entrances, name = "get_entrances"), # used internally ?
+ re_path(r'^getEntrances/(?P<caveslug>.*)', get_entrances, name = "get_entrances"),
# Cave description pages
path('cave/<slug:slug>', caveslugfwd, name="caveslugfwd"),
path('cave_debug', cave_debug, name="cave_debug"),
- path('kataster/<slug:slug>', kataster, name="kataster"),
- path('kataster', kataster, name="kataster"),
- path('fix/<slug:areacode>', fix, name="fix"),
+ path('kataster/<slug:slug>', kataster, name="kataster"), # for renaming a cave from e.g. 1623-2005-05 to 1623-264
+ path('kataster', kataster, name="kataster"), # illustrative placeholder for kataster renaming
+ path('fix/<slug:areacode>', fix, name="fix"), # one-off fix misplaced images and descriptive files
re_path(r'^newcave/$', edit_cave, name="newcave"),
re_path(r'^cave/3d/(?P<cave_id>[^/]+).3d$', cave3d, name="cave3d"),
@@ -251,9 +268,7 @@ trogglepatterns = [
path('survexdir', survex.survexdir, name="survexdir"),
path('survexfile', survex.survexcavesingle, {'cave_shortname': ''}, name="survexcavessingle"),
- path('survexfile/', survex.survexcavesingle, {'cave_shortname': ''}, name="survexcavessingle"),
path('survexfile/caves', survex.survexcaveslist, name="survexcaveslist"),
- path('survexfile/caves/', survex.survexcaveslist, name="survexcaveslist"), # auto slash not working
path('survexfile/<path:survex_file>.svx', survex.svx, name="svx"),
path('survexfile/<path:survex_file>.3d', survex.threed, name="threed"),
@@ -262,11 +277,10 @@ trogglepatterns = [
path('survexfile/<path:cave_shortname>', survex.survexcavesingle, name="survexcavessingle"),
path('survexfilewild', statistics.svxfilewild, name="svxfilewild"),
- path('survexfilewild/', statistics.svxfilewild, name="svxfilewild"),
path('survexfilewild/<int:year>', statistics.svxfilewild, name="svxfilewild"),
# The survey scans in the wallets. This short-cuts SCANS_URL which is not used anymore and is defunct
- path('survey_scans/', allscans, name="allscans"), # all the scans in all wallets
+ path('survey_scans', allscans, name="allscans"), # all the scans in all wallets
path('survey_scans/<path:path>/', walletedit, name="singlewallet"), # replaced singlewallet()
path('survey_scans/<path:path>/<file>', scansingle, name="scansingle"), # works, but html href goes direct to /expofiles/ too
path('cave/scans/<slug:caveid>', cavewallets, name="cavewallets"), # like allscans, but for just one cave
@@ -277,9 +291,7 @@ trogglepatterns = [
# The tunnel and therion drawings files pageswalletslistcave
path('drawings', dwgallfiles, name="dwgallfiles"),
- path('drawings/', dwgallfiles, name="dwgallfiles"),
path('dwgfiles', dwgallfiles, name="dwgallfiles"),
- path('dwgfiles/', dwgallfiles, name="dwgallfiles"),
path('dwgdataraw/<path:path>', dwgfilesingle, name="dwgfilesingle"),
# QMs pages - must precede other /caves pages?
@@ -315,7 +327,7 @@ trogglepatterns = [
# Final catchall which also serves expoweb handbook pages and imagestiny
# but a universal catchall also prevents the djang standard append_slash working, as every string resolves.
# try to fix in troggle/middleware.py
- re_path(r'^(.*)$', expopage, name="expopage"), # CATCHALL assumed relative to EXPOWEB
+ re_path(r'^(.*)$', expopage, name="expopage"), # CATCHALL assumed relative to EXPOWEB. This means APPEND_SLASH never works.
]
# do NOT allow DIR_ROOT prefix to all urls