Compare commits

..

27 commits

Author SHA1 Message Date
683796ea7e Merge branch 'refs/heads/prednasky' 2025-02-06 16:57:15 +01:00
4a771b802b Průhledné pruhy 2025-02-06 14:48:32 +01:00
84ed9e09a7 Průhledné pruhy 2025-02-06 13:50:01 +01:00
9460c484f7 Zobrazení Znalostí (stejně jako Přednášek) u daného seznamu 2025-02-04 23:09:47 +01:00
2767e82f11 Merge branch 'master' into prednasky 2025-02-04 22:49:06 +01:00
42c651ceb7 Zlepšení dokumentace Djanga ve Sphinxu 2025-02-04 22:47:18 +01:00
5d4b600b00 Otočení významu odpovědí na hlasování o znalostech + WTF proč to byl string 2025-02-04 21:21:15 +01:00
c1da67dbb4 Dobře, příště už při dokumentaci nebudu hrabat na typové anotace. 2025-02-04 20:33:03 +01:00
5125525238 Dokumentace aplikace prednasky 2025-01-29 01:06:00 +01:00
34f0dffd79 Merge branch 'master' into prednasky 2025-01-29 00:57:24 +01:00
9e513bba9a Kopírování je častým zdrojem chyb 2025-01-29 00:30:55 +01:00
ef9d51d922 hotfix: WTF se stalo v django-autocomplete-light=3.12.0 (%20 místo mezer apod.) 2025-01-28 19:03:51 +01:00
ca5e6728dd hotfix: Tohle by mělo opravit problém s ukládáním bodů. Nejsem si tím ale moc jistý. 2025-01-28 18:48:57 +01:00
7563dd728c Fix make/deploy 2025-01-24 22:51:09 +01:00
cb14e4a91e A očividně nevygeneroval migraci k přepsání stringů u Hlasovani.Body v commitu e933c697 2025-01-24 21:07:06 +01:00
1719e8be9a Zapomněl jsem přidat CSS místo smazaného <i> 2025-01-24 21:03:44 +01:00
0634cdad87 Očividně každý systém žere uvozovky v f-stringu jinak 2025-01-24 20:42:44 +01:00
90e7b97b85 Zakomentován starý export 2025-01-24 20:35:24 +01:00
4001822842 Oprava práv pro aplikaci přednášky 2025-01-24 20:32:28 +01:00
7ca7093371 Export hlasování do CSV 2025-01-24 20:22:38 +01:00
fbd75d2f72 Hlasování o přednáškách pomocí formsetů… 2025-01-24 19:40:54 +01:00
e12b614e1c ODPOVED -> Odpoved 2025-01-24 16:01:39 +01:00
bcda95f0b3 Stringifikace hlasování o znalostech 2025-01-24 15:51:06 +01:00
6c35a5b6f3 Uhlazení prednasky.models 2025-01-24 15:49:55 +01:00
e933c6978d Choices na Enum (u přednášek) 2025-01-24 15:36:33 +01:00
2f814956a7 Nepoužívaný kus kódu 2025-01-24 15:20:00 +01:00
f61533df0a Přidání Znalosti do modelu 2025-01-24 15:13:37 +01:00
37 changed files with 611 additions and 933 deletions

View file

@ -14,12 +14,12 @@
"flatpage" "flatpage"
], ],
[ [
"delete_flatpage", "change_flatpage",
"flatpages", "flatpages",
"flatpage" "flatpage"
], ],
[ [
"change_flatpage", "delete_flatpage",
"flatpages", "flatpages",
"flatpage" "flatpage"
], ],
@ -34,12 +34,12 @@
"galerie" "galerie"
], ],
[ [
"delete_galerie", "change_galerie",
"galerie", "galerie",
"galerie" "galerie"
], ],
[ [
"change_galerie", "delete_galerie",
"galerie", "galerie",
"galerie" "galerie"
], ],
@ -54,12 +54,12 @@
"obrazek" "obrazek"
], ],
[ [
"delete_obrazek", "change_obrazek",
"galerie", "galerie",
"obrazek" "obrazek"
], ],
[ [
"change_obrazek", "delete_obrazek",
"galerie", "galerie",
"obrazek" "obrazek"
], ],
@ -104,12 +104,12 @@
"komentar" "komentar"
], ],
[ [
"delete_komentar", "change_komentar",
"korektury", "korektury",
"komentar" "komentar"
], ],
[ [
"change_komentar", "delete_komentar",
"korektury", "korektury",
"komentar" "komentar"
], ],
@ -124,12 +124,12 @@
"korekturovanepdf" "korekturovanepdf"
], ],
[ [
"delete_korekturovanepdf", "change_korekturovanepdf",
"korektury", "korektury",
"korekturovanepdf" "korekturovanepdf"
], ],
[ [
"change_korekturovanepdf", "delete_korekturovanepdf",
"korektury", "korektury",
"korekturovanepdf" "korekturovanepdf"
], ],
@ -144,12 +144,12 @@
"oprava" "oprava"
], ],
[ [
"delete_oprava", "change_oprava",
"korektury", "korektury",
"oprava" "oprava"
], ],
[ [
"change_oprava", "delete_oprava",
"korektury", "korektury",
"oprava" "oprava"
], ],
@ -164,12 +164,12 @@
"novinky" "novinky"
], ],
[ [
"delete_novinky", "change_novinky",
"novinky", "novinky",
"novinky" "novinky"
], ],
[ [
"change_novinky", "delete_novinky",
"novinky", "novinky",
"novinky" "novinky"
], ],
@ -204,12 +204,12 @@
"prijemce" "prijemce"
], ],
[ [
"delete_prijemce", "change_prijemce",
"personalni", "personalni",
"prijemce" "prijemce"
], ],
[ [
"change_prijemce", "delete_prijemce",
"personalni", "personalni",
"prijemce" "prijemce"
], ],
@ -234,12 +234,12 @@
"skola" "skola"
], ],
[ [
"delete_skola", "change_skola",
"personalni", "personalni",
"skola" "skola"
], ],
[ [
"change_skola", "delete_skola",
"personalni", "personalni",
"skola" "skola"
], ],
@ -248,38 +248,28 @@
"personalni", "personalni",
"skola" "skola"
], ],
[
"add_hlasovani",
"prednasky",
"hlasovani"
],
[
"delete_hlasovani",
"prednasky",
"hlasovani"
],
[
"change_hlasovani",
"prednasky",
"hlasovani"
],
[ [
"view_hlasovani", "view_hlasovani",
"prednasky", "prednasky",
"hlasovani" "hlasovani"
], ],
[
"view_hlasovanioznalostech",
"prednasky",
"hlasovanioznalostech"
],
[ [
"add_prednaska", "add_prednaska",
"prednasky", "prednasky",
"prednaska" "prednaska"
], ],
[ [
"delete_prednaska", "change_prednaska",
"prednasky", "prednasky",
"prednaska" "prednaska"
], ],
[ [
"change_prednaska", "delete_prednaska",
"prednasky", "prednasky",
"prednaska" "prednaska"
], ],
@ -294,12 +284,12 @@
"seznam" "seznam"
], ],
[ [
"delete_seznam", "change_seznam",
"prednasky", "prednasky",
"seznam" "seznam"
], ],
[ [
"change_seznam", "delete_seznam",
"prednasky", "prednasky",
"seznam" "seznam"
], ],
@ -308,18 +298,38 @@
"prednasky", "prednasky",
"seznam" "seznam"
], ],
[
"add_znalost",
"prednasky",
"znalost"
],
[
"change_znalost",
"prednasky",
"znalost"
],
[
"delete_znalost",
"prednasky",
"znalost"
],
[
"view_znalost",
"prednasky",
"znalost"
],
[ [
"add_konfera", "add_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
[ [
"delete_konfera", "change_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
[ [
"change_konfera", "delete_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
@ -334,12 +344,12 @@
"konfery_ucastnici" "konfery_ucastnici"
], ],
[ [
"delete_konfery_ucastnici", "change_konfery_ucastnici",
"soustredeni", "soustredeni",
"konfery_ucastnici" "konfery_ucastnici"
], ],
[ [
"change_konfery_ucastnici", "delete_konfery_ucastnici",
"soustredeni", "soustredeni",
"konfery_ucastnici" "konfery_ucastnici"
], ],
@ -354,12 +364,12 @@
"soustredeni" "soustredeni"
], ],
[ [
"delete_soustredeni", "change_soustredeni",
"soustredeni", "soustredeni",
"soustredeni" "soustredeni"
], ],
[ [
"change_soustredeni", "delete_soustredeni",
"soustredeni", "soustredeni",
"soustredeni" "soustredeni"
], ],
@ -374,12 +384,12 @@
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
[ [
"delete_soustredeni_organizatori", "change_soustredeni_organizatori",
"soustredeni", "soustredeni",
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
[ [
"change_soustredeni_organizatori", "delete_soustredeni_organizatori",
"soustredeni", "soustredeni",
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
@ -394,12 +404,12 @@
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
[ [
"delete_soustredeni_ucastnici", "change_soustredeni_ucastnici",
"soustredeni", "soustredeni",
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
[ [
"change_soustredeni_ucastnici", "delete_soustredeni_ucastnici",
"soustredeni", "soustredeni",
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
@ -414,12 +424,12 @@
"tag" "tag"
], ],
[ [
"delete_tag", "change_tag",
"taggit", "taggit",
"tag" "tag"
], ],
[ [
"change_tag", "delete_tag",
"taggit", "taggit",
"tag" "tag"
], ],
@ -434,12 +444,12 @@
"taggeditem" "taggeditem"
], ],
[ [
"delete_taggeditem", "change_taggeditem",
"taggit", "taggit",
"taggeditem" "taggeditem"
], ],
[ [
"change_taggeditem", "delete_taggeditem",
"taggit", "taggit",
"taggeditem" "taggeditem"
], ],
@ -454,12 +464,12 @@
"cislo" "cislo"
], ],
[ [
"delete_cislo", "change_cislo",
"tvorba", "tvorba",
"cislo" "cislo"
], ],
[ [
"change_cislo", "delete_cislo",
"tvorba", "tvorba",
"cislo" "cislo"
], ],
@ -474,12 +484,12 @@
"clanek" "clanek"
], ],
[ [
"delete_clanek", "change_clanek",
"tvorba", "tvorba",
"clanek" "clanek"
], ],
[ [
"change_clanek", "delete_clanek",
"tvorba", "tvorba",
"clanek" "clanek"
], ],
@ -509,12 +519,12 @@
"pohadka" "pohadka"
], ],
[ [
"delete_pohadka", "change_pohadka",
"tvorba", "tvorba",
"pohadka" "pohadka"
], ],
[ [
"change_pohadka", "delete_pohadka",
"tvorba", "tvorba",
"pohadka" "pohadka"
], ],
@ -529,12 +539,12 @@
"problem" "problem"
], ],
[ [
"delete_problem", "change_problem",
"tvorba", "tvorba",
"problem" "problem"
], ],
[ [
"change_problem", "delete_problem",
"tvorba", "tvorba",
"problem" "problem"
], ],
@ -549,12 +559,12 @@
"rocnik" "rocnik"
], ],
[ [
"delete_rocnik", "change_rocnik",
"tvorba", "tvorba",
"rocnik" "rocnik"
], ],
[ [
"change_rocnik", "delete_rocnik",
"tvorba", "tvorba",
"rocnik" "rocnik"
], ],
@ -569,12 +579,12 @@
"tema" "tema"
], ],
[ [
"delete_tema", "change_tema",
"tvorba", "tvorba",
"tema" "tema"
], ],
[ [
"change_tema", "delete_tema",
"tvorba", "tvorba",
"tema" "tema"
], ],
@ -589,12 +599,12 @@
"uloha" "uloha"
], ],
[ [
"delete_uloha", "change_uloha",
"tvorba", "tvorba",
"uloha" "uloha"
], ],
[ [
"change_uloha", "delete_uloha",
"tvorba", "tvorba",
"uloha" "uloha"
], ],
@ -609,12 +619,12 @@
"nastaveni" "nastaveni"
], ],
[ [
"delete_nastaveni", "change_nastaveni",
"various", "various",
"nastaveni" "nastaveni"
], ],
[ [
"change_nastaveni", "delete_nastaveni",
"various", "various",
"nastaveni" "nastaveni"
], ],

View file

@ -36,6 +36,7 @@ extensions = [
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.autosectionlabel', 'sphinx.ext.autosectionlabel',
'myst_parser', 'myst_parser',
'sphinxcontrib_django',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.

View file

View file

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

View file

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

View file

@ -1,54 +0,0 @@
# 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

@ -1,59 +0,0 @@
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

@ -1,132 +0,0 @@
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

@ -1,82 +0,0 @@
.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

@ -1,47 +0,0 @@
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

@ -1,98 +0,0 @@
<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

@ -1,31 +0,0 @@
<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

@ -1,82 +0,0 @@
<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>

View file

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

View file

@ -1,11 +0,0 @@
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"),
]

View file

@ -1,93 +0,0 @@
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

@ -95,7 +95,7 @@ function safe_checkout_branch {
echo >&2 "Změna v $SCRIPT, prosím pullni manuálně" echo >&2 "Změna v $SCRIPT, prosím pullni manuálně"
exit 1 exit 1
fi fi
git checkout "$BRANCH" git checkout "$BRANCH" --
git pull git pull
git clean -f git clean -f
} }

View file

@ -57,6 +57,7 @@ DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error' CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error'
# Modules configuration # Modules configuration
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
@ -148,7 +149,6 @@ INSTALLED_APPS = (
'vyroci', 'vyroci',
'sifrovacka', 'sifrovacka',
'novinky', 'novinky',
'framadate',
# Admin upravy: # Admin upravy:

View file

@ -435,6 +435,7 @@ body.localweb, body.testweb, body.suprodweb {
height: 100%; height: 100%;
top: 0; top: 0;
z-index: -1000; z-index: -1000;
opacity: 0.7;
} }
&:before { left: 0; } &:before { left: 0; }

View file

@ -503,5 +503,10 @@ label[for=id_skola] {
font-weight: bold; font-weight: bold;
} }
/* Přednášky */
.textznalosti, .textprednasky {
font-style: italic;
}
/*******************/ /*******************/

View file

@ -0,0 +1,20 @@
/**** ROZLIŠENÍ MEZI LOKÁLNÍM, TESTOVACÍM A PRODUKČNÍM WEBEM ****/
body.localweb, body.testweb, body.suprodweb {
&:before, &:after {
content: "";
position: fixed;
width: 20px;
height: 100%;
top: 0;
z-index: -1000;
opacity: 0.7;
}
&:before { left: 0; }
&:after { right: 0; }
}
body.localweb { &:before, &:after { background: greenyellow; } }
body.testweb { &:before, &:after { background: darkorange; } }
body.suprodweb { &:before, &:after { background: red; } }
/****************************************************************/

View file

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

View file

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

View file

@ -322,14 +322,26 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
**form.cleaned_data, **form.cleaned_data,
) )
logger.info(f"Creating Hodnoceni: {hodnoceni}") logger.info(f"Creating Hodnoceni: {hodnoceni}")
# FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak
zmeny_bodu = [it for it in form.changed_data if it.startswith("body")] zmeny_bodu = [it for it in form.changed_data if it.startswith("body")]
if len(zmeny_bodu) == 1: if len(zmeny_bodu) != 0:
hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]]) body_nastaveny: None | tuple[str, object] = None
# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno def nastav_body(jake, na_kolik):
if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2: nonlocal body_nastaveny
# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo if body_nastaveny is not None:
logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.") logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.")
hodnoceni.body = -0.1 hodnoceni.body = -0.1
else:
body_nastaveny = (jake, na_kolik)
hodnoceni.__setattr__(jake, na_kolik)
for key, value in data_for_body.items():
if key.startswith("body") and value is not None:
nastav_body(key, value)
# Něco se změnilo, ale nic není nastavené = něco bylo smazáno
if body_nastaveny is None:
hodnoceni.body = None
hodnoceni.save() hodnoceni.save()
adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True) adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True)

View file

@ -0,0 +1,3 @@
"""
Aplikace umožňující orgům vypisovat si přednášky a účastníkům o nich hlasovat.
"""

View file

@ -4,11 +4,15 @@ from reversion.admin import VersionAdmin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.html import escape from django.utils.html import escape
from .models import Prednaska, Seznam, STAV_NAVRH from .models import Prednaska, Seznam, Znalost
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
class Seznam_PrednaskaInline(admin.TabularInline): class Seznam_PrednaskaInline(admin.TabularInline):
"""
Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Přednášky <prednasky.models.Prednaska>`
v adminu :py:class:`Seznamu <prednasky.models.Seznam>`.
"""
model = Prednaska.seznamy.through model = Prednaska.seznamy.through
extra = 0 extra = 0
@ -54,24 +58,57 @@ class Seznam_PrednaskaInline(admin.TabularInline):
def has_add_permission(self, req, obj): return False def has_add_permission(self, req, obj): return False
class Seznam_ZnalostInline(admin.TabularInline):
"""
Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Znalosti <prednasky.models.Znalost>`
v adminu :py:class:`Seznamu <prednasky.models.Seznam>`.
"""
model = Znalost.seznamy.through
extra = 0
def znalost__nazev(self, obj):
return mark_safe(
f"<a href='/admin/prednasky/znalost/{obj.znalost.id}'>{obj.znalost.nazev}</a>"
)
def znalost__text(self, obj):
return mark_safe(
f"<div style='width: 200px'>{escape(obj.znalost.text)}</div>"
)
znalost__nazev.short_description = u'Přednáška'
znalost__text.short_description = u'Popis pro orgy'
readonly_fields = [
'znalost__nazev',
'znalost__text',
]
exclude = ['znalost']
def has_add_permission(self, req, obj): return False
class SeznamAdmin(VersionAdmin): class SeznamAdmin(VersionAdmin):
""" Admin pro :py:class:`Seznam <prednasky.models.Seznam>` """
list_display = ['soustredeni', 'stav'] list_display = ['soustredeni', 'stav']
inlines = [Seznam_PrednaskaInline] inlines = [Seznam_PrednaskaInline, Seznam_ZnalostInline]
admin.site.register(Seznam, SeznamAdmin) admin.site.register(Seznam, SeznamAdmin)
class PrednaskaAdmin(VersionAdmin): class PrednaskaAdmin(VersionAdmin):
""" Admin pro :py:class:`Přednášku <prednasky.models.Prednaska> """
list_display = ['nazev', 'org', 'obor'] list_display = ['nazev', 'org', 'obor']
list_filter = ['org', 'obor'] list_filter = ['org', 'obor']
search_fields = [] search_fields = ['nazev']
filter_horizontal = ('seznamy', ) filter_horizontal = ('seznamy', )
actions = ['move_to_soustredeni'] actions = ['move_to_soustredeni']
def move_to_soustredeni(self, request, queryset): def move_to_soustredeni(self, request, queryset):
""" Přidá dané přednášky do seznamu, o kterém se právě hlasuje """
sous = Soustredeni.objects.first() sous = Soustredeni.objects.first()
seznam = Seznam.objects.filter(soustredeni=sous, stav=STAV_NAVRH) seznam = Seznam.objects.filter(soustredeni=sous, stav=Seznam.Stav.NAVRH)
if len(seznam) == 0: if len(seznam) == 0:
self.message_user( self.message_user(
request, request,
@ -97,3 +134,14 @@ class PrednaskaAdmin(VersionAdmin):
admin.site.register(Prednaska, PrednaskaAdmin) admin.site.register(Prednaska, PrednaskaAdmin)
class ZnalostAdmin(PrednaskaAdmin): # Trochu hack, ať nemusím vypisovat všechno znovu
"""
Admin pro :py:class:`Znalost <prednasky.models.Znalost>
TODO předělat, aby nedědila z :py:class:`prednasky.admin.PrednaskaAdmin`, ale společné věci byly zvlášť
"""
list_display = ("__str__",)
list_filter = ()
admin.site.register(Znalost, ZnalostAdmin)

View file

@ -1,7 +1,31 @@
from django import forms from django import forms
class NewPrednaskyForm(forms.Form): from .models import Hlasovani, HlasovaniOZnalostech
ucastnik = forms.CharField(label = 'Tvoje jméno', max_length = 100)
class HlasovaniPrednaskaForm(forms.Form):
""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o jedné :py:class:`Přednášce <prednasky.models.Prednaska>`
(neobsahuje téměř nic, většina se musí doplnit jiným způsobem)
"""
#: ID :py:class:`Přednášky <prednasky.models.Prednaska>`, o které se hlasuje
prednaska_id = forms.IntegerField(widget=forms.HiddenInput)
#: :py:class:`Hodnocení (Body) <prednasky.models.Hlasovani.Body>` této přednášky
body = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=Hlasovani.Body.choices, initial=Hlasovani.Body.JEDNO)
#: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniPrednaskaFormů <prednasky.forms.HlasovaniPrednaskaForm>`)
#: pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o množině :py:class:`Přednášek <prednasky.models.Prednaska>`
HlasovaniPrednaskaFormSet = forms.formset_factory(HlasovaniPrednaskaForm, extra=0)
class HlasovaniZnalostiForm(forms.Form):
""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o jedné :py:class:`Znalosti <prednasky.models.Znalost>`
(neobsahuje téměř nic, většina se musí doplnit jiným způsobem)
"""
#: ID :py:class:`Znalosti <prednasky.models.Znalost>`, o které hlasujeme
znalost_id = forms.IntegerField(widget=forms.HiddenInput)
#: :py:class:`Odpověď <prednasky.models.HlasovaniOZnalostech.Odpoved>` na tuto znalost
odpoved = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=HlasovaniOZnalostech.Odpoved.choices)
#: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniZnalostiFormů <prednasky.forms.HlasovaniZnalostiForm>`)
#: pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o množině :py:class:`Znalostí <prednasky.models.Znalost>`
HlasovaniZnalostiFormSet = forms.formset_factory(HlasovaniZnalostiForm, extra=0)

View file

@ -0,0 +1,39 @@
# Generated by Django 4.2.16 on 2025-01-24 13:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'),
('prednasky', '0018_post_split_soustredeni'),
]
operations = [
migrations.CreateModel(
name='Znalost',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nazev', models.CharField(help_text='Např. Neuronové sítě', max_length=200, verbose_name='Nadpis')),
('text', models.TextField(blank=True, help_text='Např. Perceptron, vrstevnatá síť, forward a backward propagation', null=True, verbose_name='Detailní popis')),
('seznamy', models.ManyToManyField(to='prednasky.seznam')),
],
options={
'verbose_name': 'Znalost k přednáškám',
'verbose_name_plural': 'Znalosti k přednáškám',
'db_table': 'prednasky_znalost',
},
),
migrations.CreateModel(
name='HlasovaniOZnalostech',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('odpoved', models.CharField(choices=[(-1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (1, 'Tohle vůbec neznám')], max_length=16, verbose_name='odpověď')),
('seznam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='prednasky.seznam')),
('ucastnik', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personalni.osoba')),
('znalost', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prednasky.znalost')),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-01-24 20:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('prednasky', '0019_znalost_hlasovanioznalostech'),
]
operations = [
migrations.AlterField(
model_name='hlasovani',
name='body',
field=models.IntegerField(choices=[(-1, 'rozhodně nechci'), (0, 'je mi to jedno'), (1, 'rozhodně chci')], default=0, verbose_name='Body'),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 4.2.16 on 2025-02-04 20:09
from django.db import migrations, models
def zmena_bodu(apps, _schema_editor):
HlasovaniOZnalostech = apps.get_model('prednasky','HlasovaniOZnalostech')
for h in HlasovaniOZnalostech.objects.all():
h.odpoved = -int(h.odpoved)
h.save()
class Migration(migrations.Migration):
dependencies = [
('prednasky', '0020_alter_hlasovani_body'),
]
operations = [
migrations.AlterField(
model_name='hlasovanioznalostech',
name='odpoved',
field=models.IntegerField(choices=[(1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (-1, 'Tohle vůbec neznám')], verbose_name='odpověď'),
),
migrations.RunPython(zmena_bodu, reverse_code=zmena_bodu),
]

View file

@ -1,81 +1,134 @@
from django.db import models from django.db import models
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from personalni.models import Organizator from personalni.models import Organizator, Osoba
STAV_NAVRH = 1
STAV_BUDE = 2
STAV_CHOICES = (
(STAV_NAVRH, 'Návrh'),
(STAV_BUDE, 'Bude')
)
class Seznam(models.Model): class Seznam(models.Model):
class Meta: """
db_table = 'prednasky_seznam' Spojuje :py:class:`Přednášky <prednasky.models.Prednaska>`
verbose_name = 'Seznam přednášek' se :py:class:`Soustředěními <soustredeni.models.Soustredeni>`,
verbose_name_plural = 'Seznamy přednášek' kde by mohly zaznít, nebo zazní/zazněly.
ordering = ['soustredeni', 'stav'] """
id = models.AutoField(primary_key = True) class Meta:
soustredeni = models.ForeignKey(Soustredeni,null = True, default = None, db_table = "prednasky_seznam"
on_delete=models.PROTECT) verbose_name = "Seznam přednášek"
stav = models.IntegerField('Stav',choices=STAV_CHOICES,default = STAV_NAVRH) verbose_name_plural = "Seznamy přednášek"
ordering = ["soustredeni", "stav"]
class Stav(models.IntegerChoices):
""" Stav seznamu přednášek (NAVRH se používá k hlasování viz :py:func:`daný view <prednasky.views.newPrednaska>`). """
NAVRH = 1, "Návrh"
BUDE = 2, "Bude"
id = models.AutoField(primary_key=True)
soustredeni = models.ForeignKey(Soustredeni, null=True, default=None, on_delete=models.PROTECT)
stav = models.IntegerField("Stav", choices=Stav.choices, default=Stav.NAVRH) #: :py:class:`Stav <prednasky.models.Seznam.Stav>` Seznamu
def __str__(self): def __str__(self):
return "Seznam {}přednášek na {}".format("návrhů " return f"Seznam {'návrhů ' if self.stav == Seznam.Stav.NAVRH else ''}přednášek na {self.soustredeni}"
if self.stav == STAV_NAVRH else "", self.soustredeni)
CHOICES_OBTIZNOST = (
(1, 'Lehká'),
(2, 'Střední'),
(3, 'Těžká'),
)
CHOICES_BODY = (
(-1, '-1'),
(0, '0'),
(1, '1'),
)
class Prednaska(models.Model): class Prednaska(models.Model):
"""
Reprezentuje přednášku, kterou si org může vypsat a účastník o hlasovat.
(Viz :py:class:`Hlasování <prednasky.models.Hlasovani>`.)
"""
class Meta: class Meta:
db_table = 'prednasky_prednaska' db_table = "prednasky_prednaska"
verbose_name = 'Přednáška' verbose_name = "Přednáška"
verbose_name_plural = 'Přednášky' verbose_name_plural = "Přednášky"
ordering = ['org', 'nazev'] ordering = ["org", "nazev"]
id = models.AutoField(primary_key = True) class Obtiznost(models.IntegerChoices):
nazev = models.CharField('Název', max_length = 300) LEHKA = 1, "Lehká"
org = models.ForeignKey(Organizator, on_delete=models.PROTECT) STREDNI = 2, "Střední"
popis = models.TextField('Popis pro orgy',null = True, blank = True,help_text = 'Neveřejný popis pro ostatní orgy') TEZKA = 3, "Těžká"
anotace = models.TextField('Anotace',null = True, blank = True, help_text = 'Veřejná anotace v hlasování')
obtiznost = models.IntegerField('Obtížnost', choices=CHOICES_OBTIZNOST) id = models.AutoField(primary_key=True)
obor = models.CharField('Obor', max_length = 5, help_text = 'Podmnožina MFIOB') nazev = models.CharField("Název", max_length=300)
klicova = models.CharField('Klíčová slova', max_length = 200, null = True, blank = True) org = models.ForeignKey(Organizator, on_delete=models.PROTECT)
popis = models.TextField("Popis pro orgy", null=True, blank=True, help_text="Neveřejný popis pro ostatní orgy")
anotace = models.TextField("Anotace", null=True, blank=True, help_text="Veřejná anotace v hlasování")
obtiznost = models.IntegerField("Obtížnost", choices=Obtiznost.choices) #: :py:class:`Obtížnost <prednasky.models.Prednaska.Obtiznost>` Přednášky
obor = models.CharField("Obor", max_length=5, help_text="Podmnožina MFIOB")
klicova = models.CharField("Klíčová slova", max_length=200, null=True, blank=True)
seznamy = models.ManyToManyField(Seznam) seznamy = models.ManyToManyField(Seznam)
def __str__(self): def __str__(self):
return "{} ({})".format(self.nazev, self.org) return f"{self.nazev} ({self.org})"
class Hlasovani(models.Model): class Hlasovani(models.Model):
"""
Reprezentuje hlasování jednoho účastníka
o jedné :py:class:`Přednášce <prednasky.models.Prednaska>`
v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit)
"""
class Meta: class Meta:
db_table = 'prednasky_hlasovani' db_table = "prednasky_hlasovani"
verbose_name = 'Hlasování' verbose_name = "Hlasování"
verbose_name_plural = 'Hlasování' verbose_name_plural = "Hlasování"
ordering = ['ucastnik', 'prednaska'] ordering = ["ucastnik", "prednaska"]
id = models.AutoField(primary_key = True)
class Body(models.IntegerChoices):
""" Ohodnocení přednášky v daném Hlasování (větší číslo = víc chci) """
NECHCI = -1, "rozhodně nechci"
JEDNO = 0, "je mi to jedno"
CHCI = 1, "rozhodně chci"
id = models.AutoField(primary_key=True)
prednaska = models.ForeignKey(Prednaska, on_delete=models.CASCADE) prednaska = models.ForeignKey(Prednaska, on_delete=models.CASCADE)
body = models.IntegerField('Body', default = 0, choices = CHOICES_BODY) #: Příslušné hlasování: :py:class:`Body <prednasky.models.Hlasovani.Body>`
ucastnik = models.CharField('Účastník', max_length = 100) body = models.IntegerField("Body", default=Body.JEDNO, choices=Body.choices)
seznam = models.ForeignKey(Seznam,null=True,on_delete=models.SET_NULL)
#: Účastník, který hlasoval. Pouze string:
#: *(přechod z jména na objekt Osoby nějak kape na tom,
#: že všechna předchozí hlasování zde mají náhodný string…)
#: TODO Změnit to na Osobu*
ucastnik = models.CharField("Účastník", max_length=100)
seznam = models.ForeignKey(Seznam, null=True, on_delete=models.SET_NULL)
def __str__(self): def __str__(self):
return "{} dal {} bodů {} v seznamu {}".format(self.ucastnik, return f"{self.ucastnik} dal {self.body} bodů {self.prednaska} v seznamu {self.seznam}"
self.body, self.prednaska, self.seznam)
class Znalost(models.Model):
"""
Reprezentuje znalost, na kterou se můžeme účastníka ptát (nechat je hlasovat).
(Viz :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`.)
"""
class Meta:
db_table = "prednasky_znalost"
verbose_name = "Znalost k přednáškám"
verbose_name_plural = "Znalosti k přednáškám"
nazev = models.CharField("Nadpis", max_length=200, blank=False, null=False, help_text="Např. Neuronové sítě")
text = models.TextField("Detailní popis", blank=True, null=True, help_text="Např. Perceptron, vrstevnatá síť, forward a backward propagation")
seznamy = models.ManyToManyField(Seznam)
def __str__(self):
return self.nazev
class HlasovaniOZnalostech(models.Model):
"""
Reprezentuje hlasování jednoho účastníka
o jedné :py:class:`Znalosti <prednasky.models.Znalost>`
v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit)
"""
class Odpoved(models.IntegerChoices):
""" Na kolik danou znalost účastník ovládá v daném Hlasování (větší číslo = víc zná) """
UMIM = 1, "Tohle celkem umím"
CIRCA = 0, "Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím"
NEUMIM = -1, "Tohle vůbec neznám"
odpoved = models.IntegerField(u"odpověď", choices=Odpoved.choices, blank=False, null=False) #: :py:class:`Odpověď <prednasky.models.Prednaska.Odpoved>` na HlasováníOZnalostech
znalost = models.ForeignKey(Znalost, on_delete=models.CASCADE, blank=False, null=False)
ucastnik = models.ForeignKey(Osoba, on_delete=models.CASCADE, blank=False, null=False)
seznam = models.ForeignKey(Seznam, on_delete=models.SET_NULL, blank=True, null=True)
def __str__(self):
return f"{self.ucastnik} dal {self.znalost} bodů {self.znalost} v seznamu {self.seznam}"

View file

@ -5,36 +5,36 @@
{% block content %} {% block content %}
<h1> <h1>{% block nadpis1a %}Hlasování o přednáškách{% endblock %}</h1>
{% block nadpis1a %}Hlasování o přednáškách{% endblock %}
</h1>
<p>
Jak moc by ses chtěl(a) zúčastnit následujících přednášek?
<br>
<span style="font-size: 75%">Obtížnost 1 je nejlehčí, 3 nejtěžší.</span>
</p>
<form enctype="multipart/form-data" action="." method="post"> <form enctype="multipart/form-data" action="." method="post">
{% csrf_token %} {% csrf_token %}
<table>
{% for p, h in prednasky %} <h3>Jak moc by ses chtěl(a) zúčastnit následujících přednášek?</h3>
<tr><td><label>{{p.org}}: <span style="font-size: 175%">{{p.nazev}}</span></label></td></tr> <p>Obtížnost 1 je nejlehčí, 3 nejtěžší.</p>
<tr><td><p><i>{{p.anotace}}</i></p></td></tr> {{ form_set_prednasky.management_form }}
<tr><td><label>Obor: </label> {{p.obor}}</td></tr> {% for f, p in formy_a_prednasky %}
<tr><td><label>Obtížnost: </label> {{p.obtiznost}}</td> </tr> <h4>{{p.nazev}} ({{p.org}})</h4>
{% if p.klicova %}<tr><td><label>Klíčová slova: </label> {{p.klicova}}</td></tr>{% endif%} <p class="textprednasky">{{p.anotace}}</p>
<tr><td>Hodnocení: <label>Obor: </label> {{p.obor}}<br>
<INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="-1" {% if h == -1 %} CHECKED="checked" {% endif %} > rozhodně nechci <label>Obtížnost: </label> {{p.obtiznost}}<br>
<INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="0" {% if h == 0 %} CHECKED="checked" {% endif %}> je mi to jedno {% if p.klicova %}<label>Klíčová slova: </label> {{p.klicova}}<br>{% endif%}
<INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="1" {% if h == 1 %} CHECKED="checked" {% endif %}> rozhodně chci <br>
</td></tr> {{ f }}
<tr><td>&nbsp;</td></tr> <br>
{% empty %} {% empty %}
Nejsou žádné přednášky o kterých by šlo hlasovat. Nejsou žádné přednášky o kterých by šlo hlasovat.
{% endfor %} {% endfor %}
<tr><td><input name="odeslat" type="submit" value="Odeslat"></td><tr>
</table> {{ form_set_znalosti.management_form }}
{% for f, z in formy_a_znalosti %}
{% if forloop.first %}<hr/><h3>Jak moc znáš následující?</h3>{% endif %}
<h4>{{z.nazev}}</h4>
<p class="textznalosti">{{z.text}}</p>
{{ f }}
<br>
{% endfor %}
<input type="submit" value="Odeslat"/>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -14,7 +14,7 @@
{% else %} {% else %}
<a href="/prednasky/seznam_prednasek/{{seznam.id}}">Seznam přednášek na soustředění {{seznam.soustredeni.misto}} </a> <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Seznam přednášek na soustředění {{seznam.soustredeni.misto}} </a>
{% endif %} {% endif %}
<a href="/prednasky/seznam_prednasek/{{seznam.id}}/export">Export</a> <a href="/prednasky/seznam_prednasek/{{seznam.id}}/hlasovani.csv">Export</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -12,10 +12,15 @@ urlpatterns = [
'prednasky/metaseznam_prednasek', 'prednasky/metaseznam_prednasek',
org_required(views.MetaSeznamListView.as_view()), org_required(views.MetaSeznamListView.as_view()),
name='metaseznam-list'), name='metaseznam-list'),
# path(
# 'prednasky/seznam_prednasek/<int:seznam>/export',
# org_required(views.SeznamExportView),
# name='seznam-export'
# ),
path( path(
'prednasky/seznam_prednasek/<int:seznam>/export', 'prednasky/seznam_prednasek/<int:seznam>/hlasovani.csv',
org_required(views.SeznamExportView), org_required(views.PrednaskyExportView),
name='seznam-export' name='seznam-export-csv'
), ),
path( path(
'prednasky/seznam_prednasek/<int:seznam>/', 'prednasky/seznam_prednasek/<int:seznam>/',

View file

@ -1,77 +1,142 @@
import csv
import http import http
import logging
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.views import generic from django.views import generic
from django.shortcuts import HttpResponseRedirect from django.shortcuts import HttpResponseRedirect
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum from django.db import transaction
from django.forms import Form
from various.views.pomocne import formularOKView from various.views.pomocne import formularOKView
from .forms import HlasovaniPrednaskaFormSet, HlasovaniZnalostiFormSet
from various.models import Nastaveni from various.models import Nastaveni
from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH from prednasky.models import Prednaska, Hlasovani, Znalost, HlasovaniOZnalostech, Seznam
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from personalni.models import Osoba from personalni.models import Osoba
def newPrednaska(request): PREDNASKY_PREFIX = "prednasky"
ZNALOSTI_PREFIX = "znalosti"
logger = logging.getLogger(__name__)
def newPrednaska(request: HttpRequest) -> HttpResponse:
"""
View zobrazující a ukládající účastnické hlasování
(:py:class:`Hlasování <prednasky.models.Hlasovani>`
a :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`)
o :py:class:`Přednáškách <prednasky.models.Prednaska>`
a :py:class:`Znalostech <prednasky.models.Znalost>`
"""
# hlasovani se vztahuje k nejnovejsimu soustredeni # hlasovani se vztahuje k nejnovejsimu soustredeni
sous = Nastaveni.get_solo().aktualni_sous sous = Nastaveni.get_solo().aktualni_sous
seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first() seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first()
if sous is None or seznam is None: if sous is None or seznam is None:
return render(request, 'universal.html', { return render(request, 'universal.html', {
'title': "Nelze hlasovat", 'title': "Nelze hlasovat",
'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.", 'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.",
}, status=http.HTTPStatus.NOT_FOUND) }, status=http.HTTPStatus.NOT_FOUND)
osoba = Osoba.objects.filter(user=request.user).first() osoba = Osoba.objects.filter(user=request.user).first()
ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) # id, kvůli kolizi jmen
# obsluha formulare
if request.method == 'POST':
form = Form(request.POST, request.FILES)
if form.is_valid():
# id z důvodu duplicitních jmen (přechod z jména na objekt Osoby nějak kape na tom,
# že všechna předchozí hlasování zde mají náhodný string…)
# TODO Změnit to na Osobu
# TODO v následujících řádcích je zbytečně mnoho dotazů na QuerySet (pokud účastník hlasoval, hlasoval u všech) if request.method == 'POST': # Když to byl POST, tak ukládáme.
for i in request.POST: # Načteme data do formsetů
if i[0] == 'q': form_set_prednasky = HlasovaniPrednaskaFormSet(request.POST, prefix=PREDNASKY_PREFIX)
prednaska = Prednaska.objects.filter(pk=int(i[1:]))[0] form_set_znalosti = HlasovaniZnalostiFormSet(request.POST, prefix=ZNALOSTI_PREFIX)
hlasovani = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first()
if not hlasovani: if form_set_prednasky.is_valid() and form_set_znalosti.is_valid():
hlasovani = Hlasovani() with transaction.atomic():
hlasovani.prednaska = prednaska # Místo updatování data prostě smažeme a vytvoříme nová
hlasovani.ucastnik = ucastnik seznam.hlasovani_set.filter(ucastnik=ucastnik).delete()
hlasovani.seznam = seznam seznam.hlasovanioznalostech_set.filter(ucastnik=osoba).delete()
hlasovani.body = int(request.POST[i])
hlasovani.save() for form in form_set_prednasky:
prednaska_id = form.cleaned_data['prednaska_id']
prednaska = Prednaska.objects.filter(id=prednaska_id).first()
if prednaska is None:
logger.error(f"Účastník {ucastnik} hodnotil neexistující přednášku {prednaska_id} číslem {form.cleaned_data['body']}")
continue
Hlasovani.objects.create(
prednaska=prednaska,
body=form.cleaned_data['body'],
ucastnik=ucastnik,
seznam=seznam,
)
for form in form_set_znalosti:
znalost_id = form.cleaned_data['znalost_id']
znalost = Znalost.objects.filter(id=znalost_id).first()
if znalost is None:
logger.error(f"Účastník {ucastnik} hodnotil neexistující znalost {znalost_id} číslem {form.cleaned_data['odpoved']}")
continue
HlasovaniOZnalostech.objects.create(
odpoved=form.cleaned_data['odpoved'],
znalost=znalost,
ucastnik=osoba,
seznam=seznam,
)
# presmerovani na prave vzniklou galerii
return HttpResponseRedirect('./hotovo') return HttpResponseRedirect('./hotovo')
def prednaska_hodnoceni(prednaska): else: # Pokud je nějaký formset nevalidní, vracíme je k přepracování
h = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first() prednasky = seznam.prednaska_set.all()
if h: znalosti = seznam.znalost_set.all()
return prednaska, h.body # FIXME Spadnout, pokud nesedí přednáška/znalost s formulářem. (Nějak se mi to nepovedlo.)
else: # Může se totiž stát, že se mezitím změnily přednášky (nějaká byla přidána/odebrána)
return prednaska, 0
else: # Když to nebyl POST, tak inicializujeme (pokud už o přednášce/znalosti účastník hlasoval, předvyplníme mu to).
def odpoved_prednasky(p: Prednaska) -> Hlasovani.Body:
hlasovani = p.hlasovani_set.filter(ucastnik=ucastnik).first()
return hlasovani.body if hlasovani else Hlasovani.Body.JEDNO
def odpoved_znalosti(z: Znalost) -> HlasovaniOZnalostech.Odpoved:
hlasovani = z.hlasovanioznalostech_set.filter(ucastnik=osoba).first()
return hlasovani.odpoved if hlasovani else HlasovaniOZnalostech.Odpoved.CIRCA
prednasky = seznam.prednaska_set.all()
znalosti = seznam.znalost_set.all()
form_set_prednasky = HlasovaniPrednaskaFormSet(initial=[
{"prednaska_id": p.id, "body": odpoved_prednasky(p)} for p in prednasky
], prefix=PREDNASKY_PREFIX)
form_set_znalosti = HlasovaniZnalostiFormSet(initial=[
{"znalost_id": z.id, "odpoved": odpoved_znalosti(z)} for z in znalosti
], prefix=ZNALOSTI_PREFIX)
# V případě nePOSTu nebo chyby při ukládání vracíme hlasování
return render( return render(
request, request,
'prednasky/base.html', 'prednasky/base.html',
{'prednasky': map(prednaska_hodnoceni, seznam.prednaska_set.all())} {
'form_set_prednasky': form_set_prednasky, 'form_set_znalosti': form_set_znalosti,
'formy_a_prednasky': zip(form_set_prednasky, prednasky),
'formy_a_znalosti': zip(form_set_znalosti, znalosti),
}
) )
def Prednaska_hotovo(request): def Prednaska_hotovo(request: HttpRequest) -> HttpResponse:
""" View po vyplnění :py:func:`hlasování <prednasky.views.newPrednaska>` """
return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.") return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.")
class MetaSeznamListView(generic.ListView): class MetaSeznamListView(generic.ListView):
""" Seznam všech :py:class:`Seznamů <prednasky.models.Seznam>` s odkazy na exporty """
model = Seznam model = Seznam
template_name = 'prednasky/metaseznam_prednasek.html' template_name = 'prednasky/metaseznam_prednasek.html'
class SeznamListView(generic.ListView): class SeznamListView(generic.ListView):
"""
Náhled na to, kolik která přednáška v :py:class:`Seznamu <prednasky.models.Seznam>` :py:class:`hlasů <prednasky.models.Hlasovani.Body>`.
(Je otázka, zda tento View vůbec chceme. Pokud ano, hodilo by se do něj přidat i znalosti.)
"""
template_name = 'prednasky/seznam_prednasek.html' template_name = 'prednasky/seznam_prednasek.html'
def get_queryset(self): def get_queryset(self):
@ -87,7 +152,7 @@ class SeznamListView(generic.ListView):
# hlasovani se vztahuje k nejnovejsimu soustredeni # hlasovani se vztahuje k nejnovejsimu soustredeni
sous = Soustredeni.objects.first() sous = Soustredeni.objects.first()
seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first() seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first()
for obj in self.object_list: for obj in self.object_list:
hlasovani_set = obj.hlasovani_set.filter(seznam=seznam).only('body') hlasovani_set = obj.hlasovani_set.filter(seznam=seznam).only('body')
@ -96,32 +161,86 @@ class SeznamListView(generic.ListView):
return context return context
def SeznamExportView(request, seznam): # def SeznamExportView(request, seznam):
"""Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor""" # """Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor"""
# TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro # # TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro
# lidi? # # lidi?
hlasovani = Hlasovani.objects.filter(seznam=seznam) # hlasovani = Hlasovani.objects.filter(seznam=seznam)
prednasky = Prednaska.objects.filter(seznamy=seznam) # prednasky = Prednaska.objects.filter(seznamy=seznam)
orgove = set(p.org for p in prednasky) # orgove = set(p.org for p in prednasky)
ucastnici = set(h.ucastnik for h in hlasovani) # ucastnici = set(h.ucastnik for h in hlasovani)
#
# for p in prednasky:
# p.body = []
# for u in ucastnici:
# try:
# p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body)
# except ObjectDoesNotExist:
# # účastník nehlasoval
# p.body.append("?")
#
# for h in hlasovani:
# h.ucastnik = hash(h.ucastnik)
#
# return render(
# request,
# 'prednasky/seznam_prednasek_export.txt',
# {"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove},
# content_type="text/plain"
# )
for p in prednasky:
p.body = [] def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResponse:
for u in ucastnici: """
try: Vrátí všechna :py:class:`Hlasování <prednasky.models.Hlasovani>`
p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body) i :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`
except ObjectDoesNotExist: v daném :py:class:`Seznamu <prednasky.models.Seznam>`
# účastník nehlasoval jako csv soubor (řádky = účastníci, sloupce = přednášky&znalosti).
p.body.append("?")
:param seznam: ID daného :py:class:`Seznamu <prednasky.models.Seznam>`
"""
hlasovani = Hlasovani.objects.filter(seznam=seznam).select_related("prednaska")
hlasovani_o_znalostech = HlasovaniOZnalostech.objects.filter(seznam=seznam).select_related('ucastnik', 'znalost')
# Inicializujeme sloupce
prednasky = list(Prednaska.objects.filter(seznamy=seznam))
znalosti = list(Znalost.objects.filter(seznamy=seznam))
prednasky_map: dict[int, int] = {p.id: i for i, p in enumerate(prednasky, 1)}
offset = len(prednasky_map)
znalosti_map: dict[int, int] = {z.id: i for i, z in enumerate(znalosti, offset + 1)}
width = offset + len(znalosti_map)
# A po inicializaci sloupců vyplníme tabulku
table: [str, list[str|Prednaska|Znalost,]] = {}
for h in hlasovani: for h in hlasovani:
h.ucastnik = hash(h.ucastnik) if h.ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek
table[h.ucastnik] = [h.ucastnik] + ([""] * width)
return render( if h.prednaska.id in prednasky_map:
request, table[h.ucastnik][prednasky_map[h.prednaska.id]] = h.body
'prednasky/seznam_prednasek_export.txt', else:
{"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove}, pass # TODO Padat hlasitě?
content_type="text/plain"
) for h in hlasovani_o_znalostech:
ucastnik = str(h.ucastnik) + ' ' + str(h.ucastnik.id) # id, kvůli kolizi jmen
if ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek
table[ucastnik] = [ucastnik] + ([""] * width)
if h.znalost.id in znalosti_map:
table[ucastnik][znalosti_map[h.znalost.id]] = h.odpoved
else:
pass # TODO Padat hlasitě?
response = HttpResponse(content_type="text/csv", charset="utf-8")
response["Content-Disposition"] = 'attachment; filename="hlasovani.csv"'
writer = csv.writer(response)
writer.writerow(["jména \\ přednáška|znalost"] + list(map(str, prednasky + znalosti)))
for row in table.values():
writer.writerow(list(map(str, row)))
return response

View file

@ -17,7 +17,7 @@ django-solo # Singleton model (speciálně Nastavení)
django-ckeditor-5 # Editor htmlka (hlavně v adminu u flatpages) django-ckeditor-5 # Editor htmlka (hlavně v adminu u flatpages)
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
django-taggit # Taggy v djangu (speciálně zaměření problémů) django-taggit # Taggy v djangu (speciálně zaměření problémů)
django-autocomplete-light>=3.9.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích django-autocomplete-light>=3.9.0,<3.12.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích
django-imagekit # Všechny možné obrázky v Djangu django-imagekit # Všechny možné obrázky v Djangu
django-polymorphic # Polymorfismus na django modelech (hlavně Problém nebo treenode) django-polymorphic # Polymorfismus na django modelech (hlavně Problém nebo treenode)
django-sitetree # Struktura stránek, hlavně pro meníčko django-sitetree # Struktura stránek, hlavně pro meníčko
@ -49,4 +49,5 @@ lorem
sphinx sphinx
sphinx_rtd_theme sphinx_rtd_theme
sphinxcontrib-django
myst_parser myst_parser