ruzne exporty resitelu - zatím určitě ne merge xd spíš potřebuji zpětnou vazbu... #89

Merged
zelvuska merged 5 commits from export_resitelskych_dat into master 2025-02-26 21:01:25 +01:00
5 changed files with 236 additions and 1 deletions

View file

@ -0,0 +1,88 @@
{% extends "base.html" %}
{% block content %}
<h2><strong>Export lidí</strong></h2>
<select name="select-one" id="select-one">
<option value="0">---</option>
<option value="1">Řešitelé čísla</option>
<option value="2">Řešitelé ročníku</option>
<option value="3">Všichni řešitelé, kteří ještě neodmaturovali</option>
<option value="4">Organizátoři soustředění</option>
</select>
<select name="select-two" id="select-two">
<!-- will be filled with ajax -->
</select>
<button id="download-button">Stáhnout</button>
<script defer>
const select_one = document.getElementById("select-one")
const select_two = document.getElementById("select-two")
const download_button = document.getElementById("download-button")
download_button.style.display = 'none'
select_two.style.display = 'none'
const fetch_dict_string = '{{ typy_exportu|safe }}'
const fetch_dict = JSON.parse(fetch_dict_string)
select_one.addEventListener('change', (e) => {
value = e.target.value
select_two.style.display = 'none'
select_two.innerHTML = ''
// puvodni stav
if (value == 0) {
download_button.style.display = 'none'
select_two.style.display = 'none'
return
}
// v tomto pripade muzeme rovnou stahnout
if (!(value in fetch_dict)) {
download_button.style.display = 'block'
select_two.style.display = 'none'
return
}
download_button.style.display = 'none'
fetch("/profil/exporty_lidi/get/" + value)
.then(response => response.json())
.then(data => {
const option = document.createElement('option')
option.value = 0
option.text = '---'
select_two.appendChild(option)
for (const [key, value] of Object.entries(data)) {
const option = document.createElement('option')
option.value = value["id"]
option.text = value["display"]
select_two.appendChild(option)
}
select_two.style.display = 'block'
})
})
select_two.addEventListener('change', (e) => {
value = e.target.value
if (value == 0) {
download_button.style.display = 'none'
return
}
download_button.style.display = 'block'
})
download_button.addEventListener('click', (e) => {
if (select_two.innerHTML == '') {
window.location.href = "/profil/exporty_lidi/get_csv_only_one_step/" + select_one.value
} else {
window.location.href = "/profil/exporty_lidi/get_csv/" + select_one.value + "/" + select_two.value
}
})
</script>
{% endblock %}

View file

@ -107,6 +107,13 @@
</li>
</ul>
<hr />
<h2><strong>Exporty dat lidí v semináří</strong></h2>
<ul>
<li><a href="{% url 'exporty_lidi' %}">dostupné exporty</a></li>
</ul>
<hr />
<p>Nemůžeš najít, co hledáš? Může to být v <a href="{% url 'admin:index' %}">administračním rozhraní webu</a>.</p>
{% endblock content %}

View file

@ -38,6 +38,28 @@ urlpatterns = [
'org/propagace/jak-se-dozvedeli/',
org_required(views.JakSeDozvedeliView.as_view()),
name='jak_se_dozvedeli'
),
# export dat o řešitelích
path(
'profil/exporty_lidi',
org_required(views.ExportLidiView.as_view()),
name='exporty_lidi',
),
path(
'profil/exporty_lidi/get/<int:type>',
org_required(views.get_export_options),
name='exporty_lidi_options',
),
path(
'profil/exporty_lidi/get_csv_only_one_step/<int:type>',
org_required(views.download_export_csv_only_first_step),
name='exporty_lidi_data',
),
path(
'profil/exporty_lidi/get_csv/<int:type>/<int:id>',
org_required(views.download_export_csv),
name='exporty_lidi_download',
)
]

View file

@ -20,13 +20,16 @@ from django.utils import timezone
import personalni.models as m
from soustredeni.models import Soustredeni
from odevzdavatko.models import Hodnoceni
from tvorba.models import Clanek, Uloha, Tema
from tvorba.models import Clanek, Uloha, Tema, Cislo, Rocnik
import tvorba.utils as tvorba_utils
from various.models import Nastaveni
from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
from datetime import date
import logging
import csv
from enum import Enum
import json
from various.views.pomocne import formularOKView
from various.autentizace.views import LoginView
@ -141,6 +144,54 @@ class OrgoRozcestnikView(TemplateView):
#content_type = 'text/plain; charset=UTF8'
#XXX
class PrvniTypExportu(Enum):
CISLA = 1
ROCNIKU = 2
SOUSTREDENI = 4
class ExportLidiView(TemplateView):
template_name = 'personalni/profil/export_lidi.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['typy_exportu'] = json.dumps({member.value: member.name.lower().capitalize() for member in PrvniTypExportu})
return context
def get_export_options(request, type):
if type == PrvniTypExportu.CISLA.value:
data = [{"id": c.id, "display": str(c)} for c in Cislo.objects.all()]
if type == PrvniTypExportu.ROCNIKU.value:
data = [{"id": r.id, "display": str(r)} for r in Rocnik.objects.all()]
if type == PrvniTypExportu.SOUSTREDENI.value:
data = [{"id": s.id, "display": str(s)} for s in Soustredeni.objects.all()]
return HttpResponse(json.dumps(data), content_type='application/json')
def download_export_csv_only_first_step(request, type):
if type == 3:
response = dataResiteluCsvResponse(tvorba_utils.resitele_co_neodmaturovali())
response['Content-Disposition'] = 'attachment; filename="resitele_co_neodmaturovali.csv"'
return response
def download_export_csv(request, type, id):
if type == PrvniTypExportu.CISLA.value:
response = dataResiteluCsvResponse(tvorba_utils.resi_cislo(Cislo.objects.get(id=id)))
name = str(Cislo.objects.get(id=id)).replace(" ", "_") + "_resitele_cisla.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
if type == PrvniTypExportu.ROCNIKU.value:
response = dataResiteluCsvResponse(tvorba_utils.resi_v_rocniku(Rocnik.objects.get(id=id)))
name = str(Rocnik.objects.get(id=id)).replace(" ", "_") + "_resitele_rocniku.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
if type == PrvniTypExportu.SOUSTREDENI.value:
soustredeni = Soustredeni.objects.get(id=id)
organizatori = soustredeni.organizatori.all()
organizatoriOsoby = Osoba.objects.filter(org__in=organizatori)
response = dataOsobCsvResponse(organizatoriOsoby, columns=("jmeno", "prijmeni", "email", "telefon",))
name = str(soustredeni).replace(" ", "_") + "_organizatori_soustredeni.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
class ResitelView(LoginRequiredMixin,generic.DetailView):
model = m.Resitel
@ -470,3 +521,46 @@ def dataResiteluCsvResponse(queryset, columns=None, with_header=True):
writer.writerows(queryset_list)
return response
def dataOsobCsvResponse(queryset, columns=None, with_header=True):
ticvac marked this conversation as resolved
Review

Uh, pojďme ten kód neduplikovat. Co když default_columns a field_name_overrides budou slovníky, které jako klíče budou mít typ objektu (personalni.models.Osoba nebo personalni.models.Resitel), podle toho se ta správná věc zvolí a zbytek bude jen jeden?

Uh, pojďme ten kód neduplikovat. Co když `default_columns` a `field_name_overrides` budou slovníky, které jako klíče budou mít typ objektu (`personalni.models.Osoba` nebo `personalni.models.Resitel`), podle toho se ta správná věc zvolí a zbytek bude jen jeden?
Review

Přičemž klidně seznam pro Řešitele můžeme prostě odvodit ze seznamu pro Osoby kódem, to ničemu asi nevadí…)

Přičemž klidně seznam pro Řešitele můžeme prostě odvodit ze seznamu pro Osoby kódem, to ničemu asi nevadí…)
"""Pomocná funkce pro vracení dat osob jako CSV. Musí dostat správný QuerySet, který dává Ososby"""
default_columns = (
'id',
ticvac marked this conversation as resolved
Review

Tohle iirc nefunguje pro Osoby, protože už nemají field osoba

Tohle iirc nefunguje pro Osoby, protože už nemají field `osoba`…
'jmeno',
'prijmeni',
'prezdivka',
'email',
'telefon',
'datum_narozeni',
'osloveni',
'ulice',
'mesto',
'psc',
'stat',
'jak_se_dozvedeli',
'poznamka',
'datum_registrace',
'datum_souhlasu_udaje',
'datum_souhlasu_zasilani',
)
if columns is None: columns = default_columns
def get_field_name(column_name):
return column_name
response = HttpResponse(content_type='text/csv')
writer = csv.writer(response)
# První řádek je záhlaví
if with_header:
writer.writerow(map(get_field_name, columns))
# Data:
queryset_list = queryset.values_list(*columns)
writer.writerows(queryset_list)
return response

View file

@ -28,6 +28,30 @@ def resi_v_rocniku(rocnik, cislo=None):
reseni__hodnoceni__deadline_body__cislo__poradi__lte=cislo.poradi
).distinct()
def resi_cislo(cislo):
""" Vrátí seznam řešitelů, co vyřešili nějaký problém v daném čísle.
Parametry:
cislo (typu Cislo) číslo, ve kterém chci řešitele, co něco odevzdali
Výstup:
QuerySet objektů typu Resitel
"""
ticvac marked this conversation as resolved
Review

@zelvuska už nějak bastlil autogenerovanou dokumentaci, zkuste to asi nějak poladit, ať to dopadne nějak konzistentně (hlavně ať se případně změny stylu dají dělat na jednom místě nad (aspoň trochu) strukturovanými daty a ne všude v kódu…)

(As in: mně to je jedno a je super, že to je zdokumentované, jen to pak možná bude někde dělat bordel…)

@zelvuska už nějak bastlil autogenerovanou dokumentaci, zkuste to asi nějak poladit, ať to dopadne nějak konzistentně (hlavně ať se případně změny stylu dají dělat na jednom místě nad (aspoň trochu) strukturovanými daty a ne všude v kódu…) (As in: mně to je jedno a je super, že to je zdokumentované, jen to pak možná bude někde dělat bordel…)
Review

Ale jak koukám výš, tak tam je to taky takhle… 🤷

Ale jak koukám výš, tak tam je to taky takhle… 🤷
return personalni.models.Resitel.objects.filter(
reseni__hodnoceni__deadline_body__cislo=cislo
).distinct()
ticvac marked this conversation as resolved
Review

Tohle asi funguje (pokud to něco vrací…). Samozřejmě to naráží na to, že „kdo řešil co v nějakém čísle“ je strašně špatně definované… (Technicky je tohle dotaz na to, kdo má nějaké body zadané k nějakému deadlinu, může dávat lepší smysl se ptát na řešitele, kteří poslali řešení mezi vydáním nějakého čísla a jeho (nejzazším) deadlinem, což je o trochu přesnější ale naopak je potřeba řešit případný překryv deadlinů čísel a asi to za to nestojí…)

Tohle asi funguje (pokud to něco vrací…). Samozřejmě to naráží na to, že „kdo řešil co v nějakém čísle“ je strašně špatně definované… (Technicky je tohle dotaz na to, kdo má nějaké _body_ zadané k nějakému deadlinu, může dávat lepší smysl se ptát na řešitele, kteří poslali řešení mezi vydáním nějakého čísla a jeho (nejzazším) deadlinem, což je o trochu přesnější ale naopak je potřeba řešit případný překryv deadlinů čísel a asi to za to nestojí…)
def resitele_co_neodmaturovali():
""" Vrátí seznam řešitelů, co ještě neodmaturovali.
Pokud ještě není srpen, tak zahrnuje i ty, kteří odmaturovali letos.
Výstup:
QuerySet objektů typu Resitel """
from datetime import datetime
current_year = datetime.now().year
if datetime.now().month < 8:
current_year -= 1
return personalni.models.Resitel.objects.filter(rok_maturity__gte=current_year)
def aktivniResitele(cislo, pouze_letosni=False):
""" Vrací QuerySet aktivních řešitelů, což jsou ti, co ještě neodmaturovali