add all updates

This commit is contained in:
Marc Zierle 2022-05-20 19:14:48 +02:00
parent 3e368b6e3f
commit e91e83df40
229 changed files with 1037 additions and 30208 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
static/*
./*/*pycache*/*

0
.gitmodules vendored Normal file → Executable file
View File

0
accounts/__init__.py Normal file → Executable file
View File

0
accounts/__pycache__/__init__.cpython-39.pyc Normal file → Executable file
View File

0
accounts/__pycache__/admin.cpython-39.pyc Normal file → Executable file
View File

0
accounts/__pycache__/apps.cpython-39.pyc Normal file → Executable file
View File

0
accounts/__pycache__/managers.cpython-39.pyc Normal file → Executable file
View File

0
accounts/__pycache__/models.cpython-39.pyc Normal file → Executable file
View File

0
accounts/admin.py Normal file → Executable file
View File

0
accounts/apps.py Normal file → Executable file
View File

0
accounts/managers.py Normal file → Executable file
View File

0
accounts/migrations/0001_initial.py Normal file → Executable file
View File

0
accounts/migrations/__init__.py Normal file → Executable file
View File

View File

View File

0
accounts/models.py Normal file → Executable file
View File

0
accounts/tests.py Normal file → Executable file
View File

0
accounts/views.py Normal file → Executable file
View File

0
scrapers/__init__.py → api/__init__.py Normal file → Executable file
View 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.

0
scrapers/admin.py → api/admin.py Normal file → Executable file
View File

4
scrapers/apps.py → api/apps.py Normal file → Executable file
View 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

@ -0,0 +1 @@
Subproject commit 70828ba4e14e67a1db819de5c6371713145f868c

View File

Binary file not shown.

0
scrapers/models.py → api/models.py Normal file → Executable file
View File

63
api/photolog_generator.py Normal file
View 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
View 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
View 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
View File

36
api/urls.py Executable file
View 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
View 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
View File

0
config/__pycache__/__init__.cpython-39.pyc Normal file → Executable file
View File

0
config/__pycache__/wsgi.cpython-39.pyc Normal file → Executable file
View File

7
config/asgi.py Normal file → Executable file
View 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
View File

@ -0,0 +1,6 @@
{
"name": "config",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

34
config/settings.py Normal file → Executable file
View 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
View 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
View File

0
photo_log/__init__.py Executable file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

8
photo_log/admin.py Executable file
View 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
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PhotoLogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'photo_log'

View 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')),
],
),
]

View 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/'),
),
]

View 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),
),
]

View 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'),
),
]

View 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)),
],
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View 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),
),
]

View File

Binary file not shown.

Binary file not shown.

176
photo_log/models.py Executable file
View 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
View 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
View File

13
req.txt Normal file
View 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
View 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

@ -1 +0,0 @@
Subproject commit 6dc8490bf33486b80604c9907b4c1cda03c94964

View File

@ -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'),
]

View File

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