mirror of
https://expo.survex.com/repositories/troggle/.git
synced 2025-12-17 17:37:08 +00:00
970 lines
38 KiB
Python
970 lines
38 KiB
Python
import subprocess
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
from django import forms
|
|
from django.core.files.storage import FileSystemStorage
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import redirect, render
|
|
|
|
import settings
|
|
from troggle.core.models.caves import GetCaveLookup
|
|
from troggle.core.models.survex import DrawingFile
|
|
from troggle.core.models.troggle import DataIssue, Expedition, PersonExpedition
|
|
from troggle.core.utils import (
|
|
get_cookie_max_age,
|
|
alphabet_suffix,
|
|
current_expo,
|
|
get_editor,
|
|
is_identified_user,
|
|
git_add,
|
|
git_commit,
|
|
git_string,
|
|
sanitize_name,
|
|
unique_slug,
|
|
write_and_commit,
|
|
)
|
|
from troggle.parsers.people import GetPersonExpeditionNameLookup, known_foreigner
|
|
|
|
# from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time*
|
|
from .auth import login_required_if_public
|
|
|
|
"""File upload 'views'
|
|
Note that there are other file upload forms in views/wallet_edit.py
|
|
and views/logbook_edit.py and that core/forms.py contains Django class-based forms for caves and entrances.
|
|
"""
|
|
|
|
todo = """
|
|
- Register the uploaded drawing file (views/uploads.py) using the functions in parsers/drawings.py so that queries of the database
|
|
can find newly uploaded files without having to do a database reset.
|
|
|
|
- parse the uploaded drawing file for links to wallets and scan files as done
|
|
in parsers/drawings.py
|
|
|
|
- move much of the _get() functions in the the _setup() function, esp. the population of the
|
|
files and dirs lists for display after a POST. Or remove them where they are not used.
|
|
use photoupload() as a the exemplar
|
|
|
|
- after refactoring, the commonality between all the file upload forms is now clear.
|
|
So we have a job to do in removing the redundancy if we can maintain clarity.
|
|
|
|
- Ideally we should validate uploaded file as being a valid file type, not a dubious script or hack
|
|
Validate image files using a magic recogniser in walletedit()
|
|
https://pypi.org/project/reportlab/ or
|
|
https://stackoverflow.com/questions/889333/how-to-check-if-a-file-is-a-valid-image-file
|
|
|
|
- Validate Tunnel & Therion files using an XML parser in dwgupload(). Though Julian says
|
|
tunnel is only mostly correct XML, and it does fail at least one XML parser.
|
|
Many tunnel files have non-ascii bytes in them, but they should all be utf-8.
|
|
|
|
|
|
- Enable folder creation in dwguploads or as a separate form
|
|
|
|
- Enable file rename on expofiles, not just for /surveyscans/ (aka wallets)
|
|
|
|
- Make file rename utility less ugly.
|
|
|
|
- refactor the WalletFilesForm and PhotoUpload form to use the _setup, _get, _post idiom.
|
|
"""
|
|
|
|
|
|
|
|
class WalletFilesForm(forms.Form): # not a model-form, just a form-form
|
|
"""Used only for uploading to expofiles/surveyscans/<year>/<wallet>
|
|
which is not a git repo so we do not need an "editor" to assign blame to
|
|
"""
|
|
uploadfiles = forms.FileField()
|
|
|
|
|
|
class PhotographerForm(forms.Form): # not a model-form, just a form-form
|
|
"""not a git repo so we do not need an "editor" to assign blame to"""
|
|
photographer = forms.CharField(strip=True)
|
|
|
|
class GPXuploadForm(forms.Form): # not a model-form, just a form-form
|
|
"""not a git repo so we do not need an "editor" to assign blame to"""
|
|
uploadfiles = forms.FileField()
|
|
prospector = forms.CharField(strip=True)
|
|
|
|
class GPXfixForm(forms.Form): # not a model-form, just a form-form
|
|
"""not a git repo so we do not need an "editor" to assign blame to"""
|
|
prospector = forms.CharField(strip=True)
|
|
areacode = forms.CharField(strip=True)
|
|
station = forms.CharField(strip=True)
|
|
uploadfiles = forms.FileField()
|
|
|
|
class DrawingsFilesForm(forms.Form): # not a model-form, just a form-form
|
|
uploadfiles = forms.FileField()
|
|
identified_login = forms.BooleanField(required=False,widget=forms.CheckboxInput(attrs={"onclick":"return false"})) # makes it readonly
|
|
who_are_you = forms.CharField(
|
|
widget=forms.TextInput(
|
|
attrs={"size": 100, "placeholder": "You are editing this page, who are you ? e.g. 'Becka' or 'Animal <mta@gasthof.expo>'",
|
|
"style": "vertical-align: text-top;"}
|
|
)
|
|
)
|
|
|
|
class FilesRenameForm(forms.Form): # not a model-form, just a form-form
|
|
"""Used only for renaming photos in /expofiles/photos/
|
|
not a git repo so we do not need an "editor" to assign blame to
|
|
"""
|
|
uploadfiles = forms.FileField()
|
|
renameto = forms.CharField(strip=True, required=False)
|
|
|
|
class ExpofileRenameForm(forms.Form): # not a model-form, just a form-form
|
|
"""not a git repo so we do not need an "editor" to assign blame to"""
|
|
renameto = forms.CharField(strip=True, required=False)
|
|
|
|
|
|
|
|
|
|
@login_required_if_public
|
|
def expofilerename(request, filepath):
|
|
"""Rename any single file in /expofiles/ - eventually.
|
|
Currently this just does files within wallets i.e. in /surveyscans/
|
|
and it returns control to the original wallet edit page
|
|
"""
|
|
|
|
def is_rotatable(path):
|
|
"""If file is a JPG but has no filename extension, then it must be renamed
|
|
before it can be rotated.
|
|
"""
|
|
print(f"{path}: '{Path(path).suffix.lower()}'")
|
|
if Path(path).suffix.lower() in [".png", ".jpg", ".jpeg"]:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def rotate_image(ctx):
|
|
wallet = str(Path(ctx["filepath"]).parent).lstrip("surveyscans/")
|
|
cwd = settings.SCANS_ROOT / wallet
|
|
print(f"ROTATE \n{cwd=} \n{ctx['filename']=}")
|
|
mogrify = settings.MOGRIFY
|
|
rot = subprocess.run(
|
|
[mogrify, "-rotate", "90", ctx["filename"]], cwd=cwd, capture_output=True, text=True
|
|
)
|
|
msgdata = rot.stderr + "\n" + rot.stdout + "\nreturn code: " + str(rot.returncode)
|
|
message = f'! - ROTATE - Success: rotated this file {ctx["filename"]}.' + msgdata
|
|
print(message)
|
|
# DataIssue.objects.create(parser="mogrify", message=message)
|
|
|
|
if rot.returncode != 0:
|
|
msgdata = (
|
|
"Ask a nerd to fix this.\n\n"
|
|
+ rot.stderr
|
|
+ "\n\n"
|
|
+ rot.stdout
|
|
+ "\n\nreturn code: "
|
|
+ str(rot.returncode)
|
|
)
|
|
message = f"! - ROTATE - CANNOT blurk for this file {ctx['filename']}. \n" + msgdata
|
|
print(message)
|
|
DataIssue.objects.create(parser="mogrify", message=message)
|
|
|
|
# return to the GET view
|
|
return simple_get(ctx)
|
|
|
|
def simple_get(ctx):
|
|
form = ExpofileRenameForm()
|
|
return render(
|
|
request,
|
|
"renameform.html",
|
|
{
|
|
"form": form,
|
|
"filepath": ctx["filepath"],
|
|
"filename": ctx["filename"],
|
|
"filesize": ctx["filesize"],
|
|
"files": ctx.get("files", []),
|
|
"walletpath": ctx["walletpath"],
|
|
"wallet": ctx["wallet"],
|
|
"notpics": ctx.get("notpics", []),
|
|
"rotatable": ctx["rotatable"],
|
|
},
|
|
)
|
|
|
|
def _setup(filepath_arg):
|
|
if not filepath_arg:
|
|
message = f'\n File to rename not specified "{filepath_arg}"'
|
|
print(message)
|
|
return {"response": render(request, "errors/generic.html", {"message": message})}
|
|
|
|
actualpath = Path(settings.EXPOFILES) / Path(filepath_arg)
|
|
if not actualpath.is_file():
|
|
message = f'\n File not found when attempting rename "{filepath_arg}"'
|
|
print(message)
|
|
return {"response": render(request, "errors/generic.html", {"message": message})}
|
|
|
|
filename = Path(filepath_arg).name
|
|
walletpath = Path(filepath_arg).parent
|
|
wallet = Path(walletpath).name
|
|
folder = actualpath.parent
|
|
filesize = f"{actualpath.stat().st_size:,}"
|
|
rotatable = is_rotatable(filename)
|
|
|
|
# ensure we are renaming only within wallets (surveyscans)
|
|
if not actualpath.is_relative_to(Path(settings.SCANS_ROOT)):
|
|
message = f'\n Can only do rename within wallets (expofiles/surveyscans/) currently, sorry. "{actualpath}" '
|
|
print(message)
|
|
return {"response": render(request, "errors/generic.html", {"message": message})}
|
|
|
|
files = []
|
|
dirs = []
|
|
notpics = []
|
|
dirpath = actualpath.parent
|
|
print(f'! - FORM rename expofile - start \n{filepath_arg=} \n{dirpath=} \n{walletpath=}')
|
|
|
|
if dirpath.is_dir():
|
|
try:
|
|
for f in dirpath.iterdir():
|
|
if f.is_dir():
|
|
for d in f.iterdir():
|
|
dirs.append(f"{f.name}/{d.name}")
|
|
if f.is_file():
|
|
if is_rotatable(f.name):
|
|
files.append(f.name)
|
|
else:
|
|
notpics.append(f.name)
|
|
except FileNotFoundError:
|
|
files.append("(Error. There should be at least one filename visible here. Try refresh.)")
|
|
|
|
ctx = {
|
|
"filepath": filepath_arg,
|
|
"actualpath": actualpath,
|
|
"filename": filename,
|
|
"walletpath": walletpath,
|
|
"wallet": wallet,
|
|
"folder": folder,
|
|
"filesize": filesize,
|
|
"rotatable": rotatable,
|
|
"files": sorted(files) if files else [],
|
|
"dirs": sorted(dirs) if dirs else [],
|
|
"notpics": notpics,
|
|
"dirpath": dirpath,
|
|
}
|
|
return ctx
|
|
|
|
def _post(ctx):
|
|
# handle POST actions (rotate / rename)
|
|
form = ExpofileRenameForm(request.POST)
|
|
if not form.is_valid():
|
|
message = f'Invalid form response for file renaming "{request.POST}"'
|
|
print(message)
|
|
return {"response": render(request, "errors/generic.html", {"message": message})}
|
|
|
|
if "rotate" in request.POST:
|
|
# perform rotation and return its response
|
|
return rotate_image(ctx)
|
|
|
|
if "rename" in request.POST:
|
|
if "renametoname" not in request.POST:
|
|
print("renametoname not in request.POST")
|
|
# blank filename passed it, so just treat as another GET
|
|
return simple_get(ctx)
|
|
|
|
renameto = sanitize_name(request.POST["renametoname"])
|
|
if (ctx["folder"] / renameto).is_file() or (ctx["folder"] / renameto).is_dir():
|
|
rename_bad = renameto
|
|
message = f'\n Cannot rename to an existing file or folder. "{ctx["filename"]}" -> "{(ctx["folder"] / renameto)}"'
|
|
print(message)
|
|
return render(
|
|
request,
|
|
"renameform.html",
|
|
{
|
|
"form": form,
|
|
"filepath": ctx["filepath"],
|
|
"filename": ctx["filename"],
|
|
"filesize": ctx["filesize"],
|
|
"files": ctx.get("files", []),
|
|
"walletpath": ctx["walletpath"],
|
|
"wallet": ctx["wallet"],
|
|
"notpics": ctx.get("notpics", []),
|
|
"rename_bad": rename_bad,
|
|
},
|
|
)
|
|
|
|
# perform rename
|
|
ctx["actualpath"].rename((ctx["folder"] / renameto))
|
|
message = f'\n RENAMED "{ctx["filename"]}" -> "{(ctx["folder"] / renameto)}"'
|
|
print(message)
|
|
walletid = ctx["actualpath"].relative_to(Path(settings.SCANS_ROOT)).parent.stem.replace("#", ":")
|
|
print(walletid)
|
|
return redirect(f'/survey_scans/{walletid}/')
|
|
|
|
# fallback: show GET view
|
|
return simple_get(ctx)
|
|
|
|
# main flow
|
|
# needs tidying, fold in simple_get into get()
|
|
# and change this silly return ctx["response"] thinggy to something more obvious
|
|
ctx = _setup(filepath)
|
|
if "response" in ctx:
|
|
return ctx["response"]
|
|
|
|
if request.method == "GET":
|
|
return simple_get(ctx)
|
|
|
|
if request.method == "POST":
|
|
# The refactoring done by VS coe seems to be a bit baroque here, needs further work..
|
|
result = _post(ctx)
|
|
# if _post returned a response, return it
|
|
if hasattr(result, "status_code") or isinstance(result, dict) and "response" in result:
|
|
return result if not isinstance(result, dict) else result["response"]
|
|
# else result may be a ctx dict to render
|
|
if isinstance(result, dict) and "form" in result and not result["form"].is_valid():
|
|
return simple_get(result)
|
|
# if _post returned ctx (or simple_get response), ensure we render current state
|
|
if isinstance(result, dict):
|
|
return simple_get(result)
|
|
|
|
# unrecognized method: default to GET view
|
|
return simple_get(ctx)
|
|
|
|
|
|
|
|
@login_required_if_public
|
|
def photoupload(request, folder=None):
|
|
"""Upload photo image files into /expofiles/photos/<year>/<photographer>/
|
|
This does NOT use a Django model linked to a Django form. Just a simple Django form.
|
|
You will find the Django documentation on forms very confusing, This is simpler.
|
|
|
|
|
|
When uploading from a phone, it is useful to be able to rename the file to something
|
|
meaningful as this is difficult to do on a phone. Previously we had assumed files would
|
|
be renamed to something useful before starting the upload.
|
|
Unfortunately this only works when uploading one file at at time ,
|
|
inevitable once you think about it.
|
|
|
|
Pending generic file renaming capability more generally.
|
|
"""
|
|
|
|
def _setup(folder_arg):
|
|
year = current_expo()
|
|
yearpath = Path(settings.PHOTOS_ROOT, year)
|
|
# merge previous 'context' fields into ctx
|
|
ctx = {
|
|
"year": year,
|
|
"placeholder": "AnathemaDevice",
|
|
"filesaved": False,
|
|
"actual_saved": [],
|
|
"yearpath": yearpath,
|
|
"form": FilesRenameForm(),
|
|
"formd": PhotographerForm(),
|
|
}
|
|
|
|
# Normalize folder and derive dirpath/urlfile/urldir like original logic
|
|
if folder_arg == str(year) or folder_arg == str(year) + "/":
|
|
folder_arg = None
|
|
|
|
if folder_arg is None:
|
|
folder_arg = "" # improve this later
|
|
dirpath = Path(settings.PHOTOS_ROOT, year)
|
|
urlfile = f"/expofiles/photos/{year}"
|
|
urldir = f"/photoupload/{year}"
|
|
else: # it will contain the year as well as the photographer
|
|
dirpath = Path(settings.PHOTOS_ROOT, folder_arg)
|
|
if dirpath.is_dir():
|
|
urlfile = f"/expofiles/photos/{folder_arg}"
|
|
urldir = Path("/photoupload") / folder_arg
|
|
else:
|
|
folder_arg = "" # improve this later
|
|
dirpath = Path(settings.PHOTOS_ROOT, year)
|
|
urlfile = f"/expofiles/photos/{year}"
|
|
urldir = f"/photoupload/{year}"
|
|
ctx.update({"folder": folder_arg, "dirpath": dirpath, "urlfile": urlfile, "urldir": urldir})
|
|
|
|
files = []
|
|
dirs = []
|
|
try:
|
|
for f in ctx["dirpath"].iterdir():
|
|
if f.is_dir():
|
|
dirs.append(f.name)
|
|
if f.is_file():
|
|
files.append(f.name)
|
|
except FileNotFoundError:
|
|
files.append("(no folder yet - would be created)")
|
|
ctx["files"] = sorted(files) if files else []
|
|
ctx["dirs"] = sorted(dirs) if dirs else []
|
|
|
|
return ctx
|
|
|
|
def _post(ctx):
|
|
if "photographer" in request.POST:
|
|
# then we are creating a new folder, not uploading files. This only happens
|
|
# if we are in the top level, in the /<year>/ folder.
|
|
formd = PhotographerForm(request.POST)
|
|
ctx["formd"] = formd
|
|
if formd.is_valid():
|
|
newphotographer = sanitize_name(request.POST["photographer"])
|
|
try:
|
|
(ctx["yearpath"] / newphotographer).mkdir(parents=True, exist_ok=True)
|
|
except Exception:
|
|
message = f'\n !! Permissions failure ?! 0 attempting to mkdir "{(ctx["yearpath"] / newphotographer)}"'
|
|
print(message)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
return ctx
|
|
|
|
# else form for uploads / renames
|
|
form = FilesRenameForm(request.POST, request.FILES)
|
|
ctx["form"] = form
|
|
|
|
multiple = request.FILES.getlist("uploadfiles")
|
|
if not multiple:
|
|
# user supplied no files — attach a form error and return to show it
|
|
print(f"photoupload(): no files to upload {multiple}")
|
|
form.add_error("uploadfiles", "No files uploaded.") # does not seem to be visible on form?
|
|
ctx["form"] = form
|
|
return ctx
|
|
|
|
fs = FileSystemStorage(ctx["dirpath"])
|
|
|
|
renameto = sanitize_name(request.POST.get("renameto", ""))
|
|
ctx["actual_saved"] = []
|
|
ctx["filesaved"] = False
|
|
# multiple files, ignore renameto and save each
|
|
for f in multiple:
|
|
try:
|
|
if len(multiple) == 1:
|
|
filename_to_save = renameto if renameto != "" else f.name
|
|
saved_filename = fs.save(filename_to_save, content=f)
|
|
else:
|
|
saved_filename = fs.save(f.name, content=f)
|
|
except Exception:
|
|
print(f'\n !! Permissions failure ?! attempting to save "{f.name}" in "{ctx["dirpath"]}" {renameto=}')
|
|
if "saved_filename" in locals():
|
|
if (ctx["dirpath"] / saved_filename).is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
ctx["filesaved"] = True
|
|
continue
|
|
if (ctx["dirpath"] / saved_filename).is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
ctx["filesaved"] = True
|
|
return ctx
|
|
|
|
def _get(ctx):
|
|
return ctx
|
|
|
|
# main flow
|
|
ctx = _setup(folder)
|
|
|
|
if request.method == "POST":
|
|
ctx = _post(ctx)
|
|
# if form invalid, still show GET-like view (ctx includes form with errors)
|
|
if isinstance(ctx, dict) and "form" in ctx and not ctx["form"].is_valid():
|
|
ctx = _get(ctx)
|
|
else:
|
|
ctx = _get(ctx)
|
|
|
|
return render(
|
|
request,
|
|
"photouploadform.html",
|
|
{
|
|
"form": ctx.get("form", FilesRenameForm()),
|
|
"year": ctx["year"],
|
|
"placeholder": ctx["placeholder"],
|
|
"urlfile": ctx["urlfile"],
|
|
"urldir": ctx["urldir"],
|
|
"folder": ctx["folder"],
|
|
"files": ctx.get("files", []),
|
|
"dirs": ctx.get("dirs", []),
|
|
"filesaved": ctx.get("filesaved", False),
|
|
"actual_saved": ctx.get("actual_saved", []),
|
|
},
|
|
)
|
|
|
|
|
|
|
|
@login_required_if_public
|
|
def gpxupload(request, folder=None):
|
|
"""Copy of photo upload folder is the "path"
|
|
"""
|
|
def gpxvalid(name):
|
|
# dangerous, we should check the actual file binary signature
|
|
return Path(name).suffix.lower() in [".xml", ".gpx"]
|
|
|
|
print(f"gpxupload() {folder=}")
|
|
|
|
def _setup(folder_arg):
|
|
year = current_expo()
|
|
# put year/placeholder directly on ctx (no nested 'context' dict)
|
|
ctx = {
|
|
"year": year,
|
|
"placeholder": "AnathemaDevice",
|
|
"filesaved": False,
|
|
"actual_saved": [],
|
|
"yearpath": Path(settings.EXPOFILES) / "gpslogs" / year,
|
|
"formd": GPXuploadForm(),
|
|
}
|
|
|
|
# normalize folder -> dirpath, urlfile, urldir
|
|
if folder_arg == str(year) or folder_arg == str(year) + "/":
|
|
folder_arg = None
|
|
|
|
if folder_arg is None:
|
|
folder_arg = ""
|
|
dirpath = ctx["yearpath"]
|
|
urlfile = f"/expofiles/gpslogs/{year}"
|
|
urldir = f"/gpxupload/{year}"
|
|
else:
|
|
dirpath = Path(settings.EXPOFILES) / "gpslogs" / folder_arg
|
|
if dirpath.is_dir():
|
|
urlfile = f"/expofiles/gpslogs/{folder_arg}"
|
|
urldir = Path("/gpxupload") / folder_arg
|
|
else:
|
|
folder_arg = ""
|
|
dirpath = ctx["yearpath"]
|
|
urlfile = f"/expofiles/gpslogs/{year}"
|
|
urldir = f"/gpxupload/{year}"
|
|
|
|
ctx.update({"folder": folder_arg, "dirpath": dirpath, "urlfile": urlfile, "urldir": urldir})
|
|
print(f"gpxupload() {_setup.__name__} -> {folder_arg=} {dirpath=} {urlfile=} {urldir=}")
|
|
return ctx
|
|
|
|
def _post(ctx):
|
|
print(f"gpxupload() method=POST")
|
|
for i in request.POST:
|
|
print(" ", i)
|
|
|
|
formd = GPXuploadForm(request.POST)
|
|
ctx["formd"] = formd
|
|
|
|
# prospector creation branch (preserve original behavior and prints)
|
|
if "prospector" in request.POST:
|
|
print(f"gpxupload() {request.POST=}\n {request.POST.get('prospector')=}")
|
|
if formd.is_valid():
|
|
newprospector = sanitize_name(request.POST["prospector"])
|
|
print(f"gpxupload() {newprospector=}")
|
|
try:
|
|
(ctx["yearpath"] / newprospector).mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
message = f'\n !! Permissions failure ?! 0 attempting to mkdir "{(ctx["yearpath"] / newprospector)}": {e}'
|
|
print(message)
|
|
raise
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
return ctx
|
|
|
|
# file upload branch
|
|
print(f"gpxupload() no prospector field")
|
|
print(f"gpxupload() {request.FILES=}")
|
|
for i in request.FILES:
|
|
print(" ", i)
|
|
|
|
print(f"gpxupload() about to look at request.FILES")
|
|
multiple = request.FILES.getlist("uploadfiles")
|
|
if not multiple:
|
|
# user supplied no files — attach a form error and return to show it
|
|
print(f"gpxupload(): no files to upload {multiple}")
|
|
formd.add_error("uploadfiles", "No files uploaded.") # does not seem to be visible on form?
|
|
ctx["formd"] = formd
|
|
return ctx
|
|
|
|
# NO CHECK that the files being uploaded are image files
|
|
fs = FileSystemStorage(ctx["dirpath"])
|
|
|
|
ctx["actual_saved"] = []
|
|
ctx["filesaved"] = False
|
|
|
|
for f in multiple:
|
|
if gpxvalid(f.name):
|
|
try: # crashes in Django os.chmod call if on WSL, but does save file!
|
|
saved_filename = fs.save(f.name, content=f)
|
|
except Exception:
|
|
print(
|
|
f'\n !! Permissions failure ?! 3 attempting to save "{f.name}" in "{ctx["dirpath"]}"'
|
|
)
|
|
if "saved_filename" in locals():
|
|
if (ctx["dirpath"] / saved_filename).is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
ctx["filesaved"] = True
|
|
continue
|
|
if (ctx["dirpath"] / saved_filename).is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
ctx["filesaved"] = True
|
|
else:
|
|
print(f"gpxupload(): not a GPX file {f.name=}")
|
|
|
|
return ctx
|
|
|
|
def _get(ctx):
|
|
files = []
|
|
dirs = []
|
|
try:
|
|
for f in ctx["dirpath"].iterdir():
|
|
if f.is_dir():
|
|
dirs.append(f.name)
|
|
if f.is_file():
|
|
files.append(f.name)
|
|
except FileNotFoundError:
|
|
files.append("(no folder yet - would be created)")
|
|
except Exception as e:
|
|
print(f"gpxupload() EXCEPTION\n {e}")
|
|
ctx["files"] = sorted(files) if files else []
|
|
ctx["dirs"] = sorted(dirs) if dirs else []
|
|
return ctx
|
|
|
|
# main flow
|
|
ctx = _setup(folder)
|
|
|
|
if request.method == "POST":
|
|
ctx = _post(ctx)
|
|
# if form invalid, still show GET-like view (ctx includes form with errors)
|
|
if isinstance(ctx, dict) and "formd" in ctx and not ctx["formd"].is_valid():
|
|
ctx = _get(ctx)
|
|
else:
|
|
ctx = _get(ctx)
|
|
|
|
print(f"gpxupload() drop through")
|
|
files = ctx.get("files", [])
|
|
dirs = ctx.get("dirs", [])
|
|
|
|
print(f"gpxupload() about to render..")
|
|
return render(
|
|
request,
|
|
"gpxuploadform.html",
|
|
{
|
|
"form": ctx.get("formd", GPXuploadForm()),
|
|
"year": ctx["year"],
|
|
"placeholder": ctx["placeholder"],
|
|
"urlfile": ctx["urlfile"],
|
|
"urldir": ctx["urldir"],
|
|
"folder": ctx["folder"],
|
|
"files": files,
|
|
"dirs": dirs,
|
|
"filesaved": ctx.get("filesaved", False),
|
|
"actual_saved": ctx.get("actual_saved", []),
|
|
},
|
|
)
|
|
|
|
def analyse_gpx(saved_filename, content):
|
|
"""For an uploaded GPX file, analyse it to get a *fix number
|
|
"""
|
|
print(f"analyse_gpx(): {saved_filename} -- {content.name} length: {len(content)} bytes")
|
|
|
|
@login_required_if_public
|
|
def gpxfix(request):
|
|
"""Upload one or more GPX files containing a single track which is actually a single static point: for averaging
|
|
"""
|
|
def gpxvalid(name):
|
|
if Path(name).suffix.lower() in [".gpx"]:
|
|
return True # dangerous, we should check the actual file binary signature
|
|
return False
|
|
|
|
def _setup():
|
|
year = current_expo() # read this from the GPX file in future!
|
|
ctx = {
|
|
"year": year,
|
|
"yearpath": Path("gpsfix") / year,
|
|
"folder": "",
|
|
"dirpath": Path(settings.EXPOFILES) / "gpsfix" / year,
|
|
"urlfile": None,
|
|
"urldir": None,
|
|
"formd": GPXfixForm(),
|
|
"filesaved": False,
|
|
"actual_saved": [],
|
|
"location_data": [],
|
|
"areacode": "",
|
|
"fixstring": "*fix p2025-WW-01 13.8229370 47.6874630 1871",
|
|
"entrancestring": "*entrance p2025-WW-01"
|
|
}
|
|
ctx["urlfile"] = f"/expofiles/gpsfix/{ctx['year']}"
|
|
ctx["urldir"] = f"/gpxfix/{ctx['year']}"
|
|
return ctx
|
|
|
|
def _post(ctx):
|
|
formd = GPXfixForm(request.POST)
|
|
ctx["formd"] = formd
|
|
|
|
# Check there are files to upload
|
|
multiple = request.FILES.getlist("uploadfiles")
|
|
if not multiple:
|
|
# user supplied no files — attach a form error and return to show it
|
|
print(f"gpxfix(): no files to upload {multiple}")
|
|
formd.add_error("uploadfiles", "No files uploaded.") # does not seem to be visible on form?
|
|
ctx["formd"] = formd
|
|
return ctx
|
|
|
|
# ensure base year folder exists
|
|
try:
|
|
ctx["yearpath"].mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
message = f'\n !! Permissions failure ?! 0 attempting to mkdir "{ctx["yearpath"]}": {e}'
|
|
print(message)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
# if a prospector field was submitted, validate it
|
|
if "prospector" in request.POST:
|
|
if formd.is_valid():
|
|
newprospector = sanitize_name(request.POST["prospector"])
|
|
print(f"gpxfix() prospector named: {newprospector}")
|
|
|
|
ctx["areacode"] = request.POST["areacode"].strip()
|
|
|
|
fs = FileSystemStorage(ctx["dirpath"])
|
|
ctx["actual_saved"] = []
|
|
ctx["location_data"] = []
|
|
|
|
for f in multiple:
|
|
if gpxvalid(f.name):
|
|
try:
|
|
saved_filename = fs.save(f.name, content=f)
|
|
except Exception as e:
|
|
print(f'\n !! Permissions failure ?! on attempting to save "{f.name}" in "{ctx["dirpath"]}": {e}')
|
|
# if save partially succeeded, guard against referencing undefined vars
|
|
continue
|
|
filepath = ctx["dirpath"] / saved_filename
|
|
if filepath.is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
ctx["filesaved"] = True
|
|
# analyse_gpx may use the uploaded content object
|
|
try:
|
|
ctx["location_data"].append(analyse_gpx(saved_filename, f))
|
|
except Exception as e:
|
|
print(f"gpxfix(): analyse_gpx failed for {saved_filename}: {e}")
|
|
else:
|
|
print(f"gpxfix(): not a GPX file {f.name=}")
|
|
|
|
ctx["fixstring"] = f"*fix {request.POST["station"]} 13.8229370 47.6874630 1871 # {request.POST["prospector"]}: {multiple[0]}"
|
|
ctx["entrancestring"] = f"*entrance {request.POST["station"]}"
|
|
return ctx
|
|
|
|
def _get(ctx):
|
|
files = []
|
|
dirs = []
|
|
try:
|
|
for f in ctx["dirpath"].iterdir():
|
|
if f.is_dir():
|
|
dirs.append(f.name)
|
|
if f.is_file():
|
|
files.append(f.name)
|
|
except FileNotFoundError:
|
|
files.append("(no folder yet - would be created)")
|
|
except Exception as e:
|
|
print(f"gpxfix() EXCEPTION\n {e}")
|
|
ctx["files"] = sorted(files) if files else []
|
|
ctx["dirs"] = sorted(dirs) if dirs else []
|
|
return ctx
|
|
|
|
# main flow
|
|
ctx = _setup()
|
|
|
|
if request.method == "POST":
|
|
ctx = _post(ctx)
|
|
# if form invalid, still show GET-like view (ctx includes form with errors)
|
|
if isinstance(ctx, dict) and "formd" in ctx and not ctx["formd"].is_valid():
|
|
ctx = _get(ctx)
|
|
else:
|
|
ctx = _get(ctx)
|
|
|
|
return render(
|
|
request,
|
|
"gpxfixform.html",
|
|
{
|
|
"form": ctx.get("formd", GPXfixForm()),
|
|
"year": ctx["year"],
|
|
"urlfile": ctx["urlfile"],
|
|
"urldir": ctx["urldir"],
|
|
"yearpath": ctx["yearpath"],
|
|
"folder": ctx["folder"],
|
|
"files": ctx.get("files", []),
|
|
"dirs": ctx.get("dirs", []),
|
|
"filesaved": ctx.get("filesaved", False),
|
|
"actual_saved": ctx.get("actual_saved", []),
|
|
"areacode": ctx["areacode"],
|
|
"fixstring": ctx["fixstring"],
|
|
"entrancestring": ctx["entrancestring"],
|
|
},
|
|
)
|
|
|
|
|
|
@login_required_if_public
|
|
def dwgupload(request, folder=None, gitdisable="no"):
|
|
"""Upload DRAWING files (tunnel or therion) into the upload folder in :drawings
|
|
AND registers it into the :drawings: git repo.
|
|
|
|
This does NOT use a Django model linked to a Django form. Just a simple Django form.
|
|
You will find the Django documentation on class-based forms very confusing, This is simpler.
|
|
|
|
This does not even use a Django View, it is entirely built on functions not classes.
|
|
See https://spookylukey.github.io/django-views-the-right-way/index.html
|
|
but with an even greater emphasis on simple visibility for nn-Django programmers.
|
|
|
|
We could validate the uploaded files as being a valid files using an XML parser, not a dubious script or hack,
|
|
but this won't work on Tunnel files as Tunnel does not produce exactly valid xml (!)
|
|
|
|
We use get_or_create instead of simply creating a new object in case someone uploads the same file
|
|
several times in one session, and expects them to be overwritten in the database. (Although
|
|
the actual file will be duplicated in the filesystem with different random name ending,
|
|
and this will need to be cleaned-up manually by a nerd later.)
|
|
|
|
instructions to copilot:
|
|
'refactor dwgupload() to reduce the size of the large if statement using new
|
|
functions _setup(), _get() and post()'
|
|
Stunningly, this was enough to do the whole job. It did remove some comments though,
|
|
which have been manually reinstated.
|
|
Refactored to use _setup, _post and _get helpers to reduce a large if/else block.
|
|
"""
|
|
|
|
def dwgvalid(name):
|
|
if name in [".gitignore"]:
|
|
return False
|
|
if Path(name).suffix.lower() in [".xml", ".th", ".th2", "", ".svg", ".txt"]:
|
|
return True # dangerous, we should check the actual file binary signature
|
|
return False
|
|
|
|
def dwgvaliddisp(name):
|
|
"""OK to display, even if we are not going to allow a new one to be uploaded"""
|
|
if name in [
|
|
".gitignore",
|
|
]:
|
|
return False
|
|
if Path(name).suffix.lower() in [
|
|
".xml", ".th", ".th2", "", ".svg", ".txt", ".jpg", ".jpeg", ".png", ".pdf", ".top", ".topo",
|
|
]:
|
|
return True # dangerous, we should check the actual file binary signature
|
|
return False
|
|
|
|
def _setup(folder_arg):
|
|
# initialize common state and return context dict
|
|
ctx = {}
|
|
if folder_arg is None:
|
|
folder_arg = ""
|
|
dirpath = Path(settings.DRAWINGS_DATA)
|
|
urlfile = "/dwgdataraw"
|
|
urldir = "/dwgupload"
|
|
else:
|
|
dirpath = Path(settings.DRAWINGS_DATA, folder_arg)
|
|
urlfile = Path("/dwgdataraw/") / folder_arg
|
|
urldir = Path("/dwgupload/") / folder_arg
|
|
|
|
ctx.update(
|
|
{
|
|
"folder": folder_arg,
|
|
"dirpath": dirpath,
|
|
"urlfile": urlfile,
|
|
"urldir": urldir,
|
|
"identified_login": is_identified_user(request.user),
|
|
"editor": get_editor(request),
|
|
"form": DrawingsFilesForm(),
|
|
"filesaved": False,
|
|
"actual_saved": [],
|
|
"refused": [],
|
|
"doesnotexist": "",
|
|
}
|
|
)
|
|
return ctx
|
|
|
|
def _post(ctx):
|
|
# handle POST -- validate form and save files, update ctx in-place
|
|
form = DrawingsFilesForm(request.POST, request.FILES)
|
|
ctx["form"] = form
|
|
if not form.is_valid():
|
|
return ctx # will be rendered like GET
|
|
|
|
editor = form.cleaned_data["who_are_you"]
|
|
editor = git_string(editor)
|
|
ctx["editor"] = editor
|
|
|
|
multiple = request.FILES.getlist("uploadfiles")
|
|
savepath = Path(settings.DRAWINGS_DATA, ctx["folder"])
|
|
fs = FileSystemStorage(savepath)
|
|
ctx["actual_saved"] = []
|
|
ctx["refused"] = []
|
|
|
|
git = settings.GIT if gitdisable != "yes" else "echo"
|
|
commands = []
|
|
|
|
if multiple:
|
|
for f in multiple:
|
|
if not dwgvalid(f.name):
|
|
ctx["refused"].append(f.name)
|
|
continue
|
|
try:
|
|
saved_filename = fs.save(f.name, content=f)
|
|
except Exception as e:
|
|
# save failed: abort with error page
|
|
message = f'! - FORM dwgupload - Permissions failure saving "{f.name}" in "{savepath}": {e}'
|
|
print(message)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
filepath = ctx["dirpath"] / saved_filename
|
|
if filepath.is_file():
|
|
ctx["actual_saved"].append(saved_filename)
|
|
if gitdisable != "yes":
|
|
commands = git_add(filepath, ctx["dirpath"])
|
|
dwgfile, created = DrawingFile.objects.get_or_create(
|
|
dwgpath=saved_filename, dwgname=Path(f.name).stem, filesize=f.size
|
|
)
|
|
dwgfile.save()
|
|
else:
|
|
message = f"! - FORM dwgupload - NOT A FILE {Path(ctx['dirpath'], saved_filename)=}."
|
|
print(message)
|
|
|
|
if ctx["actual_saved"]:
|
|
ctx["filesaved"] = True
|
|
if len(ctx["actual_saved"]) > 1:
|
|
dots = f"{len(ctx['actual_saved'])} files"
|
|
else:
|
|
dots = f"{ctx['actual_saved'][0]}"
|
|
commit_msg = f"Drawings upload - {dots}"
|
|
if gitdisable != "yes":
|
|
git_commit(ctx["dirpath"], commit_msg, editor, commands)
|
|
else:
|
|
if ctx["refused"]:
|
|
message = f"! - FORM dwgupload - Nothing actually saved. All were refused. {ctx['actual_saved']=}"
|
|
print(message)
|
|
|
|
return ctx
|
|
|
|
def _get(ctx):
|
|
# build file/directory listings and prepare form readonly state if needed
|
|
files = []
|
|
dirs = []
|
|
try:
|
|
for f in ctx["dirpath"].iterdir():
|
|
if f.is_dir():
|
|
if f.name not in [".git"]:
|
|
dirs.append(f.name)
|
|
continue
|
|
if f.is_file():
|
|
if dwgvaliddisp(f.name):
|
|
files.append(f.name)
|
|
continue
|
|
except FileNotFoundError:
|
|
ctx["doesnotexist"] = True
|
|
|
|
ctx["files"] = sorted(files) if files else []
|
|
ctx["dirs"] = sorted(dirs) if dirs else []
|
|
|
|
if ctx["identified_login"]:
|
|
ctx["form"].fields["who_are_you"].widget.attrs["readonly"] = "readonly"
|
|
|
|
return ctx
|
|
|
|
# main flow: setup, then POST or GET handlers, then render
|
|
ctx = _setup(folder)
|
|
|
|
if request.method == "POST":
|
|
ctx = _post(ctx)
|
|
# if form invalid, still show GET view (ctx includes form with errors)
|
|
if isinstance(ctx, dict) and "form" in ctx and not ctx["form"].is_valid():
|
|
ctx = _get(ctx)
|
|
else:
|
|
ctx = _get(ctx)
|
|
|
|
response = render(
|
|
request,
|
|
"dwguploadform.html",
|
|
{
|
|
"form": ctx["form"],
|
|
"identified_login": ctx["identified_login"],
|
|
"doesnotexist": ctx.get("doesnotexist", ""),
|
|
"urlfile": ctx["urlfile"],
|
|
"urldir": ctx["urldir"],
|
|
"folder": ctx["folder"],
|
|
"files": ctx.get("files", []),
|
|
"dirs": ctx.get("dirs", []),
|
|
"filesaved": ctx.get("filesaved", False),
|
|
"actual_saved": ctx.get("actual_saved", []),
|
|
"refused": ctx.get("refused", []),
|
|
"who_are_you": ctx["editor"],
|
|
},
|
|
)
|
|
response.set_cookie("editor_id", ctx["editor"], max_age=get_cookie_max_age(request))
|
|
return response |