2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2025-12-16 07:07:13 +00:00
Files
troggle/core/views/logbooks.py
2025-11-22 11:19:25 +02:00

487 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
"""
LOGBOOK_ENTRIES = "log_entries" # directory name
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):
"""Exports logentries from the live database to JSON files.
entries - a list, use a list of one member if writing a single entry
year - the year of the expo.
"""
def write_json_file():
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
dirpath = settings.EXPOWEB / "years" / year / LOGBOOK_ENTRIES
dirpath.mkdir(parents=True, exist_ok=True)
for le in entries:
filename = f"{le.slug}-{le.pk:03}.json"
filepath = dirpath / filename
# description = f" {le.slug} :: {le.date} - {le.title}"
# 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'))
write_json_file()
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
# )