From a0edc1e0a1b2808a86d1878a795816edb47e865a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 11 May 2021 20:22:49 +0200 Subject: [PATCH 01/22] =?UTF-8?q?Kostra=20formul=C3=A1=C5=99e=20na=20dodat?= =?UTF-8?q?e=C4=8Dn=C3=A9=20vyroben=C3=AD=20u=C5=BEivatele?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/registrace.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 seminar/views/registrace.py diff --git a/seminar/views/registrace.py b/seminar/views/registrace.py new file mode 100644 index 00000000..26e9056e --- /dev/null +++ b/seminar/views/registrace.py @@ -0,0 +1,67 @@ +""" +Registrace uživatelů k existujícím osobám + +V tomto souboru bude asi všechno, co je relevantní (kromě template), protože to +je dostatečně malá a jednorázová věc. + +Proto všechno kromě view začíná podtržítkem, aby se to nenatáhlo jako součást +seminar.views +""" + +#TODO: Logování (tohle logovat chce skoro určitě) + +from django.forms import Form +from django.views.generic.edit import FormView +import hmac + +from django.conf.settings import AUTH_USER_MODEL, SECRET_KEY +from seminar.models import Osoba + +class DodatecnaRegistraceUzivateleView(FormView): + form = _RegistraceUzivateleForm + template_name = ... + success_url_pattern = ... + + def form_valid(self, form): + pass + +class KontrolaUdajuASouhlasyView(FormView): + ... + +class _RegistraceUzivateleForm(Form): + #model = AUTH_USER_MODEL + # Zkopírováno z přihlášky :-) + username = forms.CharField(label='Přihlašovací jméno', + max_length=256, + required=True, + help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři') + password = forms.CharField( + label='Heslo', + max_length=256, + required=True, + widget=forms.PasswordInput()) + password_check = forms.CharField( + label='Ověření hesla', + max_length=256, + required=True, + widget=forms.PasswordInput()) + + # Dodatečné fieldy + token… + token = ... + verifikace_TODO = ... # TODO: Co verifikovat + + # TODO: clean_username, verifikace … + + +# Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři. + +def _gen_token(usecase: str, data: dict[str, object]): + ... + +def _verify_token(usecase: str, data: dict[str, object]): + ... + +def _invite(osoba: Osoba): + """ + Pošle dané osobě e-mail s odkazem na registraci. + """ From c9f0d60e7cd0e54f65317a7f483594eb7939dd21 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Wed, 12 May 2021 01:48:43 +0200 Subject: [PATCH 02/22] =?UTF-8?q?J=C3=A1dro=20implementace=20view=20na=20d?= =?UTF-8?q?oregistraci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/registrace.py | 136 +++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 19 deletions(-) diff --git a/seminar/views/registrace.py b/seminar/views/registrace.py index 26e9056e..6e1472fb 100644 --- a/seminar/views/registrace.py +++ b/seminar/views/registrace.py @@ -4,32 +4,119 @@ Registrace uživatelů k existujícím osobám V tomto souboru bude asi všechno, co je relevantní (kromě template), protože to je dostatečně malá a jednorázová věc. -Proto všechno kromě view začíná podtržítkem, aby se to nenatáhlo jako součást -seminar.views +Importovat prosím jen ty dva Views, ve výjimečných případech invite, nic dalšího. """ #TODO: Logování (tohle logovat chce skoro určitě) +#TODO: Omezení počtu pokusů (per token -- města / údaje se bruteforcit dají, na rozdíl od tokenů) +from enum import Enum +from django import forms +from django.contrib.auth.models import User, Permission from django.forms import Form -from django.views.generic.edit import FormView +from django.views import View +from django.views.generic.base import TemplateResponseMixin import hmac +from typing import Optional -from django.conf.settings import AUTH_USER_MODEL, SECRET_KEY +from django.conf.settings import SECRET_KEY from seminar.models import Osoba -class DodatecnaRegistraceUzivateleView(FormView): - form = _RegistraceUzivateleForm +# Složitější class-based views mi neumožňují vracet chyby. +class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): template_name = ... - success_url_pattern = ... - - def form_valid(self, form): - pass + form = RegistraceUzivateleForm + + def get(self, request, url_token): + # Ověřit token + tok_data = verify_token(UseCase.email, url_token) + if tok_data is None: + return render_to_response( + context={ + 'error': 'Token není platný', + }, + status_code=400, + ) + + # Zkontrolovat, že to není moc staré poslání + now = datetime.now() + token_generation = datetime.fromisoformat(tok_data['timestamp']) + delta = now - token_generation + if delta >= timedelta(weeks=10): + return render_to_response( + context={ + 'error': 'Vypršela časová platnost tokenu', + }, + status_code=400, + ) + + + # Najít osobu + osoba_id = int(tok_data['osoba']) # Pokud tam není, tak jsme vygenerovali špatný token my (byl validní). + osoba = m.Osoba.objects.get(id=osoba_id) + if osoba.user is not None: + return render_to_response( + context={ + 'error': 'Už máte uživatele', + }, + status_code=400, + ) + # Vrátit view s formulářem a formulářovým tokenem + form_token = gen_token(UseCase.form, data={ + 'osoba': str(osoba_id), + 'timestamp': datetime.now().isoformat(), + }) + return render_to_response( + context={ + 'form': self.form(initial={ + 'token': form_token, + }), + } + ) + + def post(self, request, url_token): + # Zkontrolovat formulář + form = self.form(self.request.POST) + if not form.is_valid(): + return render_to_response( + context={ + 'error': 'Chyba ve formuláři', # TODO: Umíme dostat konkrétní detaily? + # TODO: Formulář pro zkusení znovu? + }, + status_code=400, + ) + + form_data = form.cleaned_data + # Zkontrolovat token + token_data = verify_token(UseCase.form, form_data['token']) + if token_data is None: + return render_to_response( + context={ + 'error': 'Neplatný token', + }, + status_code=400, + ) + osoba_id = int(token_data['osoba']) + osoba = m.Osoba.objects.get(id=osoba_id) + # Zkontrolovat verifikační field + ... + # Vyrobit uživatele + u = User.objects.create_user( + username=form_data['username'], + password=form_data['password'], + email=osoba.email, + #first_name=o.jmeno, + #last_name=o.prijmeni, + ) + u.user_permissions.add(Permission.objects.get(codename__exact='resitel')) + # Přesměrovat na kontrolu údajů + return ... class KontrolaUdajuASouhlasyView(FormView): ... -class _RegistraceUzivateleForm(Form): - #model = AUTH_USER_MODEL +class RegistraceUzivateleForm(Form): + #model = User # Zkopírováno z přihlášky :-) username = forms.CharField(label='Přihlašovací jméno', max_length=256, @@ -47,21 +134,32 @@ class _RegistraceUzivateleForm(Form): widget=forms.PasswordInput()) # Dodatečné fieldy + token… - token = ... + token = forms.CharField(widget=forms.HiddenInput(), required=True) verifikace_TODO = ... # TODO: Co verifikovat # TODO: clean_username, verifikace … +def invite(osoba: Osoba): + """ + Pošle dané osobě e-mail s odkazem na registraci. + """ + ... # Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři. +class UseCase(Enum): + email = 'email' + form = 'form' -def _gen_token(usecase: str, data: dict[str, object]): +# Token kóduje všechno v sobě +def gen_token(usecase: _UseCase, data: dict[str, str]) -> str: ... -def _verify_token(usecase: str, data: dict[str, object]): - ... - -def _invite(osoba: Osoba): +def verify_token(usecase: _UseCase, token: str) -> Optional[dict[str, str]]: """ - Pošle dané osobě e-mail s odkazem na registraci. + Vrací slovník dat, pokud je token validní, jinak None. + + Inspirováno OSMO, je díky tomu jednoduché zároveň předat dekódovaná data a + výsledek ověření. """ + ... + From 1d19f48ac7aa2d9d13649e657fccd93be5cb442a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Wed, 12 May 2021 01:49:34 +0200 Subject: [PATCH 03/22] =?UTF-8?q?Rename=20souboru=20s=20doregistrac=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/{registrace.py => doregistrace.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename seminar/views/{registrace.py => doregistrace.py} (100%) diff --git a/seminar/views/registrace.py b/seminar/views/doregistrace.py similarity index 100% rename from seminar/views/registrace.py rename to seminar/views/doregistrace.py From a11d7d011cf4049e55f98e7ffc9d6448a35abd30 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 21:45:20 +0200 Subject: [PATCH 04/22] =?UTF-8?q?Implementov=C3=A1na=20validace=20token?= =?UTF-8?q?=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementoval jsem to od stolu, není to vyzkoušené. --- seminar/views/doregistrace.py | 46 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 6e1472fb..39fbff74 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -10,6 +10,7 @@ Importovat prosím jen ty dva Views, ve výjimečných případech invite, nic d #TODO: Logování (tohle logovat chce skoro určitě) #TODO: Omezení počtu pokusů (per token -- města / údaje se bruteforcit dají, na rozdíl od tokenů) +from datetime import datetime from enum import Enum from django import forms from django.contrib.auth.models import User, Permission @@ -38,9 +39,11 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): status_code=400, ) + osoba_id, tok_timestamp = tok_data # Token prostě vypadá takhle, je to zafixované. Pokud tak nevypadá, je něco moc špatně. + # Zkontrolovat, že to není moc staré poslání now = datetime.now() - token_generation = datetime.fromisoformat(tok_data['timestamp']) + token_generation = datetime.fromisoformat(tok_timestamp) delta = now - token_generation if delta >= timedelta(weeks=10): return render_to_response( @@ -52,7 +55,6 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): # Najít osobu - osoba_id = int(tok_data['osoba']) # Pokud tam není, tak jsme vygenerovali špatný token my (byl validní). osoba = m.Osoba.objects.get(id=osoba_id) if osoba.user is not None: return render_to_response( @@ -62,10 +64,7 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): status_code=400, ) # Vrátit view s formulářem a formulářovým tokenem - form_token = gen_token(UseCase.form, data={ - 'osoba': str(osoba_id), - 'timestamp': datetime.now().isoformat(), - }) + form_token = gen_token(UseCase.form, osoba=osoba_id, timestamp=datetime.now()) return render_to_response( context={ 'form': self.form(initial={ @@ -151,15 +150,36 @@ class UseCase(Enum): form = 'form' # Token kóduje všechno v sobě -def gen_token(usecase: _UseCase, data: dict[str, str]) -> str: - ... - -def verify_token(usecase: _UseCase, token: str) -> Optional[dict[str, str]]: +# Tokenové metody zároveň řeší konzistentní zakódování dat do tokenu, takže už to nemusí řešit nikdo jiný. +HMAC_FUNCTION='sha-256' +def gen_token(usecase: _UseCase, *, osoba_id: int, timestamp: datetime) -> str: + strmsg = '@'.join([usecase.value, str(osoba_id), timestamp.isoformat()]) + msg = bytes(strmsg, 'utf-8') + key = bytes(SECRET_KEY, 'utf-8') + mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest() + return mac+'@'+msg + +def verify_token(usecase: _UseCase, token: str) -> Optional[Tuple[int, datetime]]: """ - Vrací slovník dat, pokud je token validní, jinak None. + Vrací dvojici dat, pokud je token validní, jinak None. Inspirováno OSMO, je díky tomu jednoduché zároveň předat dekódovaná data a výsledek ověření. """ - ... - + try: + tok_mac, uc, tok_osoba, tok_ts = token.split('@') + except ValueError: + # Nepodařilo se rozbít na právě čtyři části, takže je token špatně. + return None + strmsg = '@'.join([usecase.value, tok_osoba, tok_ts]) + msg = bytes(strmsg, 'utf-8') + key = bytes(SECRET_KEY, 'utf-8') + valid_mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest() + + if hmac.compare_digest(tok_mac, valid_mac): + # HMAC sedí, takže data jsou v pořádku + osoba_id = int(tok_osoba) + ts = datetime.fromisoformat(tok_ts) + return (osoba_id, ts) + else: + return None From b42d18f0a47bfc84fa902cc7ec7de925f878667e Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 21:46:49 +0200 Subject: [PATCH 05/22] + TODO :-) --- seminar/views/doregistrace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 39fbff74..d33aae53 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -144,6 +144,8 @@ def invite(osoba: Osoba): """ ... +# TODO: Testy na tokeny? + # Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři. class UseCase(Enum): email = 'email' From 0df3cb8509314d1aac099d2b47aa3731321bc115 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 22:44:26 +0200 Subject: [PATCH 06/22] =?UTF-8?q?Kontrola=20PS=C4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/doregistrace.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index d33aae53..9331349f 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -97,8 +97,20 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): ) osoba_id = int(token_data['osoba']) osoba = m.Osoba.objects.get(id=osoba_id) + # Zkontrolovat verifikační field - ... + kanonicke_psc = lambda x : ''.join(filter(str.isdigit, x)) + psc = kanonicke_psc(form_data['PSC_verifikace']) + realne_psc = kanonicke_psc(osoba.psc) + # Much crypto, very secure :-) + if not hmac.compare_digest(psc, realne_psc): + return render_to_response( + context={ + 'error': 'Nesedí verifikační pole', + }, + status_code=400, + ) + # Vyrobit uživatele u = User.objects.create_user( username=form_data['username'], @@ -134,7 +146,9 @@ class RegistraceUzivateleForm(Form): # Dodatečné fieldy + token… token = forms.CharField(widget=forms.HiddenInput(), required=True) - verifikace_TODO = ... # TODO: Co verifikovat + # TODO: Tohle asi blízký útočník (e.g. zapomenuté přihlášení na školním počítači) umí odhalit. + PSC_verifikace = forms.CharField(max_length=8, required=True, label="PSČ tvého bydliště", + help_text="Chceme si jen ověřit, že se někdo cizí nezmocnil tvého odkazu") # TODO: clean_username, verifikace … From 4028ceb5b90afb637b9ac3f88ef0618aa427e1c1 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 25 May 2021 19:28:13 +0200 Subject: [PATCH 07/22] =?UTF-8?q?Dal=C5=A1=C3=AD=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/doregistrace.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 9331349f..c495c1ba 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -120,6 +120,9 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): #last_name=o.prijmeni, ) u.user_permissions.add(Permission.objects.get(codename__exact='resitel')) + + # Poslat kontrolní e-mail, že registrace proběhla (a že pokud se to nemělo stát, tak ať napíšou) + ... # Přesměrovat na kontrolu údajů return ... From cbed53c65f3d8c206280262d4624a1fc5bf1215b Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 2 Nov 2021 00:11:22 +0100 Subject: [PATCH 08/22] =?UTF-8?q?checklinks:=20Pou=C5=BEit=C3=AD,=20vyps?= =?UTF-8?q?=C3=A1n=C3=AD=20v=C3=BDstupu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checklinks.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/checklinks.sh b/checklinks.sh index 63898075..19557e3a 100755 --- a/checklinks.sh +++ b/checklinks.sh @@ -3,8 +3,16 @@ # Props to https://www.commandlinefu.com/commands/view/8234/check-broken-links-using-wget-as-a-spider set -eu +if test "$#" -lt 1; then + echo >&2 "Usage: $0 " + exit 1 +fi + logfile="$(pwd)/wget.log.$(date +%FT%T)" tmp=$(mktemp --directory) cd "$tmp" wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" + +echo "Result: (a last few lines of the file $logfile)" +sed -ne '/^Found [0-9]* broken links/,$ p' "$logfile" From 542ae6b7efc1f230d1b45d0e2b2c2490a3d263c6 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 2 Nov 2021 00:22:03 +0100 Subject: [PATCH 09/22] =?UTF-8?q?checklinks:=20Ignorujeme,=20=C5=BEe=20wge?= =?UTF-8?q?t=20neskon=C4=8D=C3=AD=20=C3=BAsp=C4=9B=C5=A1n=C4=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checklinks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checklinks.sh b/checklinks.sh index 19557e3a..71b218f2 100755 --- a/checklinks.sh +++ b/checklinks.sh @@ -12,7 +12,7 @@ logfile="$(pwd)/wget.log.$(date +%FT%T)" tmp=$(mktemp --directory) cd "$tmp" -wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" +wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" || true # wget nejspíš skončí s chybou, že něco nestáhl… echo "Result: (a last few lines of the file $logfile)" sed -ne '/^Found [0-9]* broken links/,$ p' "$logfile" From 223c4054f6f2c20f7219f912898d1efc7b87a825 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 2 Nov 2021 12:13:28 +0100 Subject: [PATCH 10/22] =?UTF-8?q?Zru=C5=A1en=20mgmt=20p=C5=99=C3=ADkaz=20"?= =?UTF-8?q?auth"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nedělal nic objevného a navíc nefungoval, bo byl v Pythonu 2. Náhrada se nezřizuje, webař se podivá do databáze nebo těch pár řádek napíše do djangoshellu ručně. Stejně se to prakticky nepoužívá… --- seminar/management/commands/auth.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 seminar/management/commands/auth.py diff --git a/seminar/management/commands/auth.py b/seminar/management/commands/auth.py deleted file mode 100644 index 71757418..00000000 --- a/seminar/management/commands/auth.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.core.management.base import BaseCommand -from django.contrib.sessions.models import Session -from django.contrib.auth.models import User - -class Command(BaseCommand): - u"""Vypiš username přihlášeného orga s daným session_key. - - Příkaz pro manage.py, který ze vstupu přečte session_key (tak, jak je - uložen v cookie sessionid) a pokud session existuje a příslušný přihlášený - uživatel má právo přihlásit se do admina, vypíše jeho username. - """ - def handle(self, *args, **options): - session_key = raw_input() - s = Session.objects.get(pk=session_key).get_decoded() - user_id = s['_auth_user_id'] - user = User.objects.get(pk=user_id) - if user.is_staff: - print(user.username) From 843c00dd604f256cebe878d49288cf21cc33dd3a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Thu, 4 Nov 2021 15:29:33 +0100 Subject: [PATCH 11/22] =?UTF-8?q?Galerie:=20None=20se=20porovn=C3=A1v?= =?UTF-8?q?=C3=A1=20p=C5=99es=20is,=20ne=20rovn=C3=ADtkem.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- galerie/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galerie/views.py b/galerie/views.py index 4570b8fb..f0d9b53b 100644 --- a/galerie/views.py +++ b/galerie/views.py @@ -24,7 +24,7 @@ def zobrazit(galerie, request): def cesta_od_korene(g): """Vrátí seznam galerií od kořene ke g""" cesta = [] - while g != None: + while g is not None: cesta.append(g) g = g.galerie_up return reversed(cesta) @@ -53,7 +53,7 @@ def nahled(request, pk, soustredeni): for g in sourozenci: if g.pk == galerie.pk: predchozi = minuly - if minuly != None and minuly.pk == galerie.pk: + if minuly is not None and minuly.pk == galerie.pk: nasledujici = g break minuly = g From 3b27b87e35bc9cb8467a218115a8f12440cb64f7 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Thu, 4 Nov 2021 15:30:29 +0100 Subject: [PATCH 12/22] =?UTF-8?q?Korektury:=20migr=200011=20smaz=C3=A1n=20?= =?UTF-8?q?nop.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0011_prevod_autora_z_charField_na_Organizator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/korektury/migrations/0011_prevod_autora_z_charField_na_Organizator.py b/korektury/migrations/0011_prevod_autora_z_charField_na_Organizator.py index 9f64bbd5..53082df9 100644 --- a/korektury/migrations/0011_prevod_autora_z_charField_na_Organizator.py +++ b/korektury/migrations/0011_prevod_autora_z_charField_na_Organizator.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.db import migrations, models def transform_autor(apps, schema_editor): - print Organizator = apps.get_model('seminar', 'Organizator') # preorgovani oprav From 5e07412d92facc29837f7ec7c3afe12cfd619a32 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 00:55:42 +0100 Subject: [PATCH 13/22] =?UTF-8?q?Ovvpfile:=20p=C5=99eveden=C3=AD=20mezer?= =?UTF-8?q?=20na=20taby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aesop/ovvpfile.py | 128 +++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/aesop/ovvpfile.py b/aesop/ovvpfile.py index 471c06c6..72150fae 100644 --- a/aesop/ovvpfile.py +++ b/aesop/ovvpfile.py @@ -1,81 +1,81 @@ # -*- coding: utf-8 -*- try: - from django.http import HttpResponse - from django.utils.encoding import force_text + from django.http import HttpResponse + from django.utils.encoding import force_text except: - force_text = str + force_text = str class OvvpFile(object): - def __init__(self): - # { header: value, ... } - self.headers = {} - # [ 'column-name', ... ] - self.columns = [] - # [ { column: value, ...}, ...] - self.rows = [] + def __init__(self): + # { header: value, ... } + self.headers = {} + # [ 'column-name', ... ] + self.columns = [] + # [ { column: value, ...}, ...] + self.rows = [] - def to_lines(self): - # header - for hk in sorted(self.headers.keys()): - yield '%s\t%s\n' % (hk, self.headers[hk]) - yield '\n' - # columns - yield '\t'.join([c for c in self.columns]) + '\n' - # rows - for r in self.rows: - yield '\t'.join([force_text(r[c]) for c in self.columns]) + '\n' + def to_lines(self): + # header + for hk in sorted(self.headers.keys()): + yield '%s\t%s\n' % (hk, self.headers[hk]) + yield '\n' + # columns + yield '\t'.join([c for c in self.columns]) + '\n' + # rows + for r in self.rows: + yield '\t'.join([force_text(r[c]) for c in self.columns]) + '\n' - def to_string(self): - return ''.join(self.to_lines()) + def to_string(self): + return ''.join(self.to_lines()) - def to_HttpResponse(self): - return HttpResponse(self.to_string(), content_type='text/plain; charset=utf-8') + def to_HttpResponse(self): + return HttpResponse(self.to_string(), content_type='text/plain; charset=utf-8') - def parse_from(self, source, with_headers=True): - "Parse data from file, string or line iterator, overwriting self" - if isinstance(source, str) or isinstance(source, unicode): - return self.parse_from(source.split('\n')) - - it = iter(source) + def parse_from(self, source, with_headers=True): + "Parse data from file, string or line iterator, overwriting self" + if isinstance(source, str) or isinstance(source, unicode): + return self.parse_from(source.split('\n')) + + it = iter(source) - # header - self.headers = {} - if with_headers: - for r in it: - if isinstance(r, str): - r = r.decode('utf8') - assert isinstance(r, unicode) - r = r.rstrip('\n') - if r == u"": - break - k, v = r.split(u'\t', 1) - self.headers[k] = v + # header + self.headers = {} + if with_headers: + for r in it: + if isinstance(r, str): + r = r.decode('utf8') + assert isinstance(r, unicode) + r = r.rstrip('\n') + if r == u"": + break + k, v = r.split(u'\t', 1) + self.headers[k] = v - # columns - r = it.next() - if isinstance(r, str): - r = r.decode('utf8') - self.columns = [cn.strip() for cn in r.split(u'\t') if cn.strip() != ""] + # columns + r = it.next() + if isinstance(r, str): + r = r.decode('utf8') + self.columns = [cn.strip() for cn in r.split(u'\t') if cn.strip() != ""] - # rows - self.rows = [] - for r in it: - if isinstance(r, str): - r = r.decode('utf8') - r = r.rstrip('\n') - if not r: - break - rtup = r.split(u'\t') - rdict = {} - for ci in range(len(self.columns)): - rdict[self.columns[ci]] = rtup[ci] - self.rows.append(rdict) - + # rows + self.rows = [] + for r in it: + if isinstance(r, str): + r = r.decode('utf8') + r = r.rstrip('\n') + if not r: + break + rtup = r.split(u'\t') + rdict = {} + for ci in range(len(self.columns)): + rdict[self.columns[ci]] = rtup[ci] + self.rows.append(rdict) + def parse(source, with_headers=True): - o = OvvpFile() - o.parse_from(source, with_headers=with_headers) - return o + o = OvvpFile() + o.parse_from(source, with_headers=with_headers) + return o From 0f381a75295964a4537016b30826434c3716a3de Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 01:08:23 +0100 Subject: [PATCH 14/22] =?UTF-8?q?Ovvpfile:=20prot=C5=99=C3=ADd=C4=9Bn?= =?UTF-8?q?=C3=AD=20mrtv=C3=A9ho=20k=C3=B3du?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jestli jsem to rozbil, tak to revertněte. Ale myslím, že jsem fakt smazal jen kód, který nemohl jít spustit… --- aesop/ovvpfile.py | 59 ++++------------------------------------------- 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/aesop/ovvpfile.py b/aesop/ovvpfile.py index 72150fae..a9d67c43 100644 --- a/aesop/ovvpfile.py +++ b/aesop/ovvpfile.py @@ -1,13 +1,8 @@ -# -*- coding: utf-8 -*- +from django.http import HttpResponse +from django.utils.encoding import force_text -try: - from django.http import HttpResponse - from django.utils.encoding import force_text -except: - force_text = str - -class OvvpFile(object): +class OvvpFile: def __init__(self): # { header: value, ... } self.headers = {} @@ -30,52 +25,6 @@ class OvvpFile(object): def to_string(self): return ''.join(self.to_lines()) + # Pozn: tohle je ta jediná funkce, která se reálně používá… def to_HttpResponse(self): return HttpResponse(self.to_string(), content_type='text/plain; charset=utf-8') - - def parse_from(self, source, with_headers=True): - "Parse data from file, string or line iterator, overwriting self" - if isinstance(source, str) or isinstance(source, unicode): - return self.parse_from(source.split('\n')) - - it = iter(source) - - # header - self.headers = {} - if with_headers: - for r in it: - if isinstance(r, str): - r = r.decode('utf8') - assert isinstance(r, unicode) - r = r.rstrip('\n') - if r == u"": - break - k, v = r.split(u'\t', 1) - self.headers[k] = v - - # columns - r = it.next() - if isinstance(r, str): - r = r.decode('utf8') - self.columns = [cn.strip() for cn in r.split(u'\t') if cn.strip() != ""] - - # rows - self.rows = [] - for r in it: - if isinstance(r, str): - r = r.decode('utf8') - r = r.rstrip('\n') - if not r: - break - rtup = r.split(u'\t') - rdict = {} - for ci in range(len(self.columns)): - rdict[self.columns[ci]] = rtup[ci] - self.rows.append(rdict) - - - -def parse(source, with_headers=True): - o = OvvpFile() - o.parse_from(source, with_headers=with_headers) - return o From 5a98bc61ac6abe9c15ff6a154aee271490a666f0 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 01:14:07 +0100 Subject: [PATCH 15/22] =?UTF-8?q?Ovvpfile:=20Trivi=C3=A1ln=C3=AD=20=C3=BAp?= =?UTF-8?q?ravy=20k=C3=B3du?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pylintu se to nelíbilo a mně dávalo smysl to opravit, teď je to snad čitelnější… --- aesop/ovvpfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aesop/ovvpfile.py b/aesop/ovvpfile.py index a9d67c43..4d58af35 100644 --- a/aesop/ovvpfile.py +++ b/aesop/ovvpfile.py @@ -14,10 +14,10 @@ class OvvpFile: def to_lines(self): # header for hk in sorted(self.headers.keys()): - yield '%s\t%s\n' % (hk, self.headers[hk]) + yield f'{hk}\t{self.headers[hk]}\n' yield '\n' # columns - yield '\t'.join([c for c in self.columns]) + '\n' + yield '\t'.join(self.columns) + '\n' # rows for r in self.rows: yield '\t'.join([force_text(r[c]) for c in self.columns]) + '\n' From c37f9fbab4fd674ad3bb89c9518334d3cbcad172 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Wed, 19 Aug 2020 03:43:21 +0200 Subject: [PATCH 16/22] =?UTF-8?q?P=C5=99id=C3=A1na=20slo=C5=BEka=20se=20v?= =?UTF-8?q?=C5=A1emi=20konfigur=C3=A1ky=20k=20rozb=C4=9Bhnut=C3=AD=20webu?= =?UTF-8?q?=20na=20serveru?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/README | 3 ++ setup/nginx/mam-test.ks.matfyz.cz | 43 ++++++++++++++++ setup/nginx/mam.mff.cuni.cz | 51 +++++++++++++++++++ setup/systemd/mamweb-prod.service | 10 ++++ setup/systemd/mamweb-test.service | 10 ++++ .../uwsgi/mamweb_prod.ini | 0 .../uwsgi/mamweb_test.ini | 0 7 files changed, 117 insertions(+) create mode 100644 setup/README create mode 100644 setup/nginx/mam-test.ks.matfyz.cz create mode 100644 setup/nginx/mam.mff.cuni.cz create mode 100644 setup/systemd/mamweb-prod.service create mode 100644 setup/systemd/mamweb-test.service rename mamweb_prod.ini => setup/uwsgi/mamweb_prod.ini (100%) rename mamweb_test.ini => setup/uwsgi/mamweb_test.ini (100%) diff --git a/setup/README b/setup/README new file mode 100644 index 00000000..3b736cdc --- /dev/null +++ b/setup/README @@ -0,0 +1,3 @@ +Tato složka obsahuje různé konfiguráky potřebné k rozběhnutí webu na serveru. + +TODO: Napsat sem i přehled toho, jak to funguje. diff --git a/setup/nginx/mam-test.ks.matfyz.cz b/setup/nginx/mam-test.ks.matfyz.cz new file mode 100644 index 00000000..27e90979 --- /dev/null +++ b/setup/nginx/mam-test.ks.matfyz.cz @@ -0,0 +1,43 @@ +server { + listen 195.113.20.177:80; + listen [2001:718:1e03:801::b1]:80; + server_name mam-test.ks.matfyz.cz; + return 301 https://$server_name$request_uri; + +} +server { + # SSL configuration + listen 195.113.20.177:443 ssl; + listen [2001:718:1e03:801::b1]:443 ssl; + + # SSL keys + ssl on; + ssl_certificate /etc/letsencrypt/live/mam-test.ks.matfyz.cz/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/mam-test.ks.matfyz.cz/privkey.pem; # managed by Certbot + ssl_dhparam /etc/ssl/dhparams.pem; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + server_name mam-test.ks.matfyz.cz; + + client_max_body_size 50M; + + auth_basic "MaMweb test - access restricted"; + auth_basic_user_file /akce/mam/www/mamweb-test/.htpasswd; + + location /static/ { + root /akce/mam/www/mamweb-test/; + } + + location /media/ { + root /akce/mam/www/mamweb-test/; + } + + location / { try_files $uri @mamweb_test; } + + location @mamweb_test { + include uwsgi_params; + uwsgi_pass unix:/tmp/uwsgi-mamweb_test.sock; + } + +} diff --git a/setup/nginx/mam.mff.cuni.cz b/setup/nginx/mam.mff.cuni.cz new file mode 100644 index 00000000..08996adf --- /dev/null +++ b/setup/nginx/mam.mff.cuni.cz @@ -0,0 +1,51 @@ + +server { + listen 195.113.20.177:80; + listen [2001:718:1e03:801::b1]:80; + server_name mam.mff.cuni.cz; + return 301 https://$server_name$request_uri; + +} +server { + # SSL configuration + # + listen 195.113.20.177:443 ssl; + listen [2001:718:1e03:801::b1]:443 ssl; + + # SSL keys + ssl on; + ssl_certificate /etc/ssl/domains/mam.mff.cuni.cz/bundle.pem; + ssl_certificate_key /etc/ssl/domains/mam.mff.cuni.cz/privkey.pem; + ssl_dhparam /etc/ssl/dhparams.pem; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + + server_name mam.mff.cuni.cz; + # server_name mamweb.bezva.org; + + client_max_body_size 50M; + + location /static/ { + root /akce/mam/www/mamweb-prod/; + } + + location /media/ { + root /akce/mam/www/mamweb-prod/; + } + + location /wiki/ { + proxy_pass http://127.0.0.1:5001/; + proxy_set_header X-Real_IP $remote_addr; + proxy_redirect off; + sub_filter 'href="/' 'href=/wiki/'; + sub_filter 'src="/' 'src=/wiki/'; + } + + location / { try_files $uri @mamweb_prod; } + + location @mamweb_prod { + include uwsgi_params; + uwsgi_pass unix:/tmp/uwsgi-mamweb_prod.sock; + } +} diff --git a/setup/systemd/mamweb-prod.service b/setup/systemd/mamweb-prod.service new file mode 100644 index 00000000..89af2a5c --- /dev/null +++ b/setup/systemd/mamweb-prod.service @@ -0,0 +1,10 @@ +[Unit] +Description=uWSGI instance to serve mam.mff.cuni.cz +After=network.target + +[Service] +WorkingDirectory=/akce/mam/www/mamweb-prod +ExecStart=/usr/bin/uwsgi --ini mamweb_prod.ini + +[Install] +WantedBy=default.target diff --git a/setup/systemd/mamweb-test.service b/setup/systemd/mamweb-test.service new file mode 100644 index 00000000..616605bb --- /dev/null +++ b/setup/systemd/mamweb-test.service @@ -0,0 +1,10 @@ +[Unit] +Description=uWSGI instance to serve mam-test.kam.mff.cuni.cz +After=network.target + +[Service] +WorkingDirectory=/akce/mam/www/mamweb-test +ExecStart=/usr/bin/uwsgi --ini mamweb_test.ini + +[Install] +WantedBy=default.target diff --git a/mamweb_prod.ini b/setup/uwsgi/mamweb_prod.ini similarity index 100% rename from mamweb_prod.ini rename to setup/uwsgi/mamweb_prod.ini diff --git a/mamweb_test.ini b/setup/uwsgi/mamweb_test.ini similarity index 100% rename from mamweb_test.ini rename to setup/uwsgi/mamweb_test.ini From abf493afe8cee819b4d6b3b174d10b282a603fdc Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 01:35:21 +0100 Subject: [PATCH 17/22] =?UTF-8?q?Update=20konfigur=C3=A1k=C5=AF=20Nginxu?= =?UTF-8?q?=20podle=20aktu=C3=A1ln=C3=ADho=20stavu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/nginx/mam-test.ks.matfyz.cz | 7 +++++++ setup/nginx/mam.mff.cuni.cz | 20 +++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/setup/nginx/mam-test.ks.matfyz.cz b/setup/nginx/mam-test.ks.matfyz.cz index 27e90979..46f9b2ec 100644 --- a/setup/nginx/mam-test.ks.matfyz.cz +++ b/setup/nginx/mam-test.ks.matfyz.cz @@ -32,6 +32,13 @@ server { location /media/ { root /akce/mam/www/mamweb-test/; } + + location /aesop-export/ { + auth_basic "AESOP API"; + auth_basic_user_file /akce/mam/www/mamweb-test/.htpasswd-aesop; + try_files $uri @mamweb_test; + } + location / { try_files $uri @mamweb_test; } diff --git a/setup/nginx/mam.mff.cuni.cz b/setup/nginx/mam.mff.cuni.cz index 08996adf..99292a67 100644 --- a/setup/nginx/mam.mff.cuni.cz +++ b/setup/nginx/mam.mff.cuni.cz @@ -26,6 +26,12 @@ server { client_max_body_size 50M; + location /aesop-export/ { + auth_basic "AESOP API"; + auth_basic_user_file /akce/mam/www/mamweb-prod/.htpasswd-aesop; + try_files $uri @mamweb_prod; + } + location /static/ { root /akce/mam/www/mamweb-prod/; } @@ -37,9 +43,17 @@ server { location /wiki/ { proxy_pass http://127.0.0.1:5001/; proxy_set_header X-Real_IP $remote_addr; - proxy_redirect off; - sub_filter 'href="/' 'href=/wiki/'; - sub_filter 'src="/' 'src=/wiki/'; + proxy_redirect / /wiki/; + #rewrite '/' '/wiki'; + sub_filter_once off; + sub_filter 'href="/' 'href="/wiki/'; + sub_filter 'src="/' 'src="/wiki/'; + sub_filter 'action="/' 'action="/wiki/'; + # Overkill: + #sub_filter '="/' '="/wiki/'; + #sub_filter ':5001/' '/wiki/'; + #sub_filter 'Location: /' 'Location: /wiki/'; + #sub_filter '_login' '_test'; } location / { try_files $uri @mamweb_prod; } From d0a7caa0326104a1491522f9f7fdd7fc6adaa91a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 03:03:21 +0100 Subject: [PATCH 18/22] =?UTF-8?q?Utils:=20seznam=5Fproblemu=20je=20okoment?= =?UTF-8?q?ovan=C3=BD=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/seminar/utils.py b/seminar/utils.py index 898e0843..6d0a0b72 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -88,6 +88,11 @@ def from_roman(rom): def seznam_problemu(): + """Funkce pro hledání nekonzistencí v databázi a dalších nežádoucích stavů webu/databáze. + + Nijak nesouvisí s Problémy zadanými řešitelům.""" + # FIXME: přejmenovat funkci? + # FIXME: Tak, jak je napsaná, asi spíš patří někam k views a ne do utils (?) problemy = [] # Pomocna fce k formatovani problemovych hlasek From a341eba928abc7e07ac29234b5c0b294d48a5a63 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 03:30:15 +0100 Subject: [PATCH 19/22] =?UTF-8?q?Na=20=C5=98e=C5=A1en=C3=AD=20ukazuje=20FK?= =?UTF-8?q?=20u=20=C5=98e=C5=A1en=C3=ADNode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seminar/models.py b/seminar/models.py index d4c3ad81..daef62e6 100644 --- a/seminar/models.py +++ b/seminar/models.py @@ -1139,6 +1139,7 @@ class Reseni(SeminarModelBase): # má ForeignKey s: # Hodnoceni + # ReseniNode def sum_body(self): return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] From ee04a25a52893171df8a54287fd08cb9cfd1badc Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 03:31:41 +0100 Subject: [PATCH 20/22] =?UTF-8?q?Testdata:=20lep=C5=A1=C3=AD=20generov?= =?UTF-8?q?=C3=A1n=C3=AD=20rok=C5=AF=20u=20=C5=99e=C5=A1itel=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/testutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seminar/testutils.py b/seminar/testutils.py index 8fe8fe0c..b0ec5866 100644 --- a/seminar/testutils.py +++ b/seminar/testutils.py @@ -77,8 +77,8 @@ def gen_osoby(rnd, size): prezdivka = rnd.choice(prezdivky) email = "@".join([unidecode.unidecode(jmeno), rnd.choice(domain)]) telefon = "".join([str(rnd.choice([k for k in range(10)])) for i in range(9)]) - narozeni = datetime.date(rnd.randint(1980, 2020), rnd.randint(1, 12), - rnd.randint(1, 28)) + narozeni = datetime.date(rnd.randint(1980, datetime.datetime.now().year), + rnd.randint(1, 12), rnd.randint(1, 28)) ulic = rnd.choice(seznam_ulic) cp = rnd.randint(1, 99) ulice = " ".join([ulic, str(cp)]) @@ -142,7 +142,7 @@ def gen_resitele(rnd, osoby, skoly): os.save() os.user.user_permissions.add(resitel_perm) resitele.append(Resitel.objects.create(osoba=os, skola=rnd.choice(skoly), - rok_maturity=rnd.randint(2019, 2029), + rok_maturity=os.datum_narozeni.year + rnd.randint(18, 21), zasilat=rnd.choice(Resitel.ZASILAT_CHOICES)[0])) return resitele From b230953173cd139ca126afd4c2773e1a0486c9a5 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Sun, 7 Nov 2021 03:32:19 +0100 Subject: [PATCH 21/22] =?UTF-8?q?Testdata:=20TODO=20=C4=8Dl=C3=A1nky=20(je?= =?UTF-8?q?n=20koment=C3=A1=C5=99=20:-))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/testutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seminar/testutils.py b/seminar/testutils.py index b0ec5866..019f66ac 100644 --- a/seminar/testutils.py +++ b/seminar/testutils.py @@ -890,6 +890,9 @@ def create_test_data(size = 6, rnd = None): # Dohackované vytvoření jednoho článku gen_clanek(rnd, organizatori, resitele) + # TODO: přidat články včetně zařazení do struktury treenodů, + # a následně otestovat konsistency check databáze z utils.py + # pomocí stránky /stav # obecné nastavení semináře, musí být už přidané ročníky a čísla, jinak se nastaví divně nastaveni = Nastaveni.objects.create( From dd5ded0936c242b771ee0862fab5fdd83634fd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 12:05:05 +0100 Subject: [PATCH 22/22] =?UTF-8?q?Fix=20jsem=20si=20jist,=20=C5=BEe=20tady?= =?UTF-8?q?=20se=20m=C4=9Bly=20br=C3=A1t=20body=20do=20=C4=8D=C3=ADsla,=20?= =?UTF-8?q?ne=20do=20ro=C4=8Dn=C3=ADku?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/views_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 56e2aaae..9392f49a 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -891,7 +891,7 @@ def TitulyView(request, rocnik, cislo): asciijmena = [] jmenovci = False # detekuje, zda jsou dva řešitelé jmenovci (modulo nabodeníčka), # pokud ano, vrátí se jako true - slovnik_s_body = body_resitelu(resitele, rocnik_obj) + slovnik_s_body = body_resitelu(resitele, cislo_obj) for resitel in resitele: resitel.titul = resitel.get_titul(slovnik_s_body[resitel.id])