From c29c12ea765eefcd2ac41a7f475c8d870adcb793 Mon Sep 17 00:00:00 2001 From: Philip Sargent Date: Thu, 31 Aug 2023 18:55:20 +0300 Subject: [PATCH] Edit Logbook Entry mostly working --- core/models/logbooks.py | 45 ++++++- core/views/other.py | 62 ++-------- core/views/uploads.py | 213 ++++++++++++++++++++++++++------ parsers/logbooks.py | 46 +++++-- templates/logbook2005style.html | 3 +- templates/logbookform.html | 4 +- urls.py | 3 +- 7 files changed, 276 insertions(+), 100 deletions(-) diff --git a/core/models/logbooks.py b/core/models/logbooks.py index 3498de1..aef21c1 100644 --- a/core/models/logbooks.py +++ b/core/models/logbooks.py @@ -1,8 +1,11 @@ +import re + from pathlib import Path from urllib.parse import urljoin from django.db import models from django.urls import reverse +from django.template import loader import settings from troggle.core.models.troggle import Expedition, TroggleModel @@ -50,7 +53,7 @@ class LogbookEntry(TroggleModel): max_length=100, blank=True, null=True, help_text="Only use this if you haven't chosen a cave" ) text = models.TextField() - slug = models.SlugField(max_length=50) + slug = models.SlugField(max_length=50) # this is tripid time_underground = models.FloatField(null=True, help_text="In decimal hours") class Meta: @@ -93,7 +96,47 @@ class LogbookEntry(TroggleModel): index = index % mx return index +def writelogbook(year, filename): + current_expedition = Expedition.objects.get(year=year) + logbook_entries = LogbookEntry.objects.filter(expedition=current_expedition).order_by( + "slug" + ) # now that slug, aka tripid, is in our standard date form, this will preserve ordering. + print(f"Logbook exported has {len(logbook_entries)} entries in it.") + + extension = "html" + template = "logbook2005style.html" + + t = loader.get_template(template) + logbookfile = t.render({"logbook_entries": logbook_entries}) + + endpath = Path(settings.EXPOWEB, "years", year, "endmatter.html") + endmatter = "" + if endpath.is_file(): + try: + with open(endpath, "r") as end: + endmatter = end.read() + except: + print(" ! Very Bad Error opening " + endpath) + + frontpath = Path(settings.EXPOWEB, "years", year, "frontmatter.html") + if frontpath.is_file(): + try: + with open(frontpath, "r") as front: + frontmatter = front.read() + except: + print(" ! Very Bad Error opening " + frontpath) + logbookfile = re.sub(r"", "\n" + frontmatter + endmatter, logbookfile) + else: + logbookfile = re.sub(r"", f"\n

Expo {year}

\n" + endmatter, logbookfile) + + dir = Path(settings.EXPOWEB) / "years" / year + filepath = Path(dir, filename) + with (open(filepath, "w")) as lb: + lb.writelines(logbookfile) + + # print(f'Logbook exported to {filepath}') + class PersonLogEntry(TroggleModel): """Single Person going on a trip, which may or may not be written up. It could account for different T/U for people in same logbook entry. diff --git a/core/views/other.py b/core/views/other.py index 8d40079..4d7d3f1 100644 --- a/core/views/other.py +++ b/core/views/other.py @@ -7,7 +7,7 @@ from django.shortcuts import render from django.template import loader from troggle.core.models.caves import Cave -from troggle.core.models.logbooks import LogbookEntry # , PersonLogEntry +from troggle.core.models.logbooks import LogbookEntry, writelogbook # , PersonLogEntry # from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time* from troggle.core.models.troggle import Expedition @@ -169,69 +169,31 @@ def controlpanel(request): ) -def exportlogbook(request, year=None, extension=None): +def exportlogbook(request, year=None): """Constructs, from the database, a complete HTML formatted logbook - for the current year. Formats available are HTML2005. Other formats - have been retired. + for the current year. Format available is now just HTML2005. + Other formats have been retired. There are no images stored in the database. However links to images work in the HTML text of a logbook entry. - This function is the recipient of the POST action os the export form in the control panel + This function is the recipient of the POST action as the export form in the control panel """ def lbeKey(lbe): - """This function goes into a lexicographic sort function""" - return str(lbe.date) + """This function goes into a lexicographic sort function - but where?!""" + return str(lbe.slug) # now that slugs are tripid such as 2023-07-30b if not request.method == "POST": return render(request, "controlPanel.html", {"expeditions": Expedition.objects.all(), "jobs_completed": ""}) else: - print(f"Logbook export {request.POST}") + # print(f"Logbook export {request.POST}") year = request.POST["year"] - current_expedition = Expedition.objects.get(year=year) - logbook_entries = LogbookEntry.objects.filter(expedition=current_expedition).order_by( - "date" - ) # need to be sorted by date! + filename = "logbook-new-format.html" - print(f"Logbook has {len(logbook_entries)} entries in it.") - - extension = "html" - response = HttpResponse(content_type="text/html") - style = "2005" - - filename = "logbook-new-format." + extension - template = "logbook" + style + "style." + extension - response["Content-Disposition"] = "attachment; filename=" + filename - t = loader.get_template(template) - logbookfile = t.render({"logbook_entries": logbook_entries}) - - endpath = Path(settings.EXPOWEB, "years", year, "endmatter.html") - endmatter = "" - if endpath.is_file(): - try: - with open(endpath, "r") as end: - endmatter = end.read() - except: - print(" ! Very Bad Error opening " + endpath) - - frontpath = Path(settings.EXPOWEB, "years", year, "frontmatter.html") - if frontpath.is_file(): - try: - with open(frontpath, "r") as front: - frontmatter = front.read() - except: - print(" ! Very Bad Error opening " + frontpath) - logbookfile = re.sub(r"", "\n" + frontmatter + endmatter, logbookfile) - else: - logbookfile = re.sub(r"", f"\n

Expo {year}

\n" + endmatter, logbookfile) - - dir = Path(settings.EXPOWEB) / "years" / year - filepath = Path(dir, filename) - with (open(filepath, "w")) as lb: - lb.writelines(logbookfile) - - # print(f'Logbook exported to {filepath}') + writelogbook(year, filename) + #response = HttpResponse(content_type="text/html") + #response["Content-Disposition"] = "attachment; filename=" + filename completed = f'Logbook exported to {filename}' return render( diff --git a/core/views/uploads.py b/core/views/uploads.py index b92ca4e..58efb37 100644 --- a/core/views/uploads.py +++ b/core/views/uploads.py @@ -1,5 +1,6 @@ import subprocess import hashlib +import string from datetime import datetime from pathlib import Path @@ -8,8 +9,11 @@ from django.core.files.storage import FileSystemStorage from django.shortcuts import render, redirect import settings -from troggle.core.models.logbooks import LogbookEntry, PersonLogEntry +from troggle.core.models.caves import GetCaveLookup +from troggle.core.models.logbooks import LogbookEntry, writelogbook, PersonLogEntry from troggle.core.models.survex import DrawingFile +from troggle.core.models.troggle import DataIssue, Expedition, PersonExpedition +from troggle.parsers.people import GetPersonExpeditionNameLookup, known_foreigner # from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time* @@ -21,11 +25,6 @@ and that core/forms.py contains Django class-based forms for caves and entrances """ todo = """ -- munge the URL of images in the logbook entry so that they work from both the " /logbookedit/" page, - the logbook /years/2023/ page and the logbook fragment page /logbookentry// - Note that this munging has already been done when the entry is imported into the database, so - when doing an online edit it has already been fixed. - - 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 @@ -48,6 +47,89 @@ todo = """ """ sha = hashlib.new('sha256') +def unique_slug(text, n): + """This gives each logbook entry a unique id based on the date+content, so the order of entries on a particular day + does not matter. This is a change (August 2023) from previous process. + + 2 hex digits would seem adequate for each expo day, but we might get a collision. + The hash is based on the content after substitution of

so should be stable. Which means these ids + can be used elsewhere in the troggle system as permanent slugs. + + When SAVING an edited entry (as opposed to a new one) we will have a different hash so we will have to + delete the original database object + """ + sha.update(text.encode('utf-8')) + return sha.hexdigest()[0:n] + +def create_new_lbe_slug(date): + onthisdate = LogbookEntry.objects.filter(date=date) + n = len(onthisdate) + # print(f" Already entries on this date: {n}\n {onthisdate}") + + alphabet = list(string.ascii_lowercase) + tid = f"{date}{alphabet[n]}" + print(tid) + return tid + +def store_edited_entry_into_database(date, place, title, text, others, author, tu, slug): + """saves a single logbook entry and related personlogentry items + + Rather similar to similarly named function in parsers/logbooks but circular reference prevents us using it directly, + and they need refactoring anyway. + """ + + year = slug[0:4] + expedition = Expedition.objects.get(year=year) + cave = GetCaveLookup().get(place.lower()) + # print(f"{place} {cave=}") + + if LogbookEntry.objects.filter(slug=slug).exists(): + # oops. + message = " ! - DUPLICATE SLUG for logbook entry " + tripdate + " - " + slug + DataIssue.objects.create(parser="logbooks", message=message) + slug = slug + "_" + unique_slug(text,2) + + nonLookupAttribs = { + "place": place, + "text": text, + "expedition": expedition, + "time_underground": tu, + "cave_slug": str(cave), + } + lookupAttribs = {"slug": slug, "date": date, "title": title} + + lbo = LogbookEntry.objects.create(**nonLookupAttribs, **lookupAttribs) + + pt_list = [] + # These entities have to be PersonExpedition objects + team = others.split(",") + team.append(author) + for name in team: + name = name.strip() + if name[0] != "*": # a name prefix of "*" is special, just a string. + try: + personyear = GetPersonExpeditionNameLookup(expedition).get(name.lower()) + if not personyear: + if known_foreigner(name): + message = f" ! - Known foreigner: '{name}' in entry {slug=}" + print(message) + else: + message = f" ! - No name match for: '{name}' in entry {slug=}" + print(message) + DataIssue.objects.create(parser="logbooks", message=message) + else: + lookupAttribs = {"personexpedition": personyear, "nickname_used": name, "logbook_entry": lbo} # lbo is primary key + nonLookupAttribs = {"time_underground": tu, "is_logbook_entry_author": (name==author)} + pt_list.append(PersonLogEntry(**nonLookupAttribs, **lookupAttribs)) + + except: + # This should not happen. We do not raise exceptions in that function + message = f" ! - EXCEPTION: '{name}' in entry {slug=}" + print(message) + DataIssue.objects.create(parser="logbooks", message=message) + raise + + PersonLogEntry.objects.bulk_create(pt_list) class FilesForm(forms.Form): # not a model-form, just a form-form uploadfiles = forms.FileField() @@ -70,9 +152,7 @@ def logbookedit(request, year=None, slug=None): """Edit a logbook entry This is daft: we have the parsed identity of the person and we render it to text as 'nickname_used' (or, previously, 'fullname'), to be re-parsed on re-importing. - And there is no guarantee that this will be the same thing. - - Someone can put in a nickname which is invalid (e.g. 2 Sophies on expo). When is this checked? + And there is no guarantee that this will be the same thing. Oh well. """ def clean_tu(tu): if tu =="": @@ -82,28 +162,14 @@ def logbookedit(request, year=None, slug=None): except: return 0 return tu - - def unique_id(text, n): - """This gives each logbook entry a unique id based on the date+content, so the order of entries on a particular day - does not matter. This is a change (August 2023) from previous process. - Otherwise we could get 2023-07-20a and 2023-07-20b swapped on exporting and re-importing logbooks - because the database does not record precedence. - 2 hex digits would seem adequate for each expo day, but we might get a collision. - The hash is based on the content after substitution of

so should be stable. Which means these ids - can be used elsewhere in the troggle system as permanent slugs. - - When SAVING an edited entry (as opposed to a new one) we will have a different hash so we will have to - delete the original database object - """ - sha.update(text.encode('utf-8')) - return sha.hexdigest()[0:n] if not year: if not slug: - year = 2023 + year = 2023 # we need a CURRENT_EXPO() function, we use this in a lot of places.. else: year = slug[0:4] print(year) + author = "" if request.method == "POST": form = LogbookEditForm(request.POST) @@ -112,11 +178,13 @@ def logbookedit(request, year=None, slug=None): print(message) return render(request, "errors/generic.html", {"message": message}) else: + # if there is no slug then this is a completely new lbe and we need to enter it into the db + # otherwise it is an update # validation all to be done yet.. date = request.POST["date"].strip() author = request.POST["author"].strip() # TODO check against personexpedition others = request.POST["others"].strip() # TODO check each against personexpedition - place = request.POST["place"].strip().replace('-','=') # no hyphens ! + place = request.POST["place"].strip().replace(' - ',' = ') # no hyphens ! title = request.POST["title"].strip() entry = request.POST["text"].strip() entry = entry.replace('\r','') # remove HTML-standard CR inserted @@ -135,16 +203,30 @@ def logbookedit(request, year=None, slug=None): dateflag = True date = odate.isoformat() - newslug = f"{date}_{unique_id(entry,2)}" - if slug: - if slug != newslug: - print(f"! Entry id changed! from {slug} to {newslug}") + if not slug: + # Creating a new logbook entry with all the gubbins + slug = create_new_lbe_slug(date) + else: + # OK we could patch the object in place, but if the people on the trip have changed this + # would get very messy. So we delete it and recreate it and all its links + print(f"- Deleting the LogBookEntry {slug}") + LogbookEntry.objects.filter(slug=slug).delete() + + print(f"- Creating the LogBookEntry {slug}") + store_edited_entry_into_database(date, place, title, entry, others, author, tu, slug) + print(f"- Rewriting the entire {year} logbook to disc ") + filename= "logbook.html" + try: + writelogbook(year, filename) # uses a template, not the code fragment below + 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}) - # OK this could be done by rendering a template, but for such a small bit of HTML, it is easier to have - # it all in one place: here + # Code fragment illustration - not actually what gets saved to database output = f''' -

{date}
+
{date}
{author}, {others}
{place} - {title}
@@ -154,6 +236,65 @@ def logbookedit(request, year=None, slug=None):
''' + # Successful POST + # So save to database and then write out whole new logbook.html file + + #TO DO author and team validation, and check that 'place' is not deleted and that *bloke not forgotten + git = settings.GIT + dirpath = Path(settings.EXPOWEB) / "years" / year + lbe_add = subprocess.run( + [git, "add", filename], cwd=dirpath, capture_output=True, text=True + ) + msgdata = ( + lbe_add.stderr + + "\n" + + lbe_add.stdout + + "\nreturn code: " + + str(lbe_add.returncode) + ) + message = f'! - FORM Logbook Edit {slug} - Success: git ADD on server for this file {filename}.' + msgdata + print(message) + if lbe_add.returncode != 0: + msgdata = ( + "Ask a nerd to fix this.\n\n" + + lbe_add.stderr + + "\n\n" + + lbe_add.stdout + + "\n\nreturn code: " + + str(lbe_add.returncode) + ) + message = ( + f"! - FORM Logbook Edit - CANNOT git ADD on server for this file {filename}. {slug} edits saved but not added to git.\n" + + msgdata + ) + print(message) + return render(request, "errors/generic.html", {"message": message}) + + lbe_commit = subprocess.run( + [git, "commit", "-m", f"Logbook edited {slug}"], + cwd=dirpath, + capture_output=True, + text=True, + ) + message = f'! - FORM Logbook Edit - {filename}. {slug} edits saved, added to git, and COMMITTED.\n' + msgdata + print(message) + #This produces return code = 1 if it commits OK + if lbe_commit.returncode != 0: + msgdata = ( + "Ask a nerd to fix this.\n\n" + + lbe_commit.stderr + + "\n" + + lbe_commit.stdout + + "\nreturn code: " + + str(lbe_commit.returncode) + ) + message = ( + f"! - FORM Logbook Edit -Error code with git on server for {filename}. {slug} edits saved, added to git, but NOT committed.\n" + + msgdata + ) + print(message) + return render(request, "errors/generic.html", {"message": message}) + return render( request, "logbookform.html", @@ -212,10 +353,10 @@ def logbookedit(request, year=None, slug=None): "tu": tu, "entry": text, "textrows": rows, - #"output": output, - }, + }, ) - else: + else: # no slug + # NEW logbook entry return render( request, "logbookform.html", diff --git a/parsers/logbooks.py b/parsers/logbooks.py index 466414c..a5f6631 100644 --- a/parsers/logbooks.py +++ b/parsers/logbooks.py @@ -1,6 +1,7 @@ import os import re import sys +import string import time from datetime import date, datetime @@ -15,6 +16,7 @@ from troggle.core.models.caves import GetCaveLookup from troggle.core.models.logbooks import LogbookEntry, PersonLogEntry from troggle.core.models.troggle import DataIssue, Expedition from troggle.core.utils import get_process_memory +from troggle.core.views.uploads import unique_slug """ Parses and imports logbooks in all their wonderful confusion @@ -106,11 +108,31 @@ ENTRIES = { logentries = [] # the entire logbook for one year is a single object: a list of entries noncaveplaces = ["travel", "Journey", "Loser Plateau", "UNKNOWN", "plateau", "base camp", "basecamp", "top camp", "topcamp"] +tripsdate = {} +alphabet = [] -def set_trip_id(year, seq): +def set_trip_seq_id(year, seq): + '''We have not parsed the trip date yet, so this is a sequence numer + ''' tid = f"{year}_s{seq:02d}" return tid +def reset_trip_id(date): + '''Now we have the date, we can set the tripid (the lbe slug) to be in our standard form + of , i.e. '2003-07-30b' + BUT this gets re-set every time the logbook is imported, + so they are not persistent as we would much prefer. + ''' + global alphabet + already =tripsdate.get(date, 0) # returns zero if none found + tripsdate[date] = already +1 + if not alphabet: + alphabet = list(string.ascii_lowercase) + + tid = f"{date}{alphabet[already]}" + # print(tid) + return tid + rx_tripperson = re.compile(r"(?i)(.*?)$") rx_round_bracket = re.compile(r"[\(\[].*?[\)\]]") @@ -246,9 +268,14 @@ def store_entry_into_database(date, place, tripcave, title, text, trippersons, a "expedition": expedition, "time_underground": logtime_underground, "cave_slug": str(tripcave), - "slug": tid, } - lookupAttribs = {"date": date, "title": title} + lookupAttribs = {"slug": tid, "date": date, "title": title} + if LogbookEntry.objects.filter(slug=tid).exists(): + # oops. + message = " ! - DUPLICATE SLUG for logbook entry " + tripdate + " - " + slug + DataIssue.objects.create(parser="logbooks", message=message) + slug = slug + "_" + unique_slug(text,2) + lbo = LogbookEntry.objects.create(**nonLookupAttribs, **lookupAttribs) pt_list = [] @@ -332,7 +359,7 @@ def parser_html(year, expedition, txt, seq=""): logbook_entry_count = 0 for trippara in tripparas: logbook_entry_count += 1 - tid = set_trip_id(year, logbook_entry_count) + tid = set_trip_seq_id(year, logbook_entry_count) # print(f' - new tid:{tid} lbe count: {logbook_entry_count}') s = re.match( @@ -376,6 +403,9 @@ def parser_html(year, expedition, txt, seq=""): continue ldate = parser_date(tripdate.strip(), year) + + # Now we have a date, we can reset tripid + tid = reset_trip_id(ldate) triptitles = triptitle.split(" - ") if len(triptitles) >= 2: place = triptitles[0] @@ -385,7 +415,7 @@ def parser_html(year, expedition, txt, seq=""): tripcontent = re.sub(r"

", "

", tripcontent).strip() triptitle = triptitle.strip() - # triptitle must be unique for a given date. We fix this here. + # triptitle must be unique for a given date. We fix this here. [Why?!] check = (ldate, triptitle) if check in dupl: dupl[check] += 1 @@ -458,7 +488,7 @@ def parser_blog(year, expedition, txt, sq=""): # print(f"{i} - {len(tripstuff)} - {tripstuff[1]}") triphead = tripheads[i] logbook_entry_count += 1 - tid = set_trip_id(year, logbook_entry_count) + "_blog" + sq + tid = set_trip_seq_id(year, logbook_entry_count) + "_blog" + sq # print(f" - tid: {tid}") # data-author="tcacrossley" @@ -580,7 +610,7 @@ def parse_logbook_for_expedition(expedition, blog=False): if logbook_parseable: # -------------------- parser = globals()[parsefunc] - print(f" - {year} parsing with {parsefunc} - {lb}") + # print(f" - {year} parsing with {parsefunc} - {lb}") print(" .", end="") logentries = parser(year, expedition, txt, sq) # this launches the right parser # -------------------- @@ -595,7 +625,7 @@ def parse_logbook_for_expedition(expedition, blog=False): def LoadLogbook(year): - """One off logbook for testing purposes, and also reloadable on '/expedition/2022?reload' + """One off logbook for testing purposes, and also reloadable on '/expedition/2023?reload' This is inside an atomic transaction""" expo = Expedition.objects.get(year=year) diff --git a/templates/logbook2005style.html b/templates/logbook2005style.html index b0491b7..67fd930 100644 --- a/templates/logbook2005style.html +++ b/templates/logbook2005style.html @@ -14,7 +14,8 @@ maintain half a dozen parser functions. Sorry about all the crap that surrounds the image tags which has been imported along with the content when UK Caving blogs have been parsed. -Exported on {% now 'Y-m-d H:m' %} using control panel webpage and exportlogbook() in troggle/code/views/other.py +Exported on {% now 'Y-m-d H:m' %} using either the control panel webpage or when editing a logbook entry online +See troggle/code/views/other.py and core.models/logbooks.py writelogbook(year, filename) --> {%for logbook_entry in logbook_entries%} diff --git a/templates/logbookform.html b/templates/logbookform.html index 9c33a50..b8a94f0 100644 --- a/templates/logbookform.html +++ b/templates/logbookform.html @@ -79,9 +79,9 @@ title="Time underground (hours)" {% if tu %}value="{{tu}}"{% else %}placeholder="0.1" {% endif %} /> -

This DOES NOT SAVE ANYTHING yet +

diff --git a/urls.py b/urls.py index 7edc26d..e88df3b 100644 --- a/urls.py +++ b/urls.py @@ -132,8 +132,7 @@ trogglepatterns = [ re_path(r'^api/QMs_json$', QMs_jsonListView.as_view()), # Logbook entries - re_path(r'^logbookentry/(?P.*)/(?P.*)/?$', logbookentry,name="logbookentry"), - re_path(r'^logbook(?P\d\d\d\d)\.(?P.*)/?$', exportlogbook, name='exportlogbook'), # e.g. /logbook2019.html # working but old CSS in + re_path(r'^logbookentry/(?P.*)/(?P.*)/?$', logbookentry,name="logbookentry"), re_path(r'^logbook$', exportlogbook, name='exportlogbook'), # Internal. editfile.html template uses these internally