2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2024-11-25 00:31:55 +00:00

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.

This commit is contained in:
Martin Green 2022-06-25 23:17:19 +01:00
parent b3d9e81499
commit 20583b04c0
9 changed files with 313 additions and 19 deletions

View File

@ -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_']

View File

@ -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})

View File

@ -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

View File

@ -5,6 +5,8 @@
<!--<script src="{{ settings.TINY_MCE_MEDIA_URL }}tiny_mce.js" type="text/javascript"></script>-->
<!-- <script type="text/javascript"> tinyMCE.init({ mode : "textareas" }); </script>-->
<script src="{{ settings.MEDIA_URL }}admin/js/vendor/jquery/jquery.js" type="text/javascript"></script>
<script src={{ settings.MEDIA_URL }}codemirror/codemirror.js></script>
<script src={{ settings.MEDIA_URL }}codemirror/xml.js></script>
<script src={{ settings.MEDIA_URL }}codemirror/javascript.js></script>
@ -38,9 +40,82 @@
height: 5%;
}
</style>
<style type=text/css>
html {
font-family: "Helvetica Neue", sans-serif;
width: 100%;
color: #666666;
text-align: center;
}
.popup-overlay {
/*Hides pop-up when there is no "active" class*/
visibility: hidden;
position: absolute;
background: #ffffff;
border: 3px solid #666666;
width: 90%;
height: 80%;
overflow: scroll;
left: 5%;
z-index: 20;
}
.popup-overlay.active {
/*displays pop-up when "active" class is present*/
visibility: visible;
text-align: center;
}
.popup-content {
/*Hides pop-up content when there is no "active" class */
visibility: hidden;
}
.popup-content.active {
/*Shows pop-up content when "active" class is present */
visibility: visible;
}
button {
display: inline-block;
vertical-align: middle;
border-radius: 30px;
margin: .20rem;
font-size: 1rem;
color: #666666;
background: #ffffff;
border: 1px solid #666666;
}
button:hover {
border: 1px solid #666666;
background: #666666;
color: #ffffff;
}
</style>
{% endblock %}
{% block body %}
<h1>Edit {{ path }}</h1>
<!--Creates the add image popup-->
<div class="add-image-popup popup-overlay">
<div class="add-image-popup popup-content">
<h2>Select Image</h2>
<p id="image_popup_content"> Loading ...</p>
<button onclick="new_image_popup()">Upload Image</button>
<button class="close" onclick="$('.add-image-popup').removeClass('active');">Close</button>
</div>
</div>
<!--Creates the new image popup-->
<div class="new-image-popup popup-overlay">
<div class="new-image-popup popup-content">
<h2>New Image</h2>
<p id="new_image_popup_content"> Loading ...</p>
<button class="close" onclick="$('.new-image-popup').removeClass('active');">Close</button>
</div>
</div>
<form action="" method="post">{% csrf_token %}
{{ form.non_field_errors }}
<div class="fieldWrapper">
@ -62,6 +137,7 @@
<button type="button" onclick="addTag('h4', '')">heading 4</button>
<button type="button" onclick="addTag('a', 'href=&quot;&quot;')">hyperlink</button>
<button type="button" onclick="addTag('p', '')">paragraph</button>
<button type="button" onclick="add_image_popup()">image</button>
<div class="fieldWrapper">
{{ form.change_message.errors }}
<label for="{{ form.title.id_for_label }}">Git change message:</label>
@ -70,6 +146,60 @@
{% include "menu.html" %}
<p><input type="submit" value="Submit" /></p>
</form>
<script>
function add_image_popup() {
$('.add-image-popup').addClass('active');
$('#image_popup_content').load("{% url 'image_selector' path %}", function() {
$('.thumbnail').click(function(){
$(".add-image-popup").removeClass("active");
addStr($( this ).attr("data-html"))
});
})
}
function new_image_popup() {
$('.add-image-popup').removeClass('active');
$('.new-image-popup').addClass('active');
$.ajax({
type : "GET",
dataType: "json",
url: "{% url 'new_image_form' path %}",
success: function(data){handle_new_image(data)}
});
}
function handle_new_image(data) {
if (data.hasOwnProperty('form')) {
$('#new_image_popup_content').html(data.form);
$('#new_image_form').on('submit', function(e){
e.preventDefault();
data = $('#new_image_form').serialize();
$.ajax({
type : "POST",
dataType: "json",
url: "{% url 'new_image_form' path %}",
data: new FormData($('#new_image_form')[0]),
processData: false,
contentType: false,
success: function(data){
handle_new_image(data);
}
});
});
}
else if (data.hasOwnProperty('html')) {
$('.new-image-popup').removeClass('active');
addStr(data.html);
}
else {
alert(data.error);
}
}
</script>
<script>
var delay;
// Initialize CodeMirror editor with a nice html5 canvas demo.
@ -103,5 +233,12 @@
editor.focus();
editor.setCursor({line: to.line , ch : to.ch + 2 + tag.length + attr.length });
}
function addStr(x){
var to = editor.getCursor(false);
editor.replaceRange(x, to);
editor.focus();
editor.setCursor({line: to.line , ch : to.ch + x.length });
}
</script>
{% endblock %}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
<title>
{{ header }}
</title>
<link rel="stylesheet" type="text/css" href="../../../css/main2.css" />
</head>
<body>
<H1>{{ header }}</H1>
<div class="centre"><img alt="" src="{{ filepath }}" />
</div>
<p>{{ description }}</p>
{% if photographer %}
<p class="caption">Photo &copy; {{ photographer }}{% if year %}, {{ year }}{% endif %}</p>
{% endif %}
<hr />
</body>
</html>

View File

@ -0,0 +1,3 @@
{% for thumbnail in thumbnails %}
<img class = "thumbnail" src = "{{ thumbnail.thumbnail_url }}" data-html = "{% include 'linked_image_template.html' with thumbnail_url=thumbnail.thumbnail_url page_url=thumbnail.page_url %}"/>
{% endfor %}

View File

@ -0,0 +1 @@
<a href='{{ page_url }}'><img src='{{ thumbnail_url }}' /></a>

View File

@ -0,0 +1,5 @@
<form id="new_image_form" action="{% url 'new_image_form' path %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>

View File

@ -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<subpath>.*)$', 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<path>.*)$', mapfile, name="mapfile"), # css, js, gpx
# Helpers to edit HTML
re_path(r'^image_selector/(?P<path>.*)', image_selector, name = 'image_selector'),
re_path(r'^new_image_form/(?P<path>.*)', 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