2023-01-19 18:35:56 +00:00
|
|
|
import io
|
|
|
|
from pathlib import Path
|
2022-06-25 23:17:19 +01:00
|
|
|
|
2023-01-19 18:35:56 +00:00
|
|
|
import django.forms as forms
|
2023-01-30 23:04:11 +00:00
|
|
|
from django.http import JsonResponse
|
|
|
|
from django.shortcuts import render
|
|
|
|
from django.template import loader
|
|
|
|
from django.urls import reverse
|
2023-01-19 18:35:56 +00:00
|
|
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
2022-06-25 23:17:19 +01:00
|
|
|
from PIL import Image
|
2023-01-19 18:35:56 +00:00
|
|
|
|
2023-07-13 11:34:52 +01:00
|
|
|
import piexif
|
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
import troggle.settings as settings
|
2023-01-19 18:35:56 +00:00
|
|
|
from troggle.core.utils import WriteAndCommitError, write_and_commit
|
2022-06-25 23:17:19 +01:00
|
|
|
|
2022-06-26 01:15:00 +01:00
|
|
|
from .auth import login_required_if_public
|
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
MAX_IMAGE_WIDTH = 1000
|
2022-06-25 23:36:53 +01:00
|
|
|
MAX_IMAGE_HEIGHT = 800
|
2022-06-25 23:17:19 +01:00
|
|
|
|
|
|
|
THUMBNAIL_WIDTH = 200
|
2022-06-25 23:36:53 +01:00
|
|
|
THUMBNAIL_HEIGHT = 200
|
2022-06-25 23:17:19 +01:00
|
|
|
|
2023-11-02 19:05:08 +00:00
|
|
|
"""This is all written by Martin Green, and completely undocumented.
|
|
|
|
|
|
|
|
It uses a lot of opinionated Django default magic, none of which I am familiar with.
|
|
|
|
Philip
|
2024-07-10 20:07:45 +01:00
|
|
|
|
|
|
|
The function image_selector() is only called from a template file doing a popup input form,
|
|
|
|
but it stores files directly in expoweb/i /l /t instead of in
|
|
|
|
/expoweb/1623/2018-MS-01/i , /l, /t
|
|
|
|
so ALL new caves have photos in the smae place, which makes the default upload EXTREMELY CONFUSING
|
2023-11-02 19:05:08 +00:00
|
|
|
"""
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-26 11:20:14 +01:00
|
|
|
def get_dir(path):
|
|
|
|
"From a path sent from urls.py, determine the directory."
|
2023-11-02 19:05:08 +00:00
|
|
|
# todo re-write this to use modern pathlib functions
|
2022-06-26 11:20:14 +01:00
|
|
|
if "/" in path:
|
2023-01-30 19:04:36 +00:00
|
|
|
return path.rsplit("/", 1)[0]
|
2022-06-26 11:20:14 +01:00
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
def image_selector(request, path):
|
2024-07-10 20:07:45 +01:00
|
|
|
"""Returns available images
|
|
|
|
called from
|
|
|
|
templates/html_editor_scripts_css.html: $('#image_popup_content').load("{% url 'image_selector' path %}" + path, function() {
|
|
|
|
"""
|
2022-06-26 11:20:14 +01:00
|
|
|
directory = get_dir(path)
|
2023-11-02 19:05:08 +00:00
|
|
|
print(f" - image selector {directory=} {path=}")
|
2022-06-25 23:17:19 +01:00
|
|
|
thumbnailspath = Path(settings.EXPOWEB) / directory / "t"
|
|
|
|
thumbnails = []
|
2022-06-26 11:20:14 +01:00
|
|
|
if thumbnailspath.is_dir():
|
|
|
|
for f in thumbnailspath.iterdir():
|
|
|
|
if f.is_file():
|
|
|
|
if directory:
|
|
|
|
base = f"{directory}/"
|
|
|
|
else:
|
|
|
|
base = ""
|
2023-01-30 19:04:36 +00:00
|
|
|
thumbnail_url = reverse("expopage", args=[f"{base}t/{f.name}"])
|
|
|
|
name_base = f.name.rsplit(".", 1)[0]
|
2022-06-26 11:20:14 +01:00
|
|
|
page_path_base = Path(settings.EXPOWEB) / directory / "l"
|
2023-01-30 19:04:36 +00:00
|
|
|
if (page_path_base / (f"{name_base}.htm")).is_file():
|
|
|
|
page_url = reverse("expopage", args=[f"{base}l/{name_base}.htm"])
|
2022-06-26 11:20:14 +01:00
|
|
|
else:
|
2023-07-05 13:18:02 +01:00
|
|
|
page_url = reverse("expopage", args=[f"{base}l/{name_base}.html"])
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-26 11:20:14 +01:00
|
|
|
thumbnails.append({"thumbnail_url": thumbnail_url, "page_url": page_url})
|
2023-01-30 19:04:36 +00:00
|
|
|
|
|
|
|
return render(request, "image_selector.html", {"thumbnails": thumbnails})
|
2023-07-13 11:34:52 +01:00
|
|
|
|
|
|
|
def reorient_image(img, exif_dict):
|
|
|
|
if piexif.ImageIFD.Orientation in exif_dict["0th"]:
|
|
|
|
print(exif_dict)
|
|
|
|
orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
|
|
|
|
|
|
|
|
if orientation == 2:
|
|
|
|
img = img.transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
elif orientation == 3:
|
|
|
|
img = img.rotate(180)
|
|
|
|
elif orientation == 4:
|
|
|
|
img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
elif orientation == 5:
|
|
|
|
img = img.rotate(-90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
elif orientation == 6:
|
|
|
|
img = img.rotate(-90, expand=True)
|
|
|
|
elif orientation == 7:
|
|
|
|
img = img.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
elif orientation == 8:
|
|
|
|
img = img.rotate(90, expand=True)
|
|
|
|
return img
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
|
2022-06-26 01:15:00 +01:00
|
|
|
@login_required_if_public
|
|
|
|
@ensure_csrf_cookie
|
2022-06-25 23:17:19 +01:00
|
|
|
def new_image_form(request, path):
|
2023-01-30 19:04:36 +00:00
|
|
|
"""Manages a form to upload new images"""
|
2022-06-26 11:20:14 +01:00
|
|
|
directory = get_dir(path)
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): {directory=} {path=}")
|
2023-01-30 19:04:36 +00:00
|
|
|
if request.method == "POST":
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): POST ")
|
2023-01-30 19:04:36 +00:00
|
|
|
form = NewWebImageForm(request.POST, request.FILES, directory=directory)
|
2022-06-25 23:17:19 +01:00
|
|
|
if form.is_valid():
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): form is valid ")
|
2024-07-10 21:18:39 +01:00
|
|
|
print(f"new_image_form(): files: {request.FILES['file_']}")
|
2022-06-25 23:17:19 +01:00
|
|
|
f = request.FILES["file_"]
|
|
|
|
binary_data = io.BytesIO()
|
|
|
|
for chunk in f.chunks():
|
|
|
|
binary_data.write(chunk)
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): binary chnks written")
|
2022-06-25 23:17:19 +01:00
|
|
|
i = Image.open(binary_data)
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): {i=}")
|
2023-07-13 11:34:52 +01:00
|
|
|
if "exif" in i.info:
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): exif: {i=}")
|
|
|
|
|
2023-07-13 11:34:52 +01:00
|
|
|
exif_dict = piexif.load(i.info["exif"])
|
|
|
|
i = reorient_image(i, exif_dict)
|
|
|
|
exif_dict['Exif'][41729] = b'1'
|
2023-10-20 12:00:38 +01:00
|
|
|
# can crash here with bad exif data
|
|
|
|
try:
|
|
|
|
exif = piexif.dump(exif_dict)
|
|
|
|
except:
|
|
|
|
exif = None
|
2023-07-13 11:34:52 +01:00
|
|
|
else:
|
|
|
|
exif = None
|
|
|
|
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): No exif problems")
|
2022-06-25 23:17:19 +01:00
|
|
|
width, height = i.size
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): {i.size=}")
|
2022-06-25 23:36:53 +01:00
|
|
|
if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGHT:
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): rescaling ")
|
2022-06-25 23:36:53 +01:00
|
|
|
scale = max(width / MAX_IMAGE_WIDTH, height / MAX_IMAGE_HEIGHT)
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): rescaling {scale=}")
|
|
|
|
try:
|
|
|
|
i = i.resize((int(width / scale), int(height / scale)), Image.LANCZOS)
|
|
|
|
except Exception as e:
|
|
|
|
print(f"new_image_form(): rescaling exception: {e} ")
|
|
|
|
|
|
|
|
print(f"new_image_form(): rescaled ")
|
2022-06-25 23:36:53 +01:00
|
|
|
tscale = max(width / THUMBNAIL_WIDTH, height / THUMBNAIL_HEIGHT)
|
2024-07-10 20:07:45 +01:00
|
|
|
thumbnail = i.resize((int(width / tscale), int(height / tscale)), Image.LANCZOS)
|
2022-06-25 23:17:19 +01:00
|
|
|
ib = io.BytesIO()
|
2023-07-05 21:08:51 +01:00
|
|
|
i = i.convert('RGB')
|
2023-04-29 22:23:40 +01:00
|
|
|
i.save(ib, format="jpeg", quality = 75)
|
2022-06-25 23:17:19 +01:00
|
|
|
tb = io.BytesIO()
|
2023-07-05 21:08:51 +01:00
|
|
|
thumbnail = thumbnail.convert('RGB')
|
2023-04-29 22:23:40 +01:00
|
|
|
thumbnail.save(tb, format="jpeg", quality = 70)
|
2022-06-25 23:17:19 +01:00
|
|
|
image_rel_path, thumb_rel_path, desc_rel_path = form.get_rel_paths()
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): \n {image_rel_path=}\n {thumb_rel_path=}\n {desc_rel_path=}")
|
2023-11-02 19:05:08 +00:00
|
|
|
image_page_template = loader.get_template("image_page_template.html") # this is the .html file in the /l/ folder
|
2023-01-30 19:04:36 +00:00
|
|
|
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}",
|
|
|
|
}
|
|
|
|
)
|
2022-06-25 23:17:19 +01:00
|
|
|
image_path, thumb_path, desc_path = form.get_full_paths()
|
2022-06-26 11:20:14 +01:00
|
|
|
# Create directories if required
|
|
|
|
for full_path in image_path, thumb_path, desc_path:
|
|
|
|
print(full_path, full_path.parent)
|
2023-07-08 22:42:06 +01:00
|
|
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
2022-06-25 23:17:19 +01:00
|
|
|
try:
|
|
|
|
change_message = form.cleaned_data["change_message"]
|
2023-01-30 19:04:36 +00:00
|
|
|
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",
|
|
|
|
)
|
2022-07-18 16:06:23 +01:00
|
|
|
except WriteAndCommitError as e:
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): WriteAndCommitError: {e.message}")
|
|
|
|
return JsonResponse({"error": e.message})
|
|
|
|
except Exception as e:
|
|
|
|
print(f"new_image_form(): EXCEPTION: {e.message}")
|
2022-06-25 23:17:19 +01:00
|
|
|
return JsonResponse({"error": e.message})
|
2024-07-10 20:07:45 +01:00
|
|
|
|
2023-01-30 19:04:36 +00:00
|
|
|
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
|
|
|
|
)
|
2022-06-25 23:17:19 +01:00
|
|
|
return JsonResponse({"html": html_snippet})
|
|
|
|
else:
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): not POST ")
|
2023-01-30 19:04:36 +00:00
|
|
|
form = NewWebImageForm(directory=directory)
|
2024-07-10 20:07:45 +01:00
|
|
|
print(f"new_image_form(): POST and not POST ")
|
2023-01-30 19:04:36 +00:00
|
|
|
template = loader.get_template("new_image_form.html")
|
|
|
|
htmlform = template.render({"form": form, "path": path}, request)
|
2022-06-25 23:17:19 +01:00
|
|
|
return JsonResponse({"form": htmlform})
|
2023-01-30 19:04:36 +00:00
|
|
|
|
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
class NewWebImageForm(forms.Form):
|
2023-01-30 19:04:36 +00:00
|
|
|
"""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)"}
|
|
|
|
)
|
|
|
|
)
|
2022-06-25 23:17:19 +01:00
|
|
|
file_ = forms.FileField()
|
2023-01-30 19:04:36 +00:00
|
|
|
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)"})
|
|
|
|
)
|
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
2023-01-30 19:04:36 +00:00
|
|
|
self.directory = Path(kwargs.pop("directory"))
|
2022-06-25 23:17:19 +01:00
|
|
|
super(forms.Form, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
def get_rel_paths(self):
|
2023-01-30 19:04:36 +00:00
|
|
|
f = self.cleaned_data["file_"]
|
|
|
|
return [
|
2023-04-29 22:23:40 +01:00
|
|
|
self.directory / "i" / (f.name.rsplit(".", 1)[0] + ".jpg"),
|
|
|
|
self.directory / "t" / (f.name.rsplit(".", 1)[0] + ".jpg"),
|
2023-01-30 19:04:36 +00:00
|
|
|
self.directory / "l" / (f.name.rsplit(".", 1)[0] + ".html"),
|
|
|
|
]
|
2022-06-25 23:17:19 +01:00
|
|
|
|
|
|
|
def get_full_paths(self):
|
2023-11-02 19:05:08 +00:00
|
|
|
return [Path(settings.EXPOWEB) / x for x in self.get_rel_paths()] # this is where we would want to insert the /caveid/ ?!
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-25 23:17:19 +01:00
|
|
|
def clean_file_(self):
|
|
|
|
for rel_path, full_path in zip(self.get_rel_paths(), self.get_full_paths()):
|
|
|
|
if full_path.exists():
|
2022-11-23 10:48:39 +00:00
|
|
|
raise forms.ValidationError(f"File already exists in {rel_path}")
|
2023-01-30 19:04:36 +00:00
|
|
|
return self.cleaned_data["file_"]
|
|
|
|
|
|
|
|
|
2022-06-26 14:16:42 +01:00
|
|
|
class HTMLarea(forms.Textarea):
|
2023-11-02 19:05:08 +00:00
|
|
|
"""This is called from CaveForm in core/forms.py which is called from core/views/caves.py when editing a cave description
|
|
|
|
(and similarly for Entrance Descriptions). It is alls called from core/views/expo.py when editing expo (i.e. handbook) pages.
|
|
|
|
"""
|
2022-06-26 14:16:42 +01:00
|
|
|
template_name = "widgets/HTMLarea.html"
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-26 18:29:20 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
2023-01-30 19:04:36 +00:00
|
|
|
self.preview = kwargs.pop("preview", False)
|
2022-06-26 18:29:20 +01:00
|
|
|
super(forms.Textarea, self).__init__(*args, **kwargs)
|
2023-01-30 19:04:36 +00:00
|
|
|
|
2022-06-26 18:29:20 +01:00
|
|
|
def get_context(self, name, value, attrs):
|
|
|
|
c = super(forms.Textarea, self).get_context(name, value, attrs)
|
2022-06-26 21:29:46 +01:00
|
|
|
c["preview"] = self.preview
|
2022-06-26 18:29:20 +01:00
|
|
|
return c
|