Initial commit

This commit is contained in:
2024-10-21 20:32:47 +02:00
commit dae5381b58
34 changed files with 1953 additions and 0 deletions

0
idescriptor/__init__.py Normal file
View File

5
idescriptor/admin.py Normal file
View File

@@ -0,0 +1,5 @@
from django.contrib import admin
from idescriptor.models import Image
admin.site.register(Image)

6
idescriptor/apps.py Normal file
View File

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

23
idescriptor/forms.py Normal file
View File

@@ -0,0 +1,23 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Hidden, HTML
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from idescriptor.models import Image
class ImageForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'form-describe-image'
self.helper.form_class = ''
self.helper.form_method = 'post'
self.helper.add_input(Hidden("action", "add_image_details"))
self.helper.add_input(Submit('submit', _('Submit')))
class Meta:
model = Image
fields = ('title', 'alt_text')

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.2 on 2024-10-21 11:41
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Image',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, help_text='Image title that will be posted', max_length=200, null=True)),
('alt_text', models.CharField(blank=True, help_text='Describe the image', max_length=2000, null=True)),
('file_path', models.CharField(max_length=1500)),
('image', models.ImageField(upload_to='')),
('file_hash', models.CharField(blank=True, max_length=64, null=True, unique=True, verbose_name='File hash')),
('last_posted', models.DateTimeField(blank=True, null=True)),
('number_times_posted', models.IntegerField(default=0)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-10-21 18:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('idescriptor', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='image',
name='alt_text',
field=models.TextField(blank=True, help_text='Describe the image', null=True),
),
]

View File

63
idescriptor/models.py Normal file
View File

@@ -0,0 +1,63 @@
import hashlib
import os
import uuid
from os.path import isfile, join
from django.db import models
from django.urls import reverse
from django.core.files import File
from django.utils.translation import gettext_lazy as _
from datetime import datetime
from imagebot import settings
def get_hash(filepath):
with open(filepath, 'rb') as file:
# read contents of the file
data = file.read()
sha256 = hashlib.sha256(data).hexdigest()
return sha256
class Image(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('ID'))
title = models.CharField(max_length=200,
help_text=_("Image title that will be posted"),
blank=True, null=True)
alt_text = models.TextField(help_text=_("Describe the image"),
blank=True, null=True)
file_path = models.CharField(max_length=1500)
image = models.ImageField()
file_hash = models.CharField(verbose_name=_('File hash'), max_length=64, blank=True, null=True, unique=True)
last_posted = models.DateTimeField(blank=True, null=True)
number_times_posted = models.IntegerField(default=0)
def __str__(self):
if self.title:
return self.title
else:
return f"{self.file_hash}"
def get_absolute_url(self):
reverse("image-update", pk=self.id)
def set_image_posted(self):
self.number_times_posted += 1
self.last_posted = datetime.now()
@staticmethod
def consume():
files = [f for f in settings.MEDIA_CONSUME_DIR.iterdir() if f.is_file()]
for path in files:
file_hash = get_hash(path)
with open(path, 'rb') as f:
django_file = File(f, name=settings.MEDIA_PROCESSED_DIR_RELATIVE / path.name)
img = Image.objects.create(
file_path=str(path),
image=django_file,
file_hash=file_hash
)
os.remove(path)

View File

@@ -0,0 +1,593 @@
/***************/
/* MAIN COLORS */
/***************/
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
--primary-semidark-one: #356c3c;
--primary-dark-one: #17311b;
--secondary-light-one: #faf1cf;
--secondary-light-two: #e1d7b5;
--background-one: var(--primary-light-one);
--background-two: var(--primary-light-two);
--background-three: var(--secondary-light-one);
--background-four: var(--primary-dark-one);
--highlight-one: var(--primary-dark-one);
--highlight-one-text: var(--secondary-light-one);
--highlight-two: var(--primary-semidark-one);
--text-one: var(--secondary-light-one);
--shadow-one: var(--primary-dark-one);
--text-two: var(--primary-dark-one);
--text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one);
}
/**************************/
/* TAG SETTINGS (GENERAL) */
/**************************/
html, body {
margin: 0;
height: 100%;
}
body {
background: var(--background-one);
color: var(--text-two);
}
table {
width: 100%;
}
a {
text-decoration: none;
color: inherit;
}
table {
border: none;
border-collapse: collapse;
background-color: var(--secondary-light-one);
word-break: break-word;
}
@media screen and (max-width: 500px) {
table {
font-size: small;
}
}
td {
border: 2px solid black;
border-collapse: collapse;
padding: 5px;
}
th {
border: 3px solid black;
border-collapse: collapse;
padding: 8px;
background-color: var(--secondary-light-two);
}
h1, h2 {
word-wrap: break-word;
color: var(--text-one);
text-shadow: 2px 2px var(--shadow-one);
}
* {
box-sizing: border-box;
}
textarea {
border-radius: 10px;
width: 100%;
margin: 5px;
}
/**************/
/* CONTAINERS */
/**************/
.container {
display: flex;
flex-wrap: wrap;
}
.container-cards {
display: flex;
flex-wrap: wrap;
}
.card {
flex: 1 25%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
}
.container-edit-buttons {
display: flex;
flex-wrap: wrap;
.btn {
margin: 5px;
}
}
/*******************************/
/* PARTIAL SPECIFIC CONTAINERS */
/*******************************/
/***********/
/* BUTTONS */
/***********/
.btn {
background-color: var(--primary-light-one);
color: var(--secondary-light-one);
padding: 16px;
border-radius: 8px;
border: none;
font-weight: bold;
}
.btn2 {
background-color: var(--secondary-light-one);
color: var(--primary-dark-one);
padding: 8px;
border-radius: 4px;
border: none;
margin: 5px;
}
/*********************/
/* UNIQUE COMPONENTS */
/*********************/
.content-box {
margin: 20px;
}
.header {
overflow: hidden;
background-color: var(--background-two);
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
}
.nav-link {
color: var(--text-one);
text-align: center;
text-decoration: none;
border-radius: 4px;
}
.header a, .header form {
float: left;
padding: 5px 12px 5px 12px;
line-height: 25px;
}
.header a:hover {
background-color: var(--highlight-one);
color: var(--highlight-one-text);
}
.header a.active {
background-color: dodgerblue;
color: white;
}
.header-right select.option {
color: #000;
background-color: var(--highlight-one);
border: 1px;
}
.header-right {
float: right;
display: flex;
border-radius: 0px 0px 15px 15px;
background-color: var(--highlight-two);
color: var(--highlight-one-text);
padding: 5px 5px 0px 5px;
height: 67px;
}
@media screen and (max-width: 500px) {
.header a {
float: none;
display: block;
text-align: left;
}
.header-right {
float: none;
}
}
.logo img {
height: 40px;
}
.form-button, .link-button a:link, .link-button a:visited {
background-color: #4ba3cd;
color: white;
padding: 14px 25px;
text-align: center;
text-decoration: none;
display: block;
margin: 10px;
border-radius: 4px;
border: none;
}
.form-button:hover, .link-button a:hover, .link-button a:active {
background-color: #4090b6;
}
.delete-button, .delete-button a:link, .delete-button a:visited {
border: none;
margin: 10px;
background-color: #a3380a;
border-radius: 4px;
}
.delete-button:hover, .delete-button a:hover, .delete-button a:active {
background-color: #8f2f06;
}
.delete-button {
display: block;
}
.search_result {
margin-top: 10px;
margin-bottom: 10px;
}
.action-menu a {
text-decoration: none;
color: inherit;
}
.action-menu ul {
display: flex;
width: 100%;
height: 80vh;
margin: auto;
max-width: 1000px;
justify-content: space-between;
text-align: center;
}
.action-menu li {
padding: 1rem 2rem 1.15rem;
text-transform: uppercase;
cursor: pointer;
color: #ebebeb;
min-width: 80px;
margin: 10px;
font-weight: bold;
list-style-type: none;
background-color: #4ba3cd;
border-radius: 5px;
}
.action-menu li:hover {
background-color: #4090b6;
animation: spring 300ms ease-out;
}
.action-menu li:active {
transform: translateY(4px);
}
.action-button ul {
display: flex;
width: 100%;
height: 80vh;
margin: auto;
max-width: 1000px;
justify-content: space-between;
text-align: center;
}
.action-button li {
text-decoration: none;
padding: 1rem 2rem 1.15rem;
text-transform: uppercase;
cursor: pointer;
color: #ebebeb;
min-width: 80px;
margin: 10px;
font-weight: bold;
list-style-type: none;
background-color: #4ba3cd;
border-radius: 5px;
}
.action-button li:hover {
background-color: #4090b6;
animation: spring 300ms ease-out;
}
.action-button li:active {
transform: translateY(4px);
}
@keyframes spring {
15% {
-webkit-transform-origin: center center;
-webkit-transform: scale(1.1, 1.05);
}
40% {
-webkit-transform-origin: center center;
-webkit-transform: scale(0.95, 0.95);
}
75% {
-webkit-transform-origin: center center;
-webkit-transform: scale(1.025, 1);
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: scale(1, 1);
}
}
.tag {
border: black 1px solid;
border-radius: 0.3rem;
padding: 2px;
margin: 4px;
}
.sex {
background: #F0DEDE;
}
.species {
background: #F8ECD7;
}
.photos {
display: flex;
flex-wrap: wrap;
}
.card-photo {
flex: 1 16%;
margin: 10px;
}
.card-photo img {
max-width: 100%;
border-radius: 10%;
}
.card h1 {
color: var(--text-three);
text-shadow: 1px 1px var(--shadow-three);
}
.card h2 {
color: var(--text-three);
text-shadow: 1px 1px var(--shadow-three);
}
.card img {
max-width: 100%;
border-radius: 10%;
}
.header-card-adoption-notice {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
}
.table-adoption-notice-info {
margin-top: 10px;
}
@media (max-width: 920px) {
.card {
flex: 1 100%;
}
}
.adoption-notice-img img, img {
max-width: 100%;
border-radius: 10%;
margin: 5px;
max-height: 250px;
}
.img-small img {
height: 100px;
}
.btn-notification {
display: inline-block;
position: relative;
padding: 0;
}
/* Make the badge float in the top right corner of the button */
.button__badge {
background-color: #fa3e3e;
border-radius: 2px;
color: white;
padding: 1px 3px;
font-size: 10px;
position: absolute; /* Position the badge within the relatively positioned button */
top: 0;
right: 0;
}
.adoption-card-report-link, .notification-card-mark-read {
margin-left: auto;
font-size: 2rem;
padding: 10px;
}
.notification-card-mark-read {
display: inline;
}
.heading-card-adoption-notice {
word-wrap: anywhere;
}
.tags {
margin-right: auto;
padding: 5px;
}
#form-adoption-notice, #form-registration {
.form-group {
margin: 30px;
}
}
.detail-adoption-notice-header h1 {
width: 50%;
display: inline-block;
}
.detail-adoption-notice-header a {
display: inline-block;
float: right;
}
@media (max-width: 920px) {
.detail-adoption-notice-header h1 {
width: 100%;
}
.detail-adoption-notice-header a {
display: inline-block;
float: none;
}
}
.container-comment-headers, .container-cards {
display: flex;
flex-wrap: wrap;
background: var(--background-two);
border-radius: 8px;
padding: 5px;
}
.comment, .notification {
flex: 1 100%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
}
.form-comments {
.btn {
margin: 5px;
}
}
.announcement {
flex: 1 100%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
h1 {
font-size: 1.2rem;
margin: 0px;
padding: 0px;
color: var(--text-two);
text-shadow: none;
}
}
.form-search {
select, input {
background-color: var(--primary-light-one);
color: var(--text-one);
border-radius: 3px;
border: none;
}
}
/************************/
/* GENERAL HIGHLIGHTING */
/************************/
.important {
border: #e01137 4px solid;
}
.warning {
border: #e09e11 4px solid;
}
.info {
border: rgba(17, 58, 224, 0.51) 4px solid;
}
/*******/
/* MAP */
/*******/
.marker {
background-image: url('../img/logo_transparent.png');
background-size: cover;
width: 50px;
height: 50px;
cursor: pointer;
}
.maplibregl-popup {
max-width: 600px !important;
}
.maplibregl-popup-content {
background-color: var(--background-three) !important;
border-radius: 8px !important;
}
.map-in-content #map {
height: 500px;
width: 90%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -0,0 +1,32 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block title %}{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% translate 'Describe images' %}">
<!-- Add additional CSS in static file -->
<link rel="stylesheet" href="{% static 'idescriptor/css/styles.css' %}">
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'idescriptor/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'idescriptor/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'idescriptor/favicon/favicon-16x16.png' %}">
</head>
<body>
{% block header %}
{% include "idescriptor/header.html" %}
{% endblock %}
<div class="container-fluid">
<div class="row">
<div class="col-sm-10 content-box">
{% block content %}{% endblock %}
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{% load static %}
{% load i18n %}
<div class="header">
<nav id="nav" class="nav justify-content-center">
<a class="nav-link " href=""><i class="fas fa-search"></i> {% translate 'Suchen' %}</a>
<a class="nav-link " href=""><i
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a>
<a class="nav-link " href="}"><i class="fas fa-info"></i> {% translate 'Über uns' %}</a>
</nav>
</div>

View File

@@ -0,0 +1,16 @@
{% extends "idescriptor/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}<title>{% translate "Organize images for a bot" %}</title>{% endblock %}
{% block content %}
<div class="card">
<h1>{{ image.title }}</h1>
<img src="{{ image.image.url }}" alt="{{ image.alt_text }}">
<div class="container">
{% crispy form %}
<a class="btn" href="{% url 'index' %}">{% trans 'Continue' %}</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "idescriptor/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}<title>{% translate "Organize images for a bot" %}</title>{% endblock %}
{% block content %}
<div class="card">
{% if image_to_describe %}
<img src="{{ image_to_describe.image.url }}" alt="{{ image_to_describe.alt_text }}">
<div class="container-form">
{% crispy form %}
</div>
{% else %}
<p>All done</p>
{% endif %}
</div>
<div class="card">
<h1>{% trans "Controls" %}</h1>
<form class="" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="consume">
<button class="btn" type="submit" id="submit">
<i class="fa-solid fa-broom"></i> {% translate "Consume" %}
</button>
</form>
</div>
{% endblock %}

3
idescriptor/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
idescriptor/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("image/<uuid:pk>/", views.ImageFormView.as_view(), name="image-update"),
]

51
idescriptor/views.py Normal file
View File

@@ -0,0 +1,51 @@
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views.generic import FormView, UpdateView
from django.urls import reverse
from idescriptor.forms import ImageForm
from idescriptor.models import Image
from django.db.models import Q
def get_image_for_descriptor():
image = Image.objects.filter(Q(alt_text=None) | Q(title=None)).first()
return image
class ImageFormView(UpdateView):
model = Image
form_class = ImageForm
template_name = "idescriptor/image-form.html"
success_url = "."
def form_valid(self, form):
return super().form_valid(form)
def index(request):
"""View function for home page of site."""
image_to_describe = get_image_for_descriptor()
if request.method == "POST":
action = request.POST.get("action")
if action == "consume":
Image.consume()
if action == "add_image_details":
form = ImageForm(request.POST, instance=image_to_describe)
if form.is_valid():
instance = form.save()
print("Didi")
return HttpResponseRedirect(reverse("image-update", args=(instance.id,)))
else:
print("Dodo")
else:
form = ImageForm(instance=image_to_describe)
else:
form = ImageForm(instance=image_to_describe)
context = {"image_to_describe": image_to_describe, "form": form}
return render(request, 'idescriptor/index.html', context=context)