mirror of
https://github.com/MarcZierle/photo-log-backend.git
synced 2024-12-29 10:57:58 +00:00
add all updates
This commit is contained in:
parent
3e368b6e3f
commit
e91e83df40
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
static/*
|
||||
./*/*pycache*/*
|
0
.gitmodules
vendored
Normal file → Executable file
0
.gitmodules
vendored
Normal file → Executable file
0
accounts/__init__.py
Normal file → Executable file
0
accounts/__init__.py
Normal file → Executable file
0
accounts/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/admin.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/admin.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/apps.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/apps.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/managers.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/managers.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/models.cpython-39.pyc
Normal file → Executable file
0
accounts/__pycache__/models.cpython-39.pyc
Normal file → Executable file
0
accounts/admin.py
Normal file → Executable file
0
accounts/admin.py
Normal file → Executable file
0
accounts/apps.py
Normal file → Executable file
0
accounts/apps.py
Normal file → Executable file
0
accounts/managers.py
Normal file → Executable file
0
accounts/managers.py
Normal file → Executable file
0
accounts/migrations/0001_initial.py
Normal file → Executable file
0
accounts/migrations/0001_initial.py
Normal file → Executable file
0
accounts/migrations/__init__.py
Normal file → Executable file
0
accounts/migrations/__init__.py
Normal file → Executable file
0
accounts/migrations/__pycache__/0001_initial.cpython-39.pyc
Normal file → Executable file
0
accounts/migrations/__pycache__/0001_initial.cpython-39.pyc
Normal file → Executable file
0
accounts/migrations/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
0
accounts/migrations/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
0
accounts/models.py
Normal file → Executable file
0
accounts/models.py
Normal file → Executable file
0
accounts/tests.py
Normal file → Executable file
0
accounts/tests.py
Normal file → Executable file
0
accounts/views.py
Normal file → Executable file
0
accounts/views.py
Normal file → Executable file
0
scrapers/__init__.py → api/__init__.py
Normal file → Executable file
0
scrapers/__init__.py → api/__init__.py
Normal file → Executable file
BIN
api/__pycache__/__init__.cpython-39.pyc
Executable file
BIN
api/__pycache__/__init__.cpython-39.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/admin.cpython-39.pyc
Executable file
BIN
api/__pycache__/admin.cpython-39.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/apps.cpython-39.pyc
Executable file
BIN
api/__pycache__/apps.cpython-39.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/models.cpython-39.pyc
Executable file
BIN
api/__pycache__/models.cpython-39.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/photolog_generator.cpython-39.pyc
Normal file
BIN
api/__pycache__/photolog_generator.cpython-39.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/photolog_layout.cpython-39.pyc
Normal file
BIN
api/__pycache__/photolog_layout.cpython-39.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/serializers.cpython-39.pyc
Normal file
BIN
api/__pycache__/serializers.cpython-39.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/urls.cpython-39.pyc
Normal file
BIN
api/__pycache__/urls.cpython-39.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/views.cpython-39.pyc
Normal file
BIN
api/__pycache__/views.cpython-39.pyc
Normal file
Binary file not shown.
0
scrapers/admin.py → api/admin.py
Normal file → Executable file
0
scrapers/admin.py → api/admin.py
Normal file → Executable file
4
scrapers/apps.py → api/apps.py
Normal file → Executable file
4
scrapers/apps.py → api/apps.py
Normal file → Executable file
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ScrapersConfig(AppConfig):
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'scrapers'
|
||||
name = 'api'
|
1
api/autocrop
Submodule
1
api/autocrop
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 70828ba4e14e67a1db819de5c6371713145f868c
|
0
scrapers/migrations/__init__.py → api/migrations/__init__.py
Normal file → Executable file
0
scrapers/migrations/__init__.py → api/migrations/__init__.py
Normal file → Executable file
BIN
api/migrations/__pycache__/__init__.cpython-39.pyc
Executable file
BIN
api/migrations/__pycache__/__init__.cpython-39.pyc
Executable file
Binary file not shown.
0
scrapers/models.py → api/models.py
Normal file → Executable file
0
scrapers/models.py → api/models.py
Normal file → Executable file
63
api/photolog_generator.py
Normal file
63
api/photolog_generator.py
Normal file
@ -0,0 +1,63 @@
|
||||
from photo_log.models import Photo
|
||||
|
||||
import os
|
||||
|
||||
from django_tex.shortcuts import render_to_pdf, compile_template_to_pdf
|
||||
from django.db.models import FileField
|
||||
from django.conf import settings
|
||||
|
||||
from .photolog_layout import generate_tex
|
||||
|
||||
|
||||
def get_img_path_by_id(photo_id):
|
||||
if not photo_id:
|
||||
return None
|
||||
photo = Photo.objects.get(id=photo_id)
|
||||
img = None
|
||||
if not photo.cropped_image.name:
|
||||
img = photo.original_image.url
|
||||
else:
|
||||
img = photo.cropped_image.url
|
||||
return img
|
||||
|
||||
|
||||
def get_all_photo_paths(slides, start_slide_image):
|
||||
id_to_path = {}
|
||||
image_paths = []
|
||||
for slide in slides:
|
||||
for photo_id in slide:
|
||||
if photo_id == None:
|
||||
continue
|
||||
|
||||
if not photo_id in id_to_path:
|
||||
path = get_img_path_by_id(photo_id)
|
||||
id_to_path[photo_id] = path
|
||||
|
||||
image_paths.append(id_to_path[photo_id])
|
||||
|
||||
start_slide_photo = get_img_path_by_id(start_slide_image)
|
||||
if not start_slide_image in id_to_path:
|
||||
path = get_img_path_by_id(start_slide_image)
|
||||
id_to_path[start_slide_image] = path
|
||||
|
||||
if start_slide_photo and not start_slide_photo in image_paths:
|
||||
image_paths.append(start_slide_photo)
|
||||
|
||||
id_to_name = {}
|
||||
for id in id_to_path:
|
||||
id_to_name[id], _ = os.path.splitext(os.path.basename(id_to_path[id]))
|
||||
|
||||
return image_paths, id_to_name
|
||||
|
||||
|
||||
def generate_photolog_from_latex(request, title, date, render_date, start_slide_image, slides):
|
||||
image_paths, id_to_name = get_all_photo_paths(slides, start_slide_image)
|
||||
|
||||
template_name = 'photolog.tex'
|
||||
context = {
|
||||
'content': generate_tex(title, date, render_date, start_slide_image, slides, id_to_name)
|
||||
}
|
||||
|
||||
pdf_bytes = compile_template_to_pdf(template_name, context)
|
||||
|
||||
return pdf_bytes
|
54
api/photolog_layout.py
Normal file
54
api/photolog_layout.py
Normal file
@ -0,0 +1,54 @@
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def generate_tex(title, date, render_date, start_slide_image, slides, id_to_name):
|
||||
log_start = "% !TEX program = xelatex\n\\documentclass[aspectratio=169]{beamer}\n\\usepackage{graphicx}\n\\usepackage{tikz}\n\\usepackage[absolute,overlay]{textpos}\n\\definecolor{gray1}{rgb}{0.85,0.85,0.85}\n\\definecolor{gray2}{rgb}{0.95,0.95,0.95}\n\\setbeamertemplate{background canvas}{%\n\\begin{tikzpicture}[remember picture,overlay]\n\\begin{scope}[xshift=0.55\\textwidth, yshift=-4.5cm]\n\\fill[even odd rule,inner color=gray2,outer color=gray1] (0,0) circle (10);\n\\node[inner sep=0pt] at (0.26,-4.02) {\\includegraphics[width=1.16\\textwidth]{wood_floor}};\n\\end{scope}\n\\end{tikzpicture}%\n}\n\\setbeamertemplate{navigation symbols}{}\n\\vfuzz=1000pt\n\\hfuzz=1000pt\n\\usepackage{fontspec}\n\\newfontfamily{\\gill} [ Path = "+str(settings.BASE_DIR)+"/static/photolog_assets/,UprightFont = * Medium,BoldFont = * Bold] {Gill Sans MT}\n\\definecolor{title_line_red}{rgb}{0.8,0.2,0.2}\n\\makeatletter\n\\let\\old@rule\\@rule\n\\def\\@rule[#1]#2#3{\\textcolor{title_line_red}{\\old@rule[#1]{#2}{#3}}}\n\\makeatother\n\\title{Fotoprotokoll}\n\\author{Antje Zierle-Kohlmorgen}\n\\institute{Zierle-Training}\n\\date{2022}\n\\begin{document}\n\\gill"
|
||||
log_end = "\\begin{frame}\n\\begin{columns}\n\\begin{column}{0.6\\textwidth}\n\\begin{center}\n\\includegraphics[width=0.8\\textwidth]{smile}\n\\end{center}\n\\end{column}\n\\begin{column}{0.4\\textwidth}\n\\Huge{Viel Erfolg!}\n\\end{column}\n\\end{columns}\n\\end{frame}\n\\end{document}"
|
||||
|
||||
title_slide_start = "\\begin{frame}\n\\begin{textblock*}{13.0cm}(3cm,1.75cm)\n\\huge{"
|
||||
title_slide_end_w_photo = "}\\\\[-0.25cm]\n\\rule{14cm}{0.05cm}\n\\begin{tikzpicture}[remember picture, overlay]\n\\node[xshift=0.8\\textwidth,yshift=-0.5cm]{\\includegraphics[height=4.5cm]{%s}};\n\\end{tikzpicture}\n\\end{textblock*}\n\\end{frame}"
|
||||
title_slide_end_wo_photo = "}\\\\[-0.25cm]\n\\rule{14cm}{0.05cm}\n\n\\end{textblock*}\n\\end{frame}"
|
||||
|
||||
slide_start = "\\begin{frame}\n\\begin{columns}"
|
||||
slide_end = "\\end{columns}\n\\hfill\\\\[1.5cm]\n\\end{frame}"
|
||||
|
||||
photo_start = "\\begin{column}{%.3f\\textwidth}\n\\begin{center}\n\\includegraphics[height=6.5cm]{"
|
||||
photo_end = "}\\end{center}\n\\end{column}"
|
||||
|
||||
assets_path = "{%s}" % (str(settings.BASE_DIR) + "/static/photolog_assets/")
|
||||
cropped_path = "{%s}" % (str(settings.BASE_DIR) + "/static/cropped_images/")
|
||||
original_path = "{%s}" % (str(settings.BASE_DIR) + "/static/original_images/")
|
||||
include_paths = "\\graphicspath{"+assets_path+","+cropped_path+","+original_path+"}\n"
|
||||
|
||||
tex_file_content = log_start
|
||||
tex_file_content += include_paths
|
||||
|
||||
tex_file_content += title_slide_start + title
|
||||
if date and render_date:
|
||||
date = datetime.strptime(date, '%Y-%m-%d')
|
||||
tex_file_content += "\\\\" + date.strftime("%d.%m.%Y")
|
||||
|
||||
if start_slide_image:
|
||||
tex_file_content += title_slide_end_w_photo % (id_to_name[start_slide_image])
|
||||
else:
|
||||
tex_file_content += title_slide_end_wo_photo
|
||||
|
||||
for slide in slides:
|
||||
tex_file_content += slide_start
|
||||
num_none_photos = sum([1 for p in slide if p==None])
|
||||
|
||||
for photo_id in slide:
|
||||
if not photo_id:
|
||||
continue
|
||||
if num_none_photos == 2: # slides are saved as [2, None, None] for only 1 image
|
||||
tex_file_content += photo_start % (3.0/len(slide))
|
||||
else:
|
||||
tex_file_content += photo_start % (1.0/len(slide))
|
||||
tex_file_content += id_to_name[photo_id]
|
||||
tex_file_content += photo_end
|
||||
tex_file_content += slide_end
|
||||
|
||||
tex_file_content += log_end
|
||||
|
||||
return tex_file_content
|
38
api/serializers.py
Executable file
38
api/serializers.py
Executable file
@ -0,0 +1,38 @@
|
||||
from rest_framework import serializers
|
||||
from photo_log.models import PhotoGroup, Photo, PhotoLog
|
||||
|
||||
|
||||
class PhotoGroupSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PhotoGroup
|
||||
fields = ('id', 'name', 'date')
|
||||
|
||||
|
||||
class PhotosSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Photo
|
||||
fields = ('id', 'legacy_id', 'group')
|
||||
|
||||
|
||||
class PhotoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Photo
|
||||
fields = ('id', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'intersections', 'original_image', 'cropped_image', 'ocr_text')
|
||||
|
||||
|
||||
class PhotoUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Photo
|
||||
fields = ('id', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'ocr_text')
|
||||
|
||||
|
||||
class PhotoLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PhotoLog
|
||||
fields = ('id', 'title', 'date', 'render_date', 'start_slide_image', 'slides', 'pdf')
|
||||
|
||||
|
||||
class PhotoLogsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PhotoLog
|
||||
fields = ('id', 'title', 'date', 'pdf')
|
0
scrapers/tests.py → api/tests.py
Normal file → Executable file
0
scrapers/tests.py → api/tests.py
Normal file → Executable file
36
api/urls.py
Executable file
36
api/urls.py
Executable file
@ -0,0 +1,36 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
PhotoGroupAPIView,
|
||||
PhotosAPIView,
|
||||
PhotoAPIView,
|
||||
AddPhotoAPIView,
|
||||
AddPhotoGroupAPIView,
|
||||
AutoCropPhotoAPIView,
|
||||
PhotoLogsAPIView,
|
||||
PhotoLogAPIView,
|
||||
AddPhotoLogAPIView,
|
||||
DestroyPhotoLogAPIView,
|
||||
DestroyPhotoAPIView,
|
||||
UpdatePhotoLogAPIView,
|
||||
UpdatePhotoAPIView,
|
||||
GeneratePhotoLogAPIView,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
#path('', PhotoGroupAPIView.as_view()),
|
||||
path('photogroups/', PhotoGroupAPIView.as_view()),
|
||||
path('photos/', PhotosAPIView.as_view()),
|
||||
path('photo/<int:pk>/', PhotoAPIView.as_view()),
|
||||
path('addphoto/', AddPhotoAPIView.as_view()),
|
||||
path('addphotogroup/', AddPhotoGroupAPIView.as_view()),
|
||||
path('cropphoto/<int:pk>/', AutoCropPhotoAPIView.as_view()),
|
||||
path('photologs/', PhotoLogsAPIView.as_view()),
|
||||
path('photolog/<int:pk>/', PhotoLogAPIView.as_view()),
|
||||
path('addphotolog/', AddPhotoLogAPIView.as_view()),
|
||||
path('deletephotolog/<int:pk>/', DestroyPhotoLogAPIView.as_view()),
|
||||
path('deletephoto/<int:pk>/', DestroyPhotoAPIView.as_view()),
|
||||
path('updatephotolog/<int:pk>/', UpdatePhotoLogAPIView.as_view()),
|
||||
path('updatephoto/<int:pk>/', UpdatePhotoAPIView.as_view()),
|
||||
path('generatephotolog/<int:pk>/', GeneratePhotoLogAPIView.as_view()),
|
||||
]
|
258
api/views.py
Executable file
258
api/views.py
Executable file
@ -0,0 +1,258 @@
|
||||
from rest_framework import generics, views, status
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_list_or_404
|
||||
from photo_log.models import PhotoGroup, Photo, PhotoLog
|
||||
from .serializers import (
|
||||
PhotoGroupSerializer,
|
||||
PhotosSerializer,
|
||||
PhotoSerializer,
|
||||
PhotoUpdateSerializer,
|
||||
PhotoLogSerializer,
|
||||
PhotoLogsSerializer,
|
||||
)
|
||||
|
||||
from .photolog_generator import generate_photolog_from_latex
|
||||
from .autocrop.autocrop import autocrop
|
||||
|
||||
from django.db.models import FileField
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from io import BytesIO
|
||||
from PIL import Image, ExifTags
|
||||
|
||||
|
||||
class PhotoGroupAPIView(generics.ListAPIView):
|
||||
queryset = PhotoGroup.objects.all()
|
||||
serializer_class = PhotoGroupSerializer
|
||||
|
||||
|
||||
class PhotosAPIView(generics.ListAPIView):
|
||||
|
||||
serializer_class = PhotoSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Photo.objects.all()
|
||||
self.serializer_class = PhotosSerializer
|
||||
|
||||
photogroup = self.request.query_params.get('photogroup')
|
||||
if photogroup is not None:
|
||||
queryset = get_list_or_404(queryset, group=photogroup)
|
||||
self.serializer_class = PhotoSerializer
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class PhotoAPIView(generics.RetrieveAPIView):
|
||||
serializer_class = PhotoSerializer
|
||||
queryset = Photo.objects.all()
|
||||
|
||||
|
||||
class AddPhotoAPIView(generics.CreateAPIView):
|
||||
queryset = Photo.objects.all()
|
||||
serializer_class = PhotoSerializer
|
||||
|
||||
|
||||
class AddPhotoGroupAPIView(generics.CreateAPIView):
|
||||
queryset = PhotoGroup.objects.all()
|
||||
serializer_class = PhotoGroupSerializer
|
||||
|
||||
|
||||
class PhotoLogAPIView(generics.RetrieveAPIView):
|
||||
serializer_class = PhotoLogSerializer
|
||||
queryset = PhotoLog.objects.all()
|
||||
|
||||
|
||||
class PhotoLogsAPIView(generics.ListAPIView):
|
||||
serializer_class = PhotoLogsSerializer
|
||||
queryset = PhotoLog.objects.all()
|
||||
|
||||
|
||||
class AddPhotoLogAPIView(generics.CreateAPIView):
|
||||
queryset = PhotoLog.objects.all()
|
||||
serializer_class = PhotoLogSerializer
|
||||
|
||||
|
||||
class DestroyPhotoLogAPIView(generics.DestroyAPIView):
|
||||
queryset = PhotoLog.objects.all()
|
||||
serializer_class = PhotoLogSerializer
|
||||
|
||||
|
||||
class DestroyPhotoAPIView(generics.DestroyAPIView):
|
||||
queryset = Photo.objects.all()
|
||||
serializer_class = PhotoSerializer
|
||||
|
||||
|
||||
class UpdatePhotoLogAPIView(generics.UpdateAPIView):
|
||||
queryset = PhotoLog.objects.all()
|
||||
serializer_class = PhotoLogSerializer
|
||||
lookup_field = 'pk'
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'ok'})
|
||||
else:
|
||||
return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class UpdatePhotoAPIView(generics.UpdateAPIView):
|
||||
queryset = Photo.objects.all()
|
||||
serializer_class = PhotoUpdateSerializer
|
||||
lookup_field = 'pk'
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'ok'})
|
||||
else:
|
||||
return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class GeneratePhotoLogAPIView(generics.RetrieveAPIView):
|
||||
|
||||
queryset = PhotoLog.objects.all()
|
||||
serializer_class = PhotoLogSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
photolog = self.get_object()
|
||||
photolog_data = self.get_serializer(photolog).data
|
||||
|
||||
if photolog_data:
|
||||
title = photolog_data['title']
|
||||
date = photolog_data['date']
|
||||
render_date = photolog_data['render_date']
|
||||
start_slide_image = photolog_data['start_slide_image']
|
||||
slides = photolog_data['slides']
|
||||
|
||||
log_bytes = generate_photolog_from_latex(request, title, date, render_date, start_slide_image, slides)
|
||||
|
||||
if log_bytes:
|
||||
photolog.pdf.save('log.pdf', ContentFile(log_bytes))
|
||||
return Response({'pdf': 'https://server.riezel.com%s' % str(photolog.pdf.url)})
|
||||
|
||||
return Response({"error": "Not Found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
def crop_photo_and_save(id, save=False):
|
||||
return "code run with id " + str(id)
|
||||
|
||||
def save_cropped_pillow_image(photo, pil_img):
|
||||
cropped_img_field = photo.cropped_image
|
||||
|
||||
buffer = BytesIO()
|
||||
pil_img.save(fp=buffer, format="PNG")
|
||||
pil_img = ContentFile(buffer.getvalue())
|
||||
|
||||
img_name = "cropped_image.png"
|
||||
cropped_img_field.save(img_name, InMemoryUploadedFile(
|
||||
pil_img,
|
||||
None, # field_name
|
||||
img_name, # image name
|
||||
'image/png',
|
||||
pil_img.tell, # image size
|
||||
None
|
||||
))
|
||||
|
||||
def simple_crop_to_bbox(photo):
|
||||
img = Image.open(photo.original_image)
|
||||
img = rotateByExif(img)
|
||||
bbox = photo.bbox_coords
|
||||
rotate_angle = photo.rotate
|
||||
|
||||
if not img:
|
||||
return None
|
||||
|
||||
#if rotate_angle:
|
||||
# img = img.rotate(rotate_angle, expand=1)
|
||||
|
||||
if bbox:
|
||||
img = img.crop((
|
||||
bbox[0][0],
|
||||
bbox[0][1],
|
||||
bbox[2][0],
|
||||
bbox[2][1]
|
||||
))
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def rotateByExif(img):
|
||||
try:
|
||||
for orientation in ExifTags.TAGS.keys():
|
||||
if ExifTags.TAGS[orientation]=='Orientation':
|
||||
break
|
||||
|
||||
exif = img._getexif()
|
||||
|
||||
if exif[orientation] == 3:
|
||||
img=img.rotate(180, expand=True)
|
||||
elif exif[orientation] == 6:
|
||||
img=img.rotate(270, expand=True)
|
||||
elif exif[orientation] == 8:
|
||||
img=img.rotate(90, expand=True)
|
||||
|
||||
except (AttributeError, KeyError, IndexError):
|
||||
# cases: image don't have getexif
|
||||
pass
|
||||
return img
|
||||
|
||||
|
||||
class AutoCropPhotoAPIView(generics.RetrieveAPIView):
|
||||
|
||||
queryset = Photo.objects.all()
|
||||
serializer_class = PhotoSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
photo = None
|
||||
|
||||
photo_data = self.get_serializer(self.get_object()).data
|
||||
|
||||
if photo_data:
|
||||
photo_id = photo_data['id']
|
||||
|
||||
if photo_id:
|
||||
if not 'mode' in request.query_params:
|
||||
return Response({'error':'cropping mode not specified (auto, bbox or inters)'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
mode = request.query_params.get('mode')
|
||||
|
||||
if mode == 'bbox':
|
||||
photo = Photo.objects.get(id=photo_id)
|
||||
cropped_img = simple_crop_to_bbox(photo)
|
||||
save_cropped_pillow_image(photo, cropped_img)
|
||||
elif mode == 'auto' or mode == 'inters':
|
||||
photo = Photo.objects.get(id=photo_id)
|
||||
img = Image.open(photo.original_image)
|
||||
|
||||
img = rotateByExif(img)
|
||||
|
||||
try:
|
||||
cropped_img, _, bbox, intersections = autocrop(img)
|
||||
except Exception:
|
||||
cropped_img = img
|
||||
bbox = None
|
||||
intersections = None
|
||||
|
||||
if mode == 'auto':
|
||||
save_cropped_pillow_image(photo, cropped_img)
|
||||
|
||||
if bbox:
|
||||
photo.bbox_coords = bbox
|
||||
photo.save()
|
||||
|
||||
if intersections:
|
||||
photo.intersections = intersections
|
||||
photo.save()
|
||||
else:
|
||||
return Response({'error':'invalid cropping mode (auto, bbox or inters)'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if photo:
|
||||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
return Response({"error": "Not Found"}, status=status.HTTP_404_NOT_FOUND)
|
0
config/__init__.py
Normal file → Executable file
0
config/__init__.py
Normal file → Executable file
0
config/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
0
config/__pycache__/__init__.cpython-39.pyc
Normal file → Executable file
Binary file not shown.
Binary file not shown.
0
config/__pycache__/wsgi.cpython-39.pyc
Normal file → Executable file
0
config/__pycache__/wsgi.cpython-39.pyc
Normal file → Executable file
7
config/asgi.py
Normal file → Executable file
7
config/asgi.py
Normal file → Executable file
@ -9,8 +9,13 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
|
||||
import os
|
||||
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
application = get_asgi_application()
|
||||
#application = get_asgi_application()
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
})
|
||||
|
6
config/package-lock.json
generated
Normal file
6
config/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "config",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
34
config/settings.py
Normal file → Executable file
34
config/settings.py
Normal file → Executable file
@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-z465dl_(vk55hxbm0bj*mp-ok3!*=ssw#!$5s2nrxa!9j+67z+
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['server.riezel.com', 'localhost', '127.0.0.1']
|
||||
ALLOWED_HOSTS = ['server.riezel.com', 'localhost', '127.0.0.1', '192.168.1.244']
|
||||
|
||||
|
||||
# Application definition
|
||||
@ -39,14 +39,23 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# third-party
|
||||
'channels', # as high as possible (channels overloads 'runserver', may conflict with e.g. whitenoise)
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'drf_yasg',
|
||||
'django_tex',
|
||||
|
||||
# local
|
||||
'accounts',
|
||||
'scrapers',
|
||||
'photo_log',
|
||||
'api',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@ -54,6 +63,13 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8080',
|
||||
'http://192.168.1.244:8080',
|
||||
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@ -70,9 +86,22 @@ TEMPLATES = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'NAME': 'tex',
|
||||
'BACKEND': 'django_tex.engine.TeXEngine',
|
||||
'APP_DIRS': True,
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates/')],
|
||||
},
|
||||
]
|
||||
|
||||
LATEX_INTERPRETER = 'xelatex'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
)
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
ASGI_APPLICATION = 'config.asgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
@ -130,6 +159,7 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||
#STATICFILES_DIRS = [os.path.join(BASE_DIR, "static/")]
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
34
config/urls.py
Normal file → Executable file
34
config/urls.py
Normal file → Executable file
@ -15,8 +15,38 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.generic import TemplateView
|
||||
from django.conf.urls.static import static
|
||||
from django.conf import settings
|
||||
|
||||
# API documentation
|
||||
from rest_framework import permissions
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
|
||||
|
||||
api_patterns = [
|
||||
path('api/v1/', include('api.urls')),
|
||||
]
|
||||
|
||||
|
||||
# API docs schema
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Photo Log API",
|
||||
default_version="v1",
|
||||
description="Storing and retrieving photos for creating photo logs.",
|
||||
),
|
||||
patterns=api_patterns,
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('scrapers/', include('scrapers.urls')),
|
||||
path('api/admin/', admin.site.urls),
|
||||
path('api/v1/docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
]
|
||||
|
||||
urlpatterns += api_patterns
|
||||
urlpatterns += static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)
|
||||
|
0
config/wsgi.py
Normal file → Executable file
0
config/wsgi.py
Normal file → Executable file
0
photo_log/__init__.py
Executable file
0
photo_log/__init__.py
Executable file
BIN
photo_log/__pycache__/__init__.cpython-39.pyc
Executable file
BIN
photo_log/__pycache__/__init__.cpython-39.pyc
Executable file
Binary file not shown.
BIN
photo_log/__pycache__/admin.cpython-39.pyc
Normal file
BIN
photo_log/__pycache__/admin.cpython-39.pyc
Normal file
Binary file not shown.
BIN
photo_log/__pycache__/apps.cpython-39.pyc
Executable file
BIN
photo_log/__pycache__/apps.cpython-39.pyc
Executable file
Binary file not shown.
BIN
photo_log/__pycache__/models.cpython-39.pyc
Normal file
BIN
photo_log/__pycache__/models.cpython-39.pyc
Normal file
Binary file not shown.
8
photo_log/admin.py
Executable file
8
photo_log/admin.py
Executable file
@ -0,0 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import PhotoGroup, Photo, PhotoLog
|
||||
|
||||
|
||||
admin.site.register(PhotoGroup)
|
||||
admin.site.register(Photo)
|
||||
admin.site.register(PhotoLog)
|
||||
|
6
photo_log/apps.py
Executable file
6
photo_log/apps.py
Executable file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PhotoLogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'photo_log'
|
36
photo_log/migrations/0001_initial.py
Executable file
36
photo_log/migrations/0001_initial.py
Executable file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-05 17:12
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import photo_log.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PhotoGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, unique=True)),
|
||||
('date', models.DateField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Photo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('legacy_id', models.IntegerField(blank=True, null=True, unique=True)),
|
||||
('original_image', models.ImageField(upload_to='original_images/')),
|
||||
('cropped_image', models.ImageField(blank=True, null=True, upload_to='cropped_images/')),
|
||||
('ocr_text', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), size=None)),
|
||||
('group', models.ForeignKey(default=photo_log.models.get_default_photogroup, on_delete=django.db.models.deletion.SET_DEFAULT, to='photo_log.photogroup')),
|
||||
],
|
||||
),
|
||||
]
|
23
photo_log/migrations/0002_auto_20220105_1757.py
Executable file
23
photo_log/migrations/0002_auto_20220105_1757.py
Executable file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-05 17:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='cropped_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='static/cropped_images/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='original_image',
|
||||
field=models.ImageField(upload_to='static/original_images/'),
|
||||
),
|
||||
]
|
19
photo_log/migrations/0003_alter_photo_ocr_text.py
Normal file
19
photo_log/migrations/0003_alter_photo_ocr_text.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-06 08:52
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0002_auto_20220105_1757'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='ocr_text',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, null=True, size=None),
|
||||
),
|
||||
]
|
20
photo_log/migrations/0004_alter_photo_group.py
Normal file
20
photo_log/migrations/0004_alter_photo_group.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-06 12:48
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import photo_log.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0003_alter_photo_ocr_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, default=photo_log.models.get_default_photogroup, on_delete=django.db.models.deletion.SET_DEFAULT, to='photo_log.photogroup'),
|
||||
),
|
||||
]
|
26
photo_log/migrations/0005_photolog.py
Normal file
26
photo_log/migrations/0005_photolog.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-13 13:32
|
||||
|
||||
import datetime
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0004_alter_photo_group'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PhotoLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=5000)),
|
||||
('date', models.DateField(default=datetime.date.today)),
|
||||
('render_date', models.BooleanField(blank=True, default=True)),
|
||||
('start_slide_image', models.IntegerField(blank=True, default=3, null=True)),
|
||||
('slides', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(null=True), blank=True, null=True, size=3), size=None)),
|
||||
],
|
||||
),
|
||||
]
|
19
photo_log/migrations/0006_alter_photolog_slides.py
Normal file
19
photo_log/migrations/0006_alter_photolog_slides.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-13 15:11
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0005_photolog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photolog',
|
||||
name='slides',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(null=True), blank=True, null=True, size=3), blank=True, default=[], size=None),
|
||||
),
|
||||
]
|
20
photo_log/migrations/0007_alter_photolog_slides.py
Normal file
20
photo_log/migrations/0007_alter_photolog_slides.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-13 15:13
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import photo_log.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0006_alter_photolog_slides'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photolog',
|
||||
name='slides',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(null=True), blank=True, null=True, size=3), blank=True, default=photo_log.models.get_empty_photolog_default, size=None),
|
||||
),
|
||||
]
|
24
photo_log/migrations/0008_auto_20220118_1847.py
Normal file
24
photo_log/migrations/0008_auto_20220118_1847.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-18 18:47
|
||||
|
||||
from django.db import migrations, models
|
||||
import photo_log.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0007_alter_photolog_slides'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='cropped_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to=photo_log.models.get_cropped_photo_path),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='original_image',
|
||||
field=models.ImageField(upload_to=photo_log.models.get_original_photo_path),
|
||||
),
|
||||
]
|
19
photo_log/migrations/0009_photolog_pdf.py
Normal file
19
photo_log/migrations/0009_photolog_pdf.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-18 19:56
|
||||
|
||||
from django.db import migrations, models
|
||||
import photo_log.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0008_auto_20220118_1847'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='photolog',
|
||||
name='pdf',
|
||||
field=models.FileField(blank=True, null=True, upload_to=photo_log.models.get_photolog_pdf_path),
|
||||
),
|
||||
]
|
19
photo_log/migrations/0010_photo_bbox_coords.py
Normal file
19
photo_log/migrations/0010_photo_bbox_coords.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-21 11:47
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0009_photolog_pdf'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='photo',
|
||||
name='bbox_coords',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(default=None, null=True), size=2), blank=True, default=None, null=True, size=4),
|
||||
),
|
||||
]
|
18
photo_log/migrations/0011_alter_photo_ocr_text.py
Normal file
18
photo_log/migrations/0011_alter_photo_ocr_text.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.8 on 2022-01-22 11:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0010_photo_bbox_coords'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='ocr_text',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
]
|
18
photo_log/migrations/0012_photo_rotate.py
Normal file
18
photo_log/migrations/0012_photo_rotate.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.8 on 2022-05-03 14:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0011_alter_photo_ocr_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='photo',
|
||||
name='rotate',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
19
photo_log/migrations/0013_photo_intersections.py
Normal file
19
photo_log/migrations/0013_photo_intersections.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.8 on 2022-05-04 07:28
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('photo_log', '0012_photo_rotate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='photo',
|
||||
name='intersections',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(default=None, null=True), size=2), blank=True, default=None, null=True, size=None),
|
||||
),
|
||||
]
|
0
photo_log/migrations/__init__.py
Executable file
0
photo_log/migrations/__init__.py
Executable file
BIN
photo_log/migrations/__pycache__/0001_initial.cpython-39.pyc
Executable file
BIN
photo_log/migrations/__pycache__/0001_initial.cpython-39.pyc
Executable file
Binary file not shown.
BIN
photo_log/migrations/__pycache__/0002_auto_20220105_1757.cpython-39.pyc
Executable file
BIN
photo_log/migrations/__pycache__/0002_auto_20220105_1757.cpython-39.pyc
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
photo_log/migrations/__pycache__/0005_photolog.cpython-39.pyc
Normal file
BIN
photo_log/migrations/__pycache__/0005_photolog.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
photo_log/migrations/__pycache__/__init__.cpython-39.pyc
Executable file
BIN
photo_log/migrations/__pycache__/__init__.cpython-39.pyc
Executable file
Binary file not shown.
176
photo_log/models.py
Executable file
176
photo_log/models.py
Executable file
@ -0,0 +1,176 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
|
||||
from datetime import date
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
|
||||
class PhotoGroup(models.Model):
|
||||
name = models.CharField(unique=True, null=False, max_length=200)
|
||||
date = models.DateField(null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
def get_default_photogroup():
|
||||
return PhotoGroup.objects.get_or_create(
|
||||
name="Unsortiert",
|
||||
date=None,
|
||||
)
|
||||
|
||||
def get_original_photo_path(instance, filename):
|
||||
_, ext = os.path.splitext(filename)
|
||||
return 'static/original_images/%s%s' % (str(uuid.uuid4()), ext)
|
||||
|
||||
def get_cropped_photo_path(instance, filename):
|
||||
_, ext = os.path.splitext(filename)
|
||||
return 'static/cropped_images/%s%s' % (str(uuid.uuid4()), ext)
|
||||
|
||||
|
||||
class Photo(models.Model):
|
||||
legacy_id = models.IntegerField(unique=True, blank=True, null=True)
|
||||
original_image = models.ImageField(upload_to=get_original_photo_path, null=False, blank=False)
|
||||
cropped_image = models.ImageField(upload_to=get_cropped_photo_path, null=True, blank=True)
|
||||
bbox_coords = ArrayField(
|
||||
ArrayField(
|
||||
models.IntegerField(unique=False, blank=False, null=True, default=None),
|
||||
size=2
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
size=4,
|
||||
)
|
||||
rotate = models.FloatField(blank=True, null=True)
|
||||
intersections = ArrayField(
|
||||
ArrayField(
|
||||
models.IntegerField(unique=False, blank=False, null=True, default=None),
|
||||
size=2
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
PhotoGroup,
|
||||
null=False,
|
||||
blank=True,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=get_default_photogroup
|
||||
)
|
||||
ocr_text = models.CharField(max_length=200, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "Photo #" + str(self.id)
|
||||
|
||||
|
||||
@receiver(models.signals.post_delete, sender=Photo)
|
||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `MediaFile` object is deleted.
|
||||
"""
|
||||
if instance.original_image:
|
||||
if os.path.isfile(instance.original_image.path):
|
||||
os.remove(instance.original_image.path)
|
||||
|
||||
if instance.cropped_image:
|
||||
if os.path.isfile(instance.cropped_image.path):
|
||||
os.remove(instance.cropped_image.path)
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=Photo)
|
||||
def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes old file from filesystem
|
||||
when corresponding `MediaFile` object is updated
|
||||
with new file.
|
||||
"""
|
||||
if not instance.pk:
|
||||
return False
|
||||
|
||||
try:
|
||||
old_file = Photo.objects.get(pk=instance.pk).original_image
|
||||
except Photo.DoesNotExist:
|
||||
return False
|
||||
|
||||
new_file = instance.original_image
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
||||
os.remove(old_file.path)
|
||||
|
||||
try:
|
||||
old_file = Photo.objects.get(pk=instance.pk).cropped_image
|
||||
except Photo.DoesNotExist:
|
||||
return False
|
||||
|
||||
if old_file.name:
|
||||
new_file = instance.cropped_image
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
||||
os.remove(old_file.path)
|
||||
|
||||
|
||||
def get_empty_photolog_default():
|
||||
return []
|
||||
|
||||
def get_photolog_pdf_path(instance, filename):
|
||||
return "static/photolog_pdf/%s.pdf" % str(uuid.uuid4())
|
||||
|
||||
class PhotoLog(models.Model):
|
||||
title = models.CharField(null=False, blank=False, max_length=5000)
|
||||
date = models.DateField(auto_now=False, default=date.today)
|
||||
render_date = models.BooleanField(null=False, blank=True, default=True)
|
||||
start_slide_image = models.IntegerField(null=True, blank=True, default=3)
|
||||
slides = ArrayField(
|
||||
ArrayField(
|
||||
models.IntegerField(blank=False, null=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
size=3
|
||||
),
|
||||
null=False,
|
||||
blank=True,
|
||||
default=get_empty_photolog_default
|
||||
)
|
||||
pdf = models.FileField(upload_to=get_photolog_pdf_path, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@receiver(models.signals.post_delete, sender=PhotoLog)
|
||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `MediaFile` object is deleted.
|
||||
"""
|
||||
if instance.pdf:
|
||||
if os.path.isfile(instance.pdf.path):
|
||||
os.remove(instance.pdf.path)
|
||||
|
||||
@receiver(models.signals.pre_save, sender=PhotoLog)
|
||||
def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes old file from filesystem
|
||||
when corresponding `MediaFile` object is updated
|
||||
with new file.
|
||||
"""
|
||||
if not instance.pk:
|
||||
return False
|
||||
|
||||
try:
|
||||
old_file = PhotoLog.objects.get(pk=instance.pk).pdf
|
||||
except PhotoLog.DoesNotExist:
|
||||
return False
|
||||
|
||||
if not old_file.name:
|
||||
return False
|
||||
|
||||
new_file = instance.pdf
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
||||
os.remove(old_file.path)
|
3
photo_log/tests.py
Executable file
3
photo_log/tests.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
0
scrapers/views.py → photo_log/views.py
Normal file → Executable file
0
scrapers/views.py → photo_log/views.py
Normal file → Executable file
13
req.txt
Normal file
13
req.txt
Normal file
@ -0,0 +1,13 @@
|
||||
imageio==2.13.5
|
||||
imutils==0.5.4
|
||||
networkx==2.6.3
|
||||
numpy==1.22.0
|
||||
opencv-python==4.5.5.62
|
||||
packaging==21.3
|
||||
Pillow==9.0.0
|
||||
pyparsing==3.0.6
|
||||
pytesseract==0.3.8
|
||||
PyWavelets==1.2.0
|
||||
scikit-image==0.19.1
|
||||
scipy==1.7.3
|
||||
tifffile==2021.11.2
|
19
requirements.txt
Executable file
19
requirements.txt
Executable file
@ -0,0 +1,19 @@
|
||||
asgiref==3.4.1
|
||||
beautifulsoup4==4.10.0
|
||||
certifi==2021.10.8
|
||||
charset-normalizer==2.0.7
|
||||
Django==3.2.8
|
||||
djangorestframework==3.13.1
|
||||
gunicorn==20.1.0
|
||||
idna==3.3
|
||||
lxml==4.6.3
|
||||
Pillow==9.0.0
|
||||
psycopg2==2.9.1
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.3
|
||||
requests==2.26.0
|
||||
six==1.16.0
|
||||
soupsieve==2.2.1
|
||||
sqlparse==0.4.2
|
||||
tqdm==4.62.3
|
||||
urllib3==1.26.7
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
Subproject commit 6dc8490bf33486b80604c9907b4c1cda03c94964
|
@ -1,7 +0,0 @@
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', TemplateView.as_view(template_name="scrapers_home.html"), name='scrapers_home'),
|
||||
]
|
@ -1,275 +0,0 @@
|
||||
select.admin-autocomplete {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single,
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||
border-color: var(--body-quiet-color);
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||
color: var(--body-fg);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 10px 5px 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: var(--darkened-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--body-quiet-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||
border: solid var(--body-quiet-color) 1px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
color: var(--body-fg);
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: var(--body-fg);
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||
background-color: var(--selected-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user