diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 3c850245..08997bb4 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -138,7 +138,7 @@ INSTALLED_APPS = ( 'various.autentizace', 'api', 'aesop', - + 'odevzdavatko', # Admin upravy: # 'material', diff --git a/mamweb/urls.py b/mamweb/urls.py index 2fdbff45..b47be1cc 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -16,6 +16,9 @@ urlpatterns = [ # Seminarova aplikace (ma vlastni podadresare) path('', include('seminar.urls')), + + # Odevzdavatko (ma vlastni podadresare) + path('', include('odevzdavatko.urls')), # Korekturovaci aplikace (ma vlastni podadresare) path('', include('korektury.urls')), diff --git a/odevzdavatko/__init__.py b/odevzdavatko/__init__.py new file mode 100644 index 00000000..a4ee2679 --- /dev/null +++ b/odevzdavatko/__init__.py @@ -0,0 +1,11 @@ +""" +Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řešení řešitelů. + +Slovníček: + Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. + Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. + Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) + Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. + +TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? +""" \ No newline at end of file diff --git a/odevzdavatko/admin.py b/odevzdavatko/admin.py new file mode 100644 index 00000000..6048eb36 --- /dev/null +++ b/odevzdavatko/admin.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from django_reverse_admin import ReverseModelAdmin +import seminar.models as m + + +class PrilohaReseniInline(admin.TabularInline): + model = m.PrilohaReseni + extra = 1 + + +class Reseni_ResiteleInline(admin.TabularInline): + model = m.Reseni_Resitele + + +@admin.register(m.Reseni) +class ReseniAdmin(ReverseModelAdmin): + base_model = m.Reseni + inline_type = 'tabular' + # inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz + inline_reverse = ['resitele'] + exclude = ['text_zkraceny', 'text_zkraceny_set'] + inlines = [PrilohaReseniInline] +# FAIL in template +# inlines = [PrilohaReseniInline,Reseni_ResiteleInline] + + +admin.site.register(m.PrilohaReseni) +admin.site.register(m.Hodnoceni) diff --git a/odevzdavatko/apps.py b/odevzdavatko/apps.py new file mode 100644 index 00000000..507c284f --- /dev/null +++ b/odevzdavatko/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OdevzdavatkoConfig(AppConfig): + name = 'odevzdavatko' diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py new file mode 100644 index 00000000..14639b65 --- /dev/null +++ b/odevzdavatko/forms.py @@ -0,0 +1,218 @@ +from django import forms +from dal import autocomplete +from django.forms import formset_factory +from django.forms.models import inlineformset_factory + +from seminar.models import Resitel +import seminar.models as m + +import logging + +# pro přidání políčka do formuláře je potřeba +# - mít v modelu tu položku, kterou chci upravovat +# - přidat do views (prihlaskaView, resitelEditView) +# - přidat do forms +# - includovat do html + +class DateInput(forms.DateInput): + # aby se datum dalo vybírat z kalendáře + input_type = 'date' + + +class PosliReseniForm(forms.Form): + #FIXME jen podproblémy daného problému + problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all()) + # to_field_name + #problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', + # through='Hodnoceni') + + # FIXME pridat vice resitelu + resitel = forms.ModelChoiceField(label="Řešitel", + queryset=Resitel.objects.all(), + widget=autocomplete.ModelSelect2( + url='autocomplete_resitel', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}) + ) + + + #resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', + # help_text='Seznam autorů řešení', through='Reseni_Resitele') + + cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení") + + #cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) + + forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES) + #forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, + # default=FORMA_EMAIL) + + poznamka = forms.CharField(label='Neveřejná poznámka', required=False) + #poznamka = models.TextField('neveřejná poznámka', blank=True, + # help_text='Neveřejná poznámka k řešení (plain text)') + + #TODO body do cisla + #TODO prilohy + + ##def __init__(self, *args, **kwargs): + ## super().__init__(*args, **kwargs) + ## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) + +class NahrajReseniForm(forms.ModelForm): + class Meta: + model = m.Reseni + fields = ('problem',) + help_texts = {'problem':''} # Nezobrazovat help text ve formuláři + + widgets = {'problem': + autocomplete.ModelSelect2Multiple( + url='autocomplete_problem_odevzdatelny', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}, + ) + } + +ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, + form = NahrajReseniForm, + fields = ('soubor','res_poznamka'), + widgets = {'res_poznamka':forms.TextInput()}, + extra = 1, + can_delete = False, + + ) + + +class JednoHodnoceniForm(forms.ModelForm): + class Meta: + model = m.Hodnoceni + fields = ('problem', 'body', 'cislo_body') + widgets = { + 'problem': autocomplete.ModelSelect2( + url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší? + ) + } + +OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, + extra = 0, + ) + +class PoznamkaReseniForm(forms.ModelForm): + class Meta: + model = m.Reseni + fields = ('poznamka',) + +# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat +DATE_FORMAT = '%Y-%m-%d' + +class OdevzdavatkoTabulkaFiltrForm(forms.Form): + """Form pro filtrování přehledové odevzdávátkové tabulky + + Inspirováno https://kam.mff.cuni.cz/mffzoom/""" + + # Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices) + + RESITELE_RELEVANTNI = 'relevantni' + RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi' + RESITELE_CHOICES = [ + (RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky + (RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'), + # Možná: všechny vč. historických? + ] + + PROBLEMY_MOJE = 'moje' + PROBLEMY_LETOSNI = 'letosni' + PROBLEMY_CHOICES = [ + (PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga + (PROBLEMY_LETOSNI, 'Všechny letošní'), + # TODO: *hlavní problémy, možná všechny... + # XXX: Chtělo by to i "aktuálně zadané... + ] + + # TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)? + + + @classmethod + def gen_terminy(cls, rocnik=None): + import datetime + from time import strftime + + from django.db.utils import OperationalError + try: + aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik + aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo + except OperationalError: + # django.db.utils.OperationalError: no such table: seminar_nastaveni + # Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál + logger = logging.getLogger(__name__) + logger.error("Rozbitá databáze (před počátečními migracemi?)") + return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')] + + # FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš. + if rocnik is not None: + aktualni_rocnik = rocnik + aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last() + + result = [] + + for cislo in m.Cislo.objects.filter( + rocnik=aktualni_rocnik, + poradi__lte=aktualni_cislo.poradi, + ).reverse(): # Standardně se řadí od nejnovějšího čísla + # Předem je mi líto kohokoliv, kdo tyhle řádky bude číst... + if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()), + f"Vydání {cislo.poradi}. čísla")) + if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()), + f"Předdeadline {cislo.poradi}. čísla")) + if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()), + f"Sous. deadline {cislo.poradi}. čísla")) + if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()), + f"Finální deadline {cislo.poradi}. čísla")) + result.append(( + strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes")) + + return result + + @classmethod + def gen_initial(cls, rocnik=None): + terminy = cls.gen_terminy(rocnik) + initial = { + 'resitele': cls.RESITELE_RELEVANTNI, + 'problemy': cls.PROBLEMY_MOJE, + # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… + 'reseni_od': terminy[-2] if rocnik is None else terminy[0], + 'reseni_do': terminy[-1], + 'neobodovane': False, + } + return initial + + def __init__(self, *args, rocnik=None, **kwargs): + if 'initial' not in kwargs: + super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs) + else: + super().__init__(*args, **kwargs) + # choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem... + # A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat... + self.terminy = self.gen_terminy(rocnik) + self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik)) + # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… + self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0] + self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik)) + self.fields['reseni_do'].initial = self.terminy[-1] + + # NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views... + resitele = forms.ChoiceField(choices=RESITELE_CHOICES) + problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES) + + reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) + reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) + neobodovane = forms.BooleanField(required=False) diff --git a/odevzdavatko/migrations/__init__.py b/odevzdavatko/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seminar/static/seminar/cross.png b/odevzdavatko/static/odevzdavatko/cross.png similarity index 100% rename from seminar/static/seminar/cross.png rename to odevzdavatko/static/odevzdavatko/cross.png diff --git a/seminar/static/seminar/dynamic_formsets.js b/odevzdavatko/static/odevzdavatko/dynamic_formsets.js similarity index 100% rename from seminar/static/seminar/dynamic_formsets.js rename to odevzdavatko/static/odevzdavatko/dynamic_formsets.js diff --git a/seminar/static/seminar/plus.png b/odevzdavatko/static/odevzdavatko/plus.png similarity index 100% rename from seminar/static/seminar/plus.png rename to odevzdavatko/static/odevzdavatko/plus.png diff --git a/seminar/templates/seminar/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html similarity index 92% rename from seminar/templates/seminar/odevzdavatko/detail.html rename to odevzdavatko/templates/odevzdavatko/detail.html index 04e8c550..35f5c8c5 100644 --- a/seminar/templates/seminar/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -4,7 +4,7 @@ {% block content %} -{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #} +{# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #} + {% endblock %} {% block content %} diff --git a/seminar/templates/seminar/org/vloz_reseni.html b/odevzdavatko/templates/odevzdavatko/posli_reseni.html similarity index 88% rename from seminar/templates/seminar/org/vloz_reseni.html rename to odevzdavatko/templates/odevzdavatko/posli_reseni.html index fa537cd9..2a6a2566 100644 --- a/seminar/templates/seminar/org/vloz_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/posli_reseni.html @@ -3,7 +3,6 @@ {% block script %} {{form.media}} - {% endblock %} {% block content %} diff --git a/seminar/templates/seminar/odevzdavatko/resitel_prehled.html b/odevzdavatko/templates/odevzdavatko/prehled_reseni.html similarity index 100% rename from seminar/templates/seminar/odevzdavatko/resitel_prehled.html rename to odevzdavatko/templates/odevzdavatko/prehled_reseni.html diff --git a/seminar/templates/seminar/odevzdavatko/seznam.html b/odevzdavatko/templates/odevzdavatko/seznam.html similarity index 100% rename from seminar/templates/seminar/odevzdavatko/seznam.html rename to odevzdavatko/templates/odevzdavatko/seznam.html diff --git a/seminar/templates/seminar/odevzdavatko/tabulka.html b/odevzdavatko/templates/odevzdavatko/tabulka.html similarity index 97% rename from seminar/templates/seminar/odevzdavatko/tabulka.html rename to odevzdavatko/templates/odevzdavatko/tabulka.html index a5e3c350..f7a00a76 100644 --- a/seminar/templates/seminar/odevzdavatko/tabulka.html +++ b/odevzdavatko/templates/odevzdavatko/tabulka.html @@ -4,7 +4,7 @@ {% block content %} -