From 20583b04c0c1a826aaddbf0342f4369a206dae2d Mon Sep 17 00:00:00 2001 From: Martin Green Date: Sat, 25 Jun 2022 23:17:19 +0100 Subject: [PATCH] Allowed user to select/upload images when editing. When uploaded thumbnails and description pages are automatically created. Git commiting can now handle multiple files at once. --- core/views/editor_helpers.py | 111 ++++++++++++++++++++++ core/views/expo.py | 2 +- lib/version_control.py | 43 +++++---- templates/editexpopage.html | 137 +++++++++++++++++++++++++++ templates/image_page_template.html | 24 +++++ templates/image_selector.html | 3 + templates/linked_image_template.html | 1 + templates/new_image_form.html | 5 + urls.py | 6 ++ 9 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 core/views/editor_helpers.py create mode 100644 templates/image_page_template.html create mode 100644 templates/image_selector.html create mode 100644 templates/linked_image_template.html create mode 100644 templates/new_image_form.html diff --git a/core/views/editor_helpers.py b/core/views/editor_helpers.py new file mode 100644 index 0000000..e0ff5cd --- /dev/null +++ b/core/views/editor_helpers.py @@ -0,0 +1,111 @@ +from django.shortcuts import render, redirect +from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse + +from django.urls import reverse, resolve +from django.template import Context, loader +import re, io +from PIL import Image +from pathlib import Path +import django.forms as forms +import troggle.settings as settings + +from troggle.lib import version_control + +MAX_IMAGE_WIDTH = 1000 +MAX_IMAGE_HEIGTH = 800 + +THUMBNAIL_WIDTH = 200 +THUMBNAIL_HEIGTH = 200 + +def image_selector(request, path): + '''Returns available images''' + directory = path.rsplit('/', 1)[0] + thumbnailspath = Path(settings.EXPOWEB) / directory / "t" + thumbnails = [] + for f in thumbnailspath.iterdir(): + if f.is_file(): + thumbnail_url = reverse('expopage', args=["%s/t/%s" % (directory, f.name)]) + name_base = f.name.rsplit('.', 1)[0] + page_path_base = Path(settings.EXPOWEB) / directory / "l" + if ((page_path_base / ("%s.htm" % name_base)).is_file()): + page_url = reverse('expopage', args=["%s/l/%s.htm" % (directory, name_base)]) + else: + page_url = reverse('expopage', args=["%s/l/%s.html" % (directory, name_base)]) + + thumbnails.append({"thumbnail_url": thumbnail_url, "page_url": page_url}) + + return render(request, 'image_selector.html', {'thumbnails': thumbnails}) + +def new_image_form(request, path): + '''Manages a form to upload new images''' + directory = path.rsplit('/', 1)[0] + if request.method == 'POST': + form = NewWebImageForm(request.POST, request.FILES, directory = directory) + if form.is_valid(): + f = request.FILES["file_"] + binary_data = io.BytesIO() + for chunk in f.chunks(): + binary_data.write(chunk) + i = Image.open(binary_data) + width, height = i.size + if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGTH: + scale = max(width / MAX_IMAGE_WIDTH, height / MAX_IMAGE_HEIGTH) + i = i.resize((int(width / scale), int(height / scale)), Image.ANTIALIAS) + tscale = max(width / THUMBNAIL_WIDTH, height / THUMBNAIL_HEIGTH) + thumbnail = i.resize((int(width / tscale), int(height / tscale)), Image.ANTIALIAS) + ib = io.BytesIO() + i.save(ib, format="png") + tb = io.BytesIO() + thumbnail.save(tb, format="png") + image_rel_path, thumb_rel_path, desc_rel_path = form.get_rel_paths() + image_page_template = loader.get_template('image_page_template.html') + image_page = image_page_template.render({'header': form.cleaned_data["header"], 'description': form.cleaned_data["description"], + 'photographer': form.cleaned_data["photographer"], 'year': form.cleaned_data["year"], + 'filepath': f'/{image_rel_path}' + }) + image_path, thumb_path, desc_path = form.get_full_paths() + try: + change_message = form.cleaned_data["change_message"] + version_control.write_and_commit([(desc_path, image_page, "utf-8"), + (image_path, ib.getbuffer(), False), + (thumb_path, tb.getbuffer(), False)], + f'{change_message} - online adding of an image') + except version_control.WriteAndCommitError as e: + return JsonResponse({"error": e.message}) + linked_image_template = loader.get_template('linked_image_template.html') + html_snippet = linked_image_template.render({'thumbnail_url': f'/{thumb_rel_path}', 'page_url': f'/{desc_rel_path}'}, request) + return JsonResponse({"html": html_snippet}) + else: + form = NewWebImageForm(directory = directory) + template = loader.get_template('new_image_form.html') + htmlform = template.render({'form': form, 'path': path}, request) + return JsonResponse({"form": htmlform}) + +class NewWebImageForm(forms.Form): + '''The form used by the editexpopage function + ''' + header = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Enter title (displayed as a header and in the tab)"})) + file_ = forms.FileField() + description = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":20, 'placeholder': "Describe the photo (using HTML)"})) + photographer = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Photographers name"}), required = False) + year = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Year photo was taken"}), required = False) + change_message = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":3, 'placeholder': "Descibe the change made (for git)"})) + + def __init__(self, *args, **kwargs): + self.directory = Path(kwargs.pop('directory')) + super(forms.Form, self).__init__(*args, **kwargs) + + def get_rel_paths(self): + f = self.cleaned_data['file_'] + return [self.directory / "i" / (f.name.rsplit('.', 1)[0] + ".png"), + self.directory / "t" / (f.name.rsplit('.', 1)[0] + ".png"), + self.directory / "l" / (f.name.rsplit('.', 1)[0] + ".html")] + + def get_full_paths(self): + return [Path(settings.EXPOWEB) / x for x in self.get_rel_paths()] + + def clean_file_(self): + for rel_path, full_path in zip(self.get_rel_paths(), self.get_full_paths()): + if full_path.exists(): + raise forms.ValidationError("File already exists in %s" % rel_path) + return self.cleaned_data['file_'] diff --git a/core/views/expo.py b/core/views/expo.py index 00cb6dc..91919a5 100644 --- a/core/views/expo.py +++ b/core/views/expo.py @@ -359,7 +359,7 @@ def editexpopage(request, path): if result != html: # Check if content changed try: change_message = pageform.cleaned_data["change_message"] - version_control.write_and_commit(filepath, result, f'{change_message} - online edit of {path}') + version_control.write_and_commit([(filepath, result, "utf-8")], f'{change_message} - online edit of {path}') except version_control.WriteAndCommitError as e: return render(request,'errors/generic.html', {'message': e.message}) diff --git a/lib/version_control.py b/lib/version_control.py index 6343914..01bd8ba 100644 --- a/lib/version_control.py +++ b/lib/version_control.py @@ -1,28 +1,35 @@ import troggle.settings as settings import subprocess -def write_and_commit(filepath, content, message): +def write_and_commit(files, message): """Writes the content to the filepath and adds and commits the file to git. If this fails, a WriteAndCommitError is raised.""" - cwd = filepath.parent - filename = filepath.name git = settings.GIT - # GIT see also core/models/cave.py writetrogglefile() - # GIT see also core/views/uploads.py dwgupload() + try: + for filepath, content, encoding in files: + cwd = filepath.parent + filename = filepath.name + # GIT see also core/models/cave.py writetrogglefile() + # GIT see also core/views/uploads.py dwgupload() - try: - with open(filepath, "w", encoding="utf8") as f: - print(f'WRITING{cwd}---{filename} ') - # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod. - # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r-- - f.write(content) - except PermissionError: - raise WriteAndCommitError(f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.') + if encoding: + mode = "w" + kwargs = {"encoding": encoding} + else: + mode = "wb" + kwargs = {} + try: + with open(filepath, mode, **kwargs) as f: + print(f'WRITING{cwd}---{filename} ') + # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod. + # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r-- + f.write(content) + except PermissionError: + raise WriteAndCommitError(f'CANNOT save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.') - try: - cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True) - if cp_add.returncode != 0: - msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout + '\n\nreturn code: ' + str(cp_add.returncode) - raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata) + cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True) + if cp_add.returncode != 0: + msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout + '\n\nreturn code: ' + str(cp_add.returncode) + raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata) cp_commit = subprocess.run([git, "commit", "-m", message], cwd=cwd, capture_output=True, text=True) # This produces return code = 1 if it commits OK, but when the repo still needs to be pushed to origin/expoweb diff --git a/templates/editexpopage.html b/templates/editexpopage.html index ef3aa20..b71c91f 100644 --- a/templates/editexpopage.html +++ b/templates/editexpopage.html @@ -5,6 +5,8 @@ + + @@ -38,9 +40,82 @@ height: 5%; } + {% endblock %} {% block body %}

Edit {{ path }}

+ + + + + +
{% csrf_token %} {{ form.non_field_errors }}
@@ -62,6 +137,7 @@ +
{{ form.change_message.errors }} @@ -70,6 +146,60 @@ {% include "menu.html" %}

+ + + {% endblock %} diff --git a/templates/image_page_template.html b/templates/image_page_template.html new file mode 100644 index 0000000..4a240e8 --- /dev/null +++ b/templates/image_page_template.html @@ -0,0 +1,24 @@ + + + + + +{{ header }} + + + + + +

{{ header }}

+
+
+ +

{{ description }}

+ +{% if photographer %} +

Photo © {{ photographer }}{% if year %}, {{ year }}{% endif %}

+{% endif %} + +
+ + diff --git a/templates/image_selector.html b/templates/image_selector.html new file mode 100644 index 0000000..5ac4e01 --- /dev/null +++ b/templates/image_selector.html @@ -0,0 +1,3 @@ +{% for thumbnail in thumbnails %} + +{% endfor %} diff --git a/templates/linked_image_template.html b/templates/linked_image_template.html new file mode 100644 index 0000000..336f1dd --- /dev/null +++ b/templates/linked_image_template.html @@ -0,0 +1 @@ + diff --git a/templates/new_image_form.html b/templates/new_image_form.html new file mode 100644 index 0000000..1a2f636 --- /dev/null +++ b/templates/new_image_form.html @@ -0,0 +1,5 @@ +
+ {% csrf_token %} + {{ form.as_p }} + +
diff --git a/urls.py b/urls.py index da420a0..5f5a711 100644 --- a/urls.py +++ b/urls.py @@ -23,6 +23,7 @@ from troggle.core.views.statistics import pathsreport, stats, dataissues from troggle.core.views.expo import expofiles_redirect, expofilessingle, expopage, editexpopage, mediapage, map, mapfile from troggle.core.views.survex import survexcaveslist, survexcavesingle, svx from troggle.core.views.auth import expologin, expologout +from troggle.core.views.editor_helpers import image_selector, new_image_form """This sets the actualurlpatterns[] and urlpatterns[] lists which django uses to resolve urls - in both directions as these are declarative. @@ -190,6 +191,11 @@ trogglepatterns = [ re_path(r'^/loser/(?P.*)$', mediapage, {'doc_root': settings.SURVEX_DATA}, name="mediapage"), # Oddly not working !? re_path(r'^map/map.html', map, name="map"), # Redirects to OpenStreetMap JavaScript re_path(r'^map/(?P.*)$', mapfile, name="mapfile"), # css, js, gpx + +# Helpers to edit HTML + re_path(r'^image_selector/(?P.*)', image_selector, name = 'image_selector'), + re_path(r'^new_image_form/(?P.*)', new_image_form, name = 'new_image_form'), + # Final catchall which also serves expoweb handbook pages and images re_path(r'^(.*)$', expopage, name="expopage"), # CATCHALL assumed relative to EXPOWEB