mirror of
https://expo.survex.com/repositories/troggle/.git
synced 2025-12-16 22:27:14 +00:00
486 lines
19 KiB
Python
486 lines
19 KiB
Python
import json
|
|
import re
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.serializers import serialize
|
|
from django.db.models import Q
|
|
from django.shortcuts import redirect, render
|
|
from django.views.generic.list import ListView
|
|
from django.contrib.auth.models import User
|
|
|
|
|
|
import troggle.settings as settings
|
|
from troggle.core.models.logbooks import QM, LogbookEntry, PersonLogEntry, writelogbook
|
|
from troggle.core.models.survex import SurvexBlock, SurvexFile
|
|
from troggle.core.models.troggle import Expedition, Person
|
|
from troggle.core.models.wallets import Wallet
|
|
from troggle.core.utils import TROG, current_expo, add_commit, git_commit, git_add, get_editor
|
|
from troggle.parsers.imports import import_logbook
|
|
|
|
"""These views are for logbook items when they appear in an 'expedition' page
|
|
and for persons: their individual pages and their perseonexpedition pages.
|
|
|
|
It uses the global object TROG to hold some cached pages. USELESS as cache only works single-threaded, single-user.
|
|
"""
|
|
|
|
todo = """- Fix the get_person_chronology() display bug.
|
|
|
|
- Fix id= value preservation on editing
|
|
"""
|
|
|
|
|
|
def notablepersons(request):
|
|
def notabilitykey(person):
|
|
return person.notability()
|
|
|
|
print(request)
|
|
persons = Person.objects.order_by('fullname')
|
|
# From what I can tell, "persons" seems to be the table rows, while "pcols" is the table columns. - AC 16 Feb 09
|
|
pcols = []
|
|
ncols = 4
|
|
nc = int((len(persons) + ncols - 1) / ncols)
|
|
for i in range(ncols):
|
|
pcols.append(persons[i * nc : (i + 1) * nc])
|
|
|
|
notablepersons = []
|
|
# Needed recoding because of Django CVE-2021-45116
|
|
for person in persons:
|
|
if person.bisnotable():
|
|
notablepersons.append(person)
|
|
notablepersons.sort(key=notabilitykey, reverse=True)
|
|
|
|
return render(
|
|
request, "notablepersons.html", {"persons": persons, "pcols": pcols, "notablepersons": notablepersons}
|
|
)
|
|
|
|
def people_ids(request):
|
|
|
|
ensure_users_are_persons()
|
|
persons = Person.objects.order_by('fullname')
|
|
# From what I can tell, "persons" seems to be the table rows, while "pcols" is the table columns. - AC 16 Feb 09
|
|
pcols = []
|
|
ncols = 4
|
|
nc = int((len(persons) + ncols - 1) / ncols)
|
|
for i in range(ncols):
|
|
pcols.append(persons[i * nc : (i + 1) * nc])
|
|
|
|
|
|
return render(
|
|
request, "people_ids.html", {"persons": persons, "pcols": pcols}
|
|
)
|
|
|
|
|
|
def expedition(request, expeditionname):
|
|
"""Returns a rendered page for one expedition, specified by the year e.g. '2019'.
|
|
If page caching is enabled, it caches the dictionaries used to render the template page.
|
|
|
|
This is not as difficult to understand as it looks.
|
|
Yes there are many levels of indirection, with multiple trees being traversed at the same time.
|
|
And the Django special syntax makes this hard for normal Python programmers.
|
|
|
|
Remember that 'personexpedition__expedition' is interpreted by Django to mean the
|
|
'expedition' object which is connected by a foreign key to the 'personexpedition'
|
|
object, which is a field of the PersonLogEntry object:
|
|
PersonLogEntry.objects.filter(personexpedition__expedition=expo)
|
|
|
|
Queries are not evaluated to hit the database until a result is actually used. Django
|
|
does lazy evaluation.
|
|
|
|
"""
|
|
current = current_expo() # creates new expo after 31st Dec.
|
|
try:
|
|
expo = Expedition.objects.get(year=int(expeditionname))
|
|
except:
|
|
message = (
|
|
"Expedition not found - database apparently empty, you probably need to do a full re-import of all data."
|
|
)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
ts = TROG["pagecache"]["expedition"] # not much use unless single user!
|
|
if request.user.is_authenticated:
|
|
logged_in = True
|
|
if "reload" in request.GET:
|
|
if expeditionname in ts:
|
|
del ts[expeditionname] # clean out cache for page
|
|
expo.logbookentry_set.all().delete()
|
|
import_logbook(year=expo.year)
|
|
else:
|
|
logged_in = False
|
|
|
|
if settings.CACHEDPAGES:
|
|
if expeditionname in ts:
|
|
# print('! - expo {expeditionanme} using cached page')
|
|
return render(request, "expedition.html", {**ts[expeditionname], "logged_in": logged_in})
|
|
|
|
|
|
entries = expo.logbookentry_set.only('date','title').filter(expedition=expo)
|
|
blocks = expo.survexblock_set.only('date','name').filter(expedition=expo).prefetch_related('scanswallet', 'survexfile')
|
|
dateditems = list(entries) + list(blocks) # evaluates the Django query and hits db
|
|
dates = sorted(set([item.date for item in dateditems]))
|
|
|
|
allpersonlogentries = PersonLogEntry.objects.prefetch_related('logbook_entry').select_related('personexpedition__expedition').filter(personexpedition__expedition=expo)
|
|
|
|
personexpodays = []
|
|
for personexpedition in expo.personexpedition_set.all().prefetch_related('person'):
|
|
expotrips = allpersonlogentries.filter(personexpedition=personexpedition) # lazy
|
|
expoblocks = blocks.filter(survexpersonrole__personexpedition=personexpedition)
|
|
|
|
prow = []
|
|
|
|
for date in dates:
|
|
personentries = expotrips.filter(logbook_entry__date=date) # lazy
|
|
personblocks = set(expoblocks.filter(date=date)) # not lazy
|
|
pcell = {}
|
|
pcell["personentries"] = personentries
|
|
pcell["survexblocks"] = personblocks
|
|
if issunday := (date.weekday() == 6): # WALRUS
|
|
pcell["sunday"] = issunday
|
|
prow.append(pcell)
|
|
personexpodays.append({"personexpedition": personexpedition, "personrow": prow, "sortname": personexpedition.person.last_name})
|
|
|
|
expeditions = Expedition.objects.only('year') # top menu only, evaluated only when template renders, only need "year"
|
|
|
|
ts[expeditionname] = {
|
|
"year": int(expeditionname),
|
|
"expedition": expo,
|
|
"expeditions": expeditions,
|
|
"personexpodays": personexpodays,
|
|
"settings": settings,
|
|
"dateditems": dateditems,
|
|
"dates": dates,
|
|
}
|
|
TROG["pagecache"]["expedition"][expeditionname] = ts[expeditionname]
|
|
|
|
return render(request, "expedition.html", {**ts[expeditionname], "logged_in": logged_in})
|
|
|
|
|
|
class Expeditions_tsvListView(ListView):
|
|
"""This uses the Django built-in shortcut mechanism
|
|
It defaults to use a template with name <app-label>/<model-name>_list.html.
|
|
https://www.agiliq.com/blog/2017/12/when-and-how-use-django-listview/
|
|
https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Generic_views
|
|
Either a queryset variable or set_queryset() function is used, but not needed
|
|
if you want all the obejcts of a particaulr type in which case just set model = <object>
|
|
"""
|
|
|
|
template_name = "core/expeditions_tsv_list.html" # if not present then uses core/expedition_list.html
|
|
# queryset = Expedition.objects.all()
|
|
# context_object_name = 'expedition'
|
|
model = Expedition # equivalent to .objects.all() for a queryset
|
|
|
|
|
|
class Expeditions_jsonListView(ListView):
|
|
template_name = "core/expeditions_json_list.html"
|
|
model = Expedition
|
|
|
|
class QMs_jsonListView(ListView):
|
|
template_name = "core/QMs_json_list.html"
|
|
model = QM
|
|
|
|
def person(request, slug=""):
|
|
"""Now very much simpler with an unambiguous slug
|
|
"""
|
|
try:
|
|
this_person = Person.objects.get(slug=slug)
|
|
except:
|
|
msg = f" Person '{slug=}' not found in database. DATABASE RESET required - ask a nerd."
|
|
print(msg)
|
|
return render(request, "errors/generic.html", {"message": msg})
|
|
|
|
current_year = current_expo()
|
|
return render(request, "person.html", {"person": this_person, "year": current_year})
|
|
|
|
def get_person_chronology(personexpedition):
|
|
"""
|
|
This is just a nasty convoluted way of trying the make the template do more work than it is sensible to ask it to do.
|
|
Rewrite more simply with the login in the python, not in Django template language (you bastard Curtis).
|
|
"""
|
|
res = {}
|
|
for personlogentry in personexpedition.personlogentry_set.all():
|
|
a = res.setdefault(personlogentry.logbook_entry.date, {})
|
|
a.setdefault("personlogentries", []).append(personlogentry)
|
|
|
|
for personrole in personexpedition.survexpersonrole_set.all():
|
|
if personrole.survexblock.date: # avoid bad data from another bug
|
|
a = res.setdefault(personrole.survexblock.date, {})
|
|
a.setdefault("personroles", []).append(personrole.survexblock)
|
|
|
|
# build up the tables
|
|
rdates = sorted(list(res.keys()))
|
|
|
|
res2 = []
|
|
for rdate in rdates:
|
|
personlogentries = res[rdate].get("personlogentries", [])
|
|
personroles = res[rdate].get("personroles", [])
|
|
for n in range(max(len(personlogentries), len(personroles))):
|
|
res2.append(
|
|
(
|
|
(n == 0 and rdate or "--"),
|
|
(n < len(personlogentries) and personlogentries[n]),
|
|
(n < len(personroles) and personroles[n]),
|
|
)
|
|
)
|
|
|
|
return res2
|
|
|
|
|
|
def personexpedition(request, slug="", year=""):
|
|
try:
|
|
person = Person.objects.get(slug=slug)
|
|
this_expedition = Expedition.objects.get(year=year)
|
|
personexpedition = person.personexpedition_set.get(expedition=this_expedition)
|
|
personchronology = get_person_chronology(personexpedition)
|
|
current_year = current_expo()
|
|
|
|
return render(
|
|
request, "personexpedition.html", {"personexpedition": personexpedition, "personchronology": personchronology, "year": current_year}
|
|
)
|
|
except:
|
|
msg = f" Person '{slug=}' or year '{year=}' not found in database. Please report this to a nerd."
|
|
print(msg)
|
|
return render(request, "errors/generic.html", {"message": msg})
|
|
|
|
def logentrydelete(request, year):
|
|
"""This only gets called by a POST from the logreport page
|
|
|
|
This function is dedicated to James Waite who managed to make so many duplicate logbook entries
|
|
that we needed a sopecial mechanism to delete them.
|
|
"""
|
|
for i in request.POST:
|
|
print(f" - '{i}' {request.POST[i]}")
|
|
eslug = request.POST["entry_slug"]
|
|
entry = LogbookEntry.objects.get(slug=eslug)
|
|
# OK we delete it from the db and then re-save logbook.html file
|
|
# to ensure that the permanent record also has the entry deleted.
|
|
entry.delete()
|
|
|
|
print(f"- Rewriting the entire {year} logbook to disc ")
|
|
filename= "logbook.html"
|
|
try:
|
|
writelogbook(year, filename) # uses a template
|
|
except:
|
|
message = f'! - Logbook saving failed - \n!! Permissions failure ?! on attempting to save file "logbook.html"'
|
|
print(message)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
|
|
return redirect(f"/logreport/{year}")
|
|
|
|
def get_entries(year):
|
|
expo = Expedition.objects.get(year=int(year))
|
|
entries = expo.logbookentry_set.all() # lazy list
|
|
dateditems = list(entries) # evaluates the Django query and hits db
|
|
try:
|
|
for entry in dateditems:
|
|
people = PersonLogEntry.objects.filter(logbook_entry=entry)
|
|
entry.who = []
|
|
for p in people:
|
|
if p.is_logbook_entry_author:
|
|
entry.author = p
|
|
else:
|
|
entry.who.append(p)
|
|
except Exception as e:
|
|
msg = f' Logbook report for year:"{year}" not implemented yet\n{e}\n {context}'
|
|
print(msg)
|
|
return render(request, "errors/generic.html", {"message": msg})
|
|
return dateditems
|
|
|
|
|
|
def logreport(request, year=1999):
|
|
"""
|
|
Remember that 'personexpedition__expedition' is interpreted by Django to mean the
|
|
'expedition' object which is connected by a foreign key to the 'personexpedition'
|
|
object, which is a field of the PersonLogEntry object:
|
|
PersonLogEntry.objects.filter(personexpedition__expedition=expo)
|
|
|
|
"""
|
|
# print(f"logreport(): begun")
|
|
|
|
expeditions = Expedition.objects.all() # top menu only, evaluated only when template renders
|
|
logged_in = False
|
|
if request.user.is_superuser: # expoadmin is both .is_staff and ._is_superuser
|
|
logged_in = True
|
|
|
|
try:
|
|
expo = Expedition.objects.get(year=int(year))
|
|
except:
|
|
message = (
|
|
"Expedition not found - database apparently empty, you probably need to do a full re-import of all data."
|
|
)
|
|
return render(request, "errors/generic.html", {"message": message})
|
|
|
|
dateditems = get_entries(year)
|
|
dates = sorted(set([item.date for item in dateditems]))
|
|
|
|
# print(f"logreport(): trying..")
|
|
context = {
|
|
"year": year,
|
|
"expedition": expo,
|
|
"expeditions": expeditions,
|
|
"settings": settings,
|
|
"dateditems": dateditems,
|
|
"dates": dates,
|
|
"logged_in": logged_in,
|
|
}
|
|
# print(f"logreport(): rendering..")
|
|
return render(request, "logreport.html", context)
|
|
|
|
|
|
|
|
def logbookentry(request, date, slug):
|
|
"""Displays a single logbook entry
|
|
however, if an author has not used the correct URL in an image or a reference, then a link from
|
|
inside a logbook entry can arrive with this default address prefix. So we
|
|
have to handle that error without crashing.
|
|
"""
|
|
try:
|
|
trips = LogbookEntry.objects.filter(date=date) # all the trips not just this one
|
|
except ValidationError:
|
|
msg = f' Logbook entry invalid date:"{date}" probably because of relative (not absolute) addressing of "src=" or "haref=" in the text'
|
|
print(msg)
|
|
return render(request, "errors/generic.html", {"message": msg})
|
|
this_logbookentry = trips.filter(date=date, slug=slug)
|
|
year = slug[:4]
|
|
|
|
if this_logbookentry:
|
|
if len(this_logbookentry) > 1:
|
|
# BUG
|
|
return render(request, "object_list.html", {"object_list": this_logbookentry})
|
|
else:
|
|
# https://stackoverflow.com/questions/739776/how-do-i-do-an-or-filter-in-a-django-query
|
|
wallets = Wallet.objects.filter(Q(survexblock__date=date) | Q(walletdate=date)).distinct()
|
|
svxothers = SurvexFile.objects.filter(survexblock__date=date).distinct()
|
|
|
|
this_logbookentry = this_logbookentry[0]
|
|
# This is the only page that uses next_.. and prev_..
|
|
# and it is calculated on the fly in the model
|
|
return render(
|
|
request,
|
|
"logbookentry.html",
|
|
{"logbookentry": this_logbookentry,"trips": trips,
|
|
"svxothers": svxothers, "wallets": wallets, "year": year},
|
|
)
|
|
else:
|
|
msg = f' Logbook entry slug:"{slug}" not found in database on date:"{date}" '
|
|
print(msg)
|
|
return render(request, "errors/generic.html", {"message": msg})
|
|
|
|
|
|
def get_people(request, expeditionslug):
|
|
exp = Expedition.objects.get(year=expeditionslug)
|
|
return render(request, "options.html", {"items": [(pe.slug, pe.name) for pe in exp.personexpedition_set.all()]})
|
|
|
|
|
|
def get_logbook_entries(request, expeditionslug):
|
|
exp = Expedition.objects.get(year=expeditionslug)
|
|
return render(
|
|
request, "options.html", {"items": [(le.slug, f"{le.date} - {le.title}") for le in exp.logbookentry_set.all()]}
|
|
)
|
|
|
|
def logbook_entries_export(request, year):
|
|
exp = Expedition.objects.get(year=year)
|
|
|
|
entries = get_entries(year)
|
|
# for e in entries:
|
|
# print(f"{e.pk:03} {e}")
|
|
|
|
editor = get_editor(request)
|
|
write_entries(entries, year, editor)
|
|
return redirect(f"/logreport/{year}")
|
|
|
|
|
|
def write_entries(entries, year, editor):
|
|
|
|
dirpath = settings.EXPOWEB / "years" / year / "log_entries"
|
|
try:
|
|
dirpath.mkdir(parents=True, exist_ok=True)
|
|
except PermissionError as e:
|
|
raise PermissionError(
|
|
f"CANNOT make the directory.\nPERMISSIONS incorrectly set on server for this file {filepath}. Ask a nerd to fix this: {e}"
|
|
)
|
|
except Exception as e:
|
|
raise OSError(
|
|
f"CANNOT make the directory for {filepath}. Ask a nerd to fix this: {e}"
|
|
)
|
|
|
|
for le in entries:
|
|
# REPLACE this with hand-built serializer which includes .author, .who which were added to the entries but re not in the model Class directly
|
|
# see below for Gemini code to do that. Going to bed now.
|
|
jsondict = serialize("json", [le], fields=('slug', 'date', 'expedition', 'title', 'cave', 'place', 'other_people', 'time_underground', 'text'))
|
|
|
|
filename = f"{le.slug}-{le.pk:03}.json"
|
|
filepath = dirpath / filename
|
|
description = f" {le.slug} :: {le.date} - {le.title}"
|
|
try:
|
|
with open(filepath, 'w', encoding='utf-8') as json_f:
|
|
json.dump(jsondict, json_f, indent=1)
|
|
except PermissionError as e:
|
|
raise PermissionError(
|
|
f"CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filepath}. Ask a nerd to fix this: {e}"
|
|
)
|
|
except Exception as e:
|
|
print(f"CANNOT write this file {filepath}. Exception dumping json. Ask a nerd to fix this: {e}")
|
|
raise e
|
|
|
|
git_add(filename, dirpath)
|
|
|
|
commit_msg = f"Exporting logbook entries as individual files"
|
|
git_commit(dirpath, commit_msg, editor)
|
|
return True
|
|
|
|
# Gemini has the answer, get what I need from this:
|
|
|
|
# from django.http import JsonResponse, HttpResponse
|
|
# # from .models import LogbookEntry, PersonLogEntry, Person # Import your models
|
|
# from django.forms.models import model_to_dict
|
|
# from datetime import datetime
|
|
# import json
|
|
# from decimal import Decimal
|
|
|
|
# # Re-using the custom encoder from the previous suggestion for robust date/decimal handling
|
|
# class CustomJSONEncoder(json.JSONEncoder):
|
|
# def default(self, obj):
|
|
# if isinstance(obj, datetime):
|
|
# return obj.isoformat()
|
|
# if isinstance(obj, Decimal):
|
|
# return str(obj)
|
|
# return json.JSONEncoder.default(self, obj)
|
|
|
|
# def export_entry_with_author_details(request, entry_id):
|
|
# try:
|
|
# # 1. Get the LogbookEntry instance
|
|
# entry = LogbookEntry.objects.get(pk=entry_id)
|
|
|
|
# # 2. Get the related PersonLogEntry and Person (Author)
|
|
# # Use .select_related() for efficiency
|
|
# author_link = PersonLogEntry.objects.select_related('person').get(
|
|
# entry=entry,
|
|
# is_author=True # Adjust filter based on your logic
|
|
# )
|
|
# author = author_link.person
|
|
|
|
# except (LogbookEntry.DoesNotExist, PersonLogEntry.DoesNotExist):
|
|
# return HttpResponse(f'Entry or Author not found for ID {entry_id}', status=404)
|
|
|
|
# # 3. Manually create the nested dictionary structure
|
|
# # Use model_to_dict for easy extraction of the simple fields
|
|
|
|
# # Author data (specify fields you want to expose)
|
|
# author_data = model_to_dict(author, fields=['id', 'first_name', 'last_name', 'email'])
|
|
|
|
# # Entry data (specify fields you want to expose)
|
|
# entry_data = model_to_dict(entry, fields=['id', 'title', 'content', 'date_created'])
|
|
|
|
# # Nest the author data inside the entry data
|
|
# entry_data['author'] = author_data
|
|
|
|
# # Add data from the intermediate model if needed (e.g., the date the person was added)
|
|
# entry_data['author_assignment_date'] = author_link.date_assigned.isoformat()
|
|
|
|
|
|
# # 4. Return the custom dictionary using JsonResponse
|
|
# return JsonResponse(
|
|
# entry_data,
|
|
# encoder=CustomJSONEncoder,
|
|
# safe=False # Set to True if entry_data was a list/QuerySet
|
|
# ) |