framadate celkem fungujici, nesplnuje design zbytku webu...

This commit is contained in:
ticvac 2025-01-26 18:21:30 +01:00
parent 174087edc7
commit 479c9eb192
18 changed files with 706 additions and 0 deletions

0
framadate/__init__.py Normal file
View file

3
framadate/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
framadate/apps.py Normal file
View file

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

View file

@ -0,0 +1,54 @@
# Generated by Django 4.2.16 on 2025-01-26 17:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Framadate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='FramadateDate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('day', models.DateField()),
('time', models.TimeField(blank=True, null=True)),
('framadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='days_set', to='framadate.framadate')),
],
options={
'ordering': ['day', 'time'],
},
),
migrations.CreateModel(
name='NameInFramadate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('framadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='names_set', to='framadate.framadate')),
],
),
migrations.CreateModel(
name='Record',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice', models.IntegerField(choices=[(0, 'Unknown'), (3, 'Yes'), (2, 'No'), (1, 'Maybe')], default=0)),
('framadateDate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records_set', to='framadate.framadatedate')),
('nameInFramadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records_set', to='framadate.nameinframadate')),
],
options={
'ordering': ['framadateDate__day', 'framadateDate__time'],
},
),
]

View file

59
framadate/models.py Normal file
View file

@ -0,0 +1,59 @@
from django.db import models
from datetime import datetime
# Create your models here
class Framadate(models.Model):
name = models.TextField()
"isRelevant if all dates are in the future"
@property
def isRelevant(self):
for day in self.days_set.all():
if day.day < datetime.now().date():
return False
return True
def __str__(self):
return self.name
class NameInFramadate(models.Model):
framadate = models.ForeignKey(Framadate, on_delete=models.CASCADE, related_name="names_set")
name = models.CharField(max_length=200)
def __str__(self):
return self.name + " - in - " + self.framadate.name
class FramadateDate(models.Model):
framadate = models.ForeignKey(Framadate, on_delete=models.CASCADE, related_name="days_set")
day = models.DateField()
time = models.TimeField(null=True, blank=True)
def __str__(self):
return self.day.strftime("%Y-%m-%d") + " - in- " + self.framadate.name
class Meta:
ordering = ['day', 'time']
class Choice(models.IntegerChoices):
UNKNOWN = 0
YES = 3
NO = 2
MAYBE = 1
# Quirk -> name and framadateDate needs to share same Framadate...
class Record(models.Model):
nameInFramadate = models.ForeignKey(NameInFramadate, on_delete=models.CASCADE, related_name="records_set")
framadateDate = models.ForeignKey(FramadateDate, on_delete=models.CASCADE, related_name="records_set")
choice = models.IntegerField(choices=Choice.choices, default=Choice.UNKNOWN)
@property
def choiceName(self):
return Choice(self.choice).name
def __str__(self):
if self.framadateDate.time:
return self.nameInFramadate.name + " - in - " + self.framadateDate.framadate.name + " - " + self.framadateDate.day.strftime("%Y-%m-%d") + " - " + self.framadateDate.time.strftime("%H") + ":00 - " + Choice(self.choice).name
return self.nameInFramadate.name + " - in - " + self.framadateDate.framadate.name + " - " + self.framadateDate.day.strftime("%Y-%m-%d") + " - " + Choice(self.choice).name
class Meta:
ordering = ['framadateDate__day', 'framadateDate__time']

View file

@ -0,0 +1,132 @@
textarea {
font-family: "Tomorrow", serif;
font-weight: 400;
font-style: normal;
font-size: 1.5em;
padding: 0.3em 0.5em;
}
.framadate-detail {
display: flex;
flex-direction: column;
gap: 0.8em;
overflow-x: hidden;
}
.table {
overflow-x: auto;
padding-bottom: 2em;
display: flex;
flex-direction: column;
gap: 0.3em;
.row {
display: flex;
gap: 0.3em;
width: max-content;
.item {
/* background-color: red; */
width: 140px;
text-align: center;
padding: 0.8em 0;
}
}
.YES {
background-color: green;
}
.NO {
background-color: red;
}
.MAYBE {
background-color: goldenrod;
}
.UNKNOWN {
background-color: gray;
}
.radio-column {
display: flex;
flex-direction: column;
align-items: start;
label {
width: 100%;
padding: 0.5em 0;
}
input {
display: none;
}
label:has(input:checked) {
background-color: #2973B2;
color: white;
border-radius: 10px;
}
}
#name {
font-family: "Tomorrow", serif;
font-size: 1em;
}
.submit {
transition: box-shadow 0.2s ease;
background-color: #2973B2;
color: white;
text-decoration: none;
padding: 0.5em 1em;
margin: 1em;
width: min-content;
white-space: nowrap;
border-radius: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: none;
font-size: 1.4em;
font-family: "Tomorrow", serif;
width: 150px;
}
.delete {
background-color: red;
color: black;
}
.submit:hover {
box-shadow: none;
}
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.mini-row .item {
font-size: 0.8em;
}
.item-with-edit {
display: flex;
align-items: center;
justify-content: center;
gap: 1em;
a {
color: #2973B2;
}
}
.delete-framadate {
font-size: 0.5em;
margin-left: 2em;
color: red;
}

View file

@ -0,0 +1,82 @@
.label-name {
display: flex;
flex-direction: row;
gap: 2em;
align-items: center;
font-family: "Tomorrow", serif;
input {
font-family: "Tomorrow", serif;
font-size: 1em;
padding: 0.5em;
border-radius: 10px;
text-align: center;
border: 1px solid #606060;
}
}
.dates {
border: 1px solid #ccc;
padding: 1em;
margin: 1em 0;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 0.5em;
.date {
display: flex;
flex-direction: row;
gap: 2em;
align-items: center;
}
}
input[type="date"], input[type="time"] {
font-family: "Tomorrow", serif;
font-size: 1em;
padding: 0.3em 0.7em;
border: 1px solid #606060;
border-radius: 10px;
margin-left: 0.5em;
}
.controls {
margin-bottom: 2em;
display: flex;
flex-direction: column;
gap: 0.5em;
align-items: start;
}
.add-button {
border: none;
background-color: #2973B2;
color: white;
padding: 0.4em 1em;
font-size: 1em;
font-family: "Tomorrow", serif;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
transition: box-shadow 0.2s ease;
margin: 0.5em;
}
.add-button:hover {
box-shadow: none;
}
.span-controls {
display: flex;
gap: 2em;
label {
display: flex;
align-items: center;
gap: 1em;
}
}
.create {
background-color: green;
}

View file

@ -0,0 +1,47 @@
body {
font-family: "Tomorrow", serif;
font-weight: 400;
font-style: normal;
margin: 1em;
padding: 1em;
border: 1px solid black;
border-radius: 20px;
font-size: 1.2em;
.framadates {
display: flex;
flex-direction: column;
gap: 0.8em;
a {
color: black;
}
}
h1 {
margin: 0.3em 0.5em;
}
.column {
display: flex;
flex-direction: column;
justify-content: start;
}
.new {
transition: box-shadow 0.2s ease;
background-color: #2973B2;
color: white;
text-decoration: none;
padding: 0.5em 1em;
margin: 1em 0;
width: min-content;
white-space: nowrap;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.new:hover {
box-shadow: none;
}
}

View file

@ -0,0 +1,98 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load static %}
<link rel="stylesheet" href="{% static 'framadate/style.css' %}">
<link rel="stylesheet" href="{% static 'framadate/framadate_detail.css' %}">
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<!-- icons -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=edit" />
<div class="framadate-detail">
<a href="/framadate" class="back">Back</a>
<h1>{{ framadate.name }} <a href="/framadate/{{ framadate.id }}/delete" class="delete-framadate">Delete</a></h1>
<form class="table" method="post" action="/framadate/{{ framadate.id }}/save-all">
{% csrf_token %}
<!-- hint -->
<div class="row">
<div class="item"></div>
{% for mydate in framadate.days_set.all %}
<div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div>
{% endfor %}
</div>
<!-- other users -->
{% for name in framadate.names_set.all %}
<div class="row">
<div class="item item-with-edit">
{{ name.name }}
<a href="/framadate/{{ framadate.id }}/edit/{{ name.id }}"><span class="material-symbols-rounded">edit</span></a>
</div>
{% for record in name.records_set.all %}
<div class="item {{ record.choiceName }}">{{ record.choiceName }}</div>
{% endfor %}
</div>
{% endfor %}
<!-- hint -->
<div class="row mini-row">
<div class="item"></div>
{% for mydate in framadate.days_set.all %}
<div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div>
{% endfor %}
</div>
<!-- inputs -->
{% if edditing %}
<div class="row">
<div class="item center"><input type="text" class="item" name="name" id="name" value="{{ name.name }}"></div>
{% for record in name.records_set.all %}
<div class="item radio-column">
<label><input type="radio" name="choice_{{ record.id }}" value="3" {% if record.choice == 3 %} checked {%endif%}>YES</label>
<label><input type="radio" name="choice_{{ record.id }}" value="2" {% if record.choice == 2 %} checked {%endif%}>NO</label>
<label><input type="radio" name="choice_{{ record.id }}" value="1" {% if record.choice == 1 %} checked {%endif%}>MAYBE</label>
</div>
{% endfor %}
</div>
{% else %}
<div class="row">
<div class="item center"><input type="text" class="item" name="name" id="name"></div>
{% for mydate in framadate.days_set.all %}
<div class="item radio-column">
<label><input type="radio" name="choice_{{ mydate.id }}" value="3">YES</label>
<label><input type="radio" name="choice_{{ mydate.id }}" value="2">NO</label>
<label><input type="radio" name="choice_{{ mydate.id }}" value="1">MAYBE</label>
</div>
{% endfor %}
</div>
{% endif %}
<!-- save handeling -->
<div class="row">
<input type="submit" class="submit" value="Save">
{% if edditing %}
<input name="delete" type="submit" class="submit delete" value="Delete" onclick="return confirmSubmit()">
<input type="hidden" name="delete_id" value="{{ name.id }}">
{% endif %}
</div>
<!-- best dates -->
<h3>Best days - {{ max_count }}</h3>
<div class="row">
{% for mydate in best_dates %}
<div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div>
{% endfor %}
</div>
</form>
</div>
<script>
function confirmSubmit() {
return confirm("Are you sure you want delete the records?");
}
// prevent .delete-framadate click event
document.querySelector(".delete-framadate").addEventListener("click", function(event) {
if (!confirm("Are you sure you want delete the framadate?")) {
event.preventDefault();
}
});
</script>

View file

@ -0,0 +1,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load static %}
<link rel="stylesheet" href="{% static 'framadate/style.css' %}">
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<div class="column">
<h1>All current framadates</h1>
<div class="framadates">
{% for framadate in object_list %}
{% if framadate.isRelevant %}
<a href="framadate/{{ framadate.id }}">{{ framadate.name }}</a>
{% endif %}
{% endfor %}
</div>
<a href="/framadate/new" class="new">Create new</a>
</div>
<div>
<h1>Past framadates</h1>
<div class="framadates">
{% for framadate in object_list %}
{% if not framadate.isRelevant %}
<a href="framadate/{{ framadate.id }}">{{ framadate.name }}</a>
{% endif %}
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,82 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load static %}
<link rel="stylesheet" href="{% static 'framadate/style.css' %}">
<link rel="stylesheet" href="{% static 'framadate/new_framadate.css' %}">
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<a href="/framadate" class="back">Back</a>
<h1>New framadate</h1>
<form action="/framadate/new" method="post">
{% csrf_token %}
<label class="label-name">name: <input type="text" required name="name"></label>
<div class="dates">
<!-- <div id="date_1" class="date">
<label>date: <input type="date" name="" id="" required></label>
<label>time (optional, if empty -> entire day ): <input type="time" name="" id="" step="3600000"></label>
<button type="button" onclick="removeDate(1)">remove</button>
</div> -->
</div>
<!-- controls -->
<div class="controls">
<button type="button" class="add-button" onclick="addDate()">Add date</button>
<div class="span-controls">
<label>from: <input id="from" type="date"></label>
<label>to: <input id="to" type="date"></label>
<button type="button" onclick="addSpan()" class="add-button">Add span</button>
</div>
</div>
<input type="submit" value="Create" class="add-button create">
</form>
<script>
numberOfDates = 1;
function addDate() {
let newDate = document.createElement("div");
newDate.classList.add("date");
numberOfDates++;
newDate.id = "date_" + numberOfDates;
// step does not work in safari...
newDate.innerHTML = `
<label>date: <input type="date" name="date" required></label>
<label>time (optional, if empty -> entire day ): <input type="time" name="time" step="3600"></label>
<button type="button" onclick="removeDate(`+numberOfDates+`)">remove</button>
`;
dates = document.querySelector(".dates");
dates.appendChild(newDate);
}
function removeDate(id_to_remove) {
document.getElementById("date_" + id_to_remove).remove();
}
let from = document.getElementById("from");
let to = document.getElementById("to");
function addSpan() {
let fromDate = new Date(from.value);
let toDate = new Date(to.value);
// print date of every date in range
while (fromDate <= toDate) {
let newDate = document.createElement("div");
newDate.classList.add("date");
numberOfDates++;
newDate.id = "date_" + numberOfDates;
newDate.innerHTML = `
<label>date: <input type="date" name="date" required value="`+fromDate.toISOString().split("T")[0]+`"></label>
<label>time (optional, if empty -> entire day ): <input type="time" name="time" step="3600"></label>
<button type="button" onclick="removeDate(`+numberOfDates+`)">remove</button>
`;
dates = document.querySelector(".dates");
dates.appendChild(newDate);
fromDate.setDate(fromDate.getDate() + 1);
}
}
</script>

3
framadate/tests.py Normal file
View file

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

11
framadate/urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path("framadate", views.FramadateListView.as_view(), name="framadate_list"),
path("framadate/new", views.NewFramadate.as_view(), name="new_framadate"),
path("framadate/<int:pk>", views.FramadateDetail.as_view(), name="framadate_detail"),
path("framadate/<int:pk>/edit/<int:name_id>", views.FramadateDetailEdit.as_view(), name="framadate_detail_edit"),
path("framadate/<int:pk>/save-all", views.save_all, name="save_all"),
path("framadate/<int:pk>/delete", views.delete_framadate, name="delete"),
]

93
framadate/views.py Normal file
View file

@ -0,0 +1,93 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.views.generic.list import ListView
from django.views.generic import TemplateView
from .models import *
class FramadateListView(ListView):
model = Framadate
template_name = 'framadate/framadate_list.html'
class NewFramadate(TemplateView):
template_name = 'framadate/new_framadate.html'
def post(self, request, *args, **kwargs):
new_framadate = Framadate.objects.create(name=request.POST['name'])
dates = request.POST.getlist('date')
times = request.POST.getlist('time')
for i in range(len(dates)):
FramadateDate.objects.create(framadate=new_framadate, day=dates[i], time=(times[i] if times[i] else None))
return HttpResponseRedirect("/framadate/" + str(new_framadate.id))
class FramadateDetail(TemplateView):
template_name = 'framadate/framadate_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
addContextForFramadate(context, **kwargs)
return context
class FramadateDetailEdit(TemplateView):
template_name = 'framadate/framadate_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
addContextForFramadate(context, **kwargs)
# data to edit
context["edditing"] = True
context["name"] = NameInFramadate.objects.get(pk=kwargs['name_id'])
return context
def save_all(request, pk):
if request.method != "POST":
return HttpResponse("Not a POST request")
if request.POST.get("name") == "":
return HttpResponseRedirect(reverse('framadate_detail', args=[pk]))
framadate = Framadate.objects.get(pk=pk)
# deleting
if request.POST.get("delete"):
try:
NameInFramadate.objects.get(pk=request.POST['delete_id']).delete()
except:
return HttpResponse("Ups something went wrong, can't delete")
return HttpResponseRedirect(reverse('framadate_detail', args=[pk]))
# new
if not request.POST.get("delete_id"):
new_name = NameInFramadate.objects.create(framadate=framadate, name=request.POST['name'])
for day in framadate.days_set.all():
Record.objects.create(nameInFramadate=new_name, framadateDate=day, choice=(request.POST["choice_" + str(day.id)] if request.POST.get("choice_" + str(day.id)) else 0))
else: # edditing
name_id = request.POST['delete_id']
name = NameInFramadate.objects.get(pk=name_id)
name.name = request.POST['name']
name.save()
for record in name.records_set.all():
record.choice = (request.POST["choice_" + str(record.id)] if request.POST.get("choice_" + str(record.id)) else 0)
record.save()
return HttpResponseRedirect(reverse('framadate_detail', args=[pk]))
def delete_framadate(request, pk):
framadate = Framadate.objects.get(pk=pk)
framadate.delete()
return HttpResponseRedirect("/framadate")
def addContextForFramadate(context, **kwargs):
context['framadate'] = Framadate.objects.get(pk=kwargs['pk'])
context['choices'] = Choice.choices
context['best_dates'] = []
best_dates = []
best = -1
for day in context['framadate'].days_set.all(): # kinda slow?
count = 0
for record in day.records_set.all():
if record.choice == 3:
count += 1
if count > best:
best_dates = [day]
best = count
elif count == best:
best_dates.append(day)
context['best_dates'] = best_dates
context['max_count'] = best

View file

@ -148,6 +148,7 @@ INSTALLED_APPS = (
'vyroci',
'sifrovacka',
'novinky',
'framadate',
# Admin upravy:

View file

@ -62,6 +62,9 @@ urlpatterns = [
# Miniapka na šifrovačku
path('sifrovacka/', include('sifrovacka.urls')),
# Framadate
path('', include('framadate.urls')),
]
# This is only needed when using runserver.

View file

@ -13,3 +13,4 @@ from soustredeni.models import *
from treenode.models import *
from tvorba.models import *
from various.models import *
from framadate.models import *