diff --git a/seminar/forms.py b/seminar/forms.py index 6a0e7911..fb313272 100644 --- a/seminar/forms.py +++ b/seminar/forms.py @@ -2,6 +2,7 @@ from django import forms from dal import autocomplete from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.models import User +from django.forms.models import inlineformset_factory from .models import Skola, Resitel, Osoba, Problem import seminar.models as m @@ -252,6 +253,25 @@ class VlozReseniForm(forms.Form): 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',) + + 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, + + ) diff --git a/seminar/migrations/0074_auto_20200228_1401.py b/seminar/migrations/0074_auto_20200228_1401.py new file mode 100644 index 00000000..447d5aab --- /dev/null +++ b/seminar/migrations/0074_auto_20200228_1401.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.9 on 2020-02-28 13:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0073_copy_osoba_email_to_user_email'), + ] + + operations = [ + migrations.AddField( + model_name='prilohareseni', + name='res_poznamka', + field=models.TextField(blank=True, help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje', verbose_name='poznámka řešitele'), + ), + migrations.AlterField( + model_name='hodnoceni', + name='problem', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.Problem', verbose_name='problém'), + ), + ] diff --git a/seminar/migrations/0075_auto_20200228_2010.py b/seminar/migrations/0075_auto_20200228_2010.py new file mode 100644 index 00000000..2e703e4e --- /dev/null +++ b/seminar/migrations/0075_auto_20200228_2010.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.9 on 2020-02-28 19:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0074_auto_20200228_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='hodnoceni', + name='body', + field=models.DecimalField(decimal_places=1, max_digits=8, null=True, verbose_name='body'), + ), + ] diff --git a/seminar/migrations/0076_auto_20200228_2013.py b/seminar/migrations/0076_auto_20200228_2013.py new file mode 100644 index 00000000..04aaea1e --- /dev/null +++ b/seminar/migrations/0076_auto_20200228_2013.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.9 on 2020-02-28 19:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0075_auto_20200228_2010'), + ] + + operations = [ + migrations.AlterField( + model_name='hodnoceni', + name='cislo_body', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.Cislo', verbose_name='číslo pro body'), + ), + ] diff --git a/seminar/models.py b/seminar/models.py index f8c41ed0..6f1c89f5 100644 --- a/seminar/models.py +++ b/seminar/models.py @@ -920,10 +920,10 @@ class Hodnoceni(SeminarModelBase): body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', - blank=False, null=False) + blank=False, null=True) cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body', - related_name='hodnoceni', blank=False, null=False, on_delete=models.PROTECT) + related_name='hodnoceni', blank=False, null=True, on_delete=models.PROTECT) reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) @@ -935,17 +935,16 @@ class Hodnoceni(SeminarModelBase): -## FIXME: Budeme řešit později, pokud to bude potřeba. -#def aux_generate_filename(self, filename): -# """Pomocná funkce generující ošetřený název souboru v adresáři s datem""" -# clean = get_valid_filename( -# unidecode(filename.replace('/', '-').replace('\0', '')) -# ) -# datedir = timezone.now().strftime('%Y-%m') -# fname = "%s_%s" % ( -# timezone.now().strftime('%Y-%m-%d-%H:%M'), -# clean) -# return os.path.join(datedir, fname) +def aux_generate_filename(self, filename): + """Pomocná funkce generující ošetřený název souboru v adresáři s datem""" + clean = get_valid_filename( + unidecode(filename.replace('/', '-').replace('\0', '')) + ) + datedir = timezone.now().strftime('%Y-%m') + fname = "{}_{}".format( + timezone.now().strftime('%Y-%m-%d-%H:%M'), + clean) + return os.path.join(datedir, fname) # Django neumí jednoduše serializovat partial nebo třídu s __call__ # (https://docs.djangoproject.com/en/1.8/topics/migrations/), @@ -988,9 +987,12 @@ class PrilohaReseni(SeminarModelBase): poznamka = models.TextField('neveřejná poznámka', blank=True, help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') + + res_poznamka = models.TextField('poznámka řešitele', blank=True, + help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') def __str__(self): - return self.soubor + return str(self.soubor) class Pohadka(SeminarModelBase): diff --git a/seminar/static/seminar/dynamic_formsets.js b/seminar/static/seminar/dynamic_formsets.js new file mode 100644 index 00000000..a0d99d0a --- /dev/null +++ b/seminar/static/seminar/dynamic_formsets.js @@ -0,0 +1,49 @@ +// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 +function updateElementIndex(el, prefix, ndx) { + var id_regex = new RegExp('(' + prefix + '-\\d+)'); + var replacement = prefix + '-' + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } +} + +// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 +function deleteForm(prefix, btn) { + var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val()); + if (total >= 1){ + btn.closest('div').remove(); + var forms = $('.attachment'); + $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length); + for (var i=0, formCount=forms.length; i + {{form.media}} + +{% endblock %} + +{% block content %} +

+ {% block nadpis1a %}{% block nadpis1b %} + Vložit řešení + {% endblock %}{% endblock %} +

+
+ {% csrf_token %} +{{form}} +{{prilohy.management_form}} +
+{% for form in prilohy.forms %} +
+ {{form.non_field_errors}} + {{form.errors}} + + {{ form }} +
+ +
+{% endfor %} +
+ + + +
+ +{% endblock %} diff --git a/seminar/urls.py b/seminar/urls.py index bc1c89a8..1d5348da 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -109,6 +109,7 @@ urlpatterns = [ path('auth/resitel/', views.ResitelView.as_view(), name='seminar_resitel'), path('autocomplete/skola/',views.SkolaAutocomplete.as_view(), name='autocomplete_skola'), path('autocomplete/resitel/',views.ResitelAutocomplete.as_view(), name='autocomplete_resitel'), + path('autocomplete/problem/odevzdatelny',views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'), path('auth/reset_password/', views.PasswordResetView.as_view(), name='reset_password'), path('auth/change_password/', views.PasswordChangeView.as_view(), name='change_password'), path('auth/reset_password_done/', views.PasswordResetDoneView.as_view(), name='reset_password_done'), @@ -118,6 +119,7 @@ urlpatterns = [ path('temp/add_solution', views.AddSolutionView.as_view(),name='seminar_vloz_reseni'), + path('temp/submit_solution', views.SubmitSolutionView.as_view(),name='seminar_nahraj_reseni'), path('', views.TitulniStranaView.as_view(), name='titulni_strana'), diff --git a/seminar/views.py b/seminar/views.py index e174ab28..0136dfb2 100644 --- a/seminar/views.py +++ b/seminar/views.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _ from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect from django.db.models import Q, Sum, Count from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.generic.edit import FormView +from django.views.generic.edit import FormView, CreateView from django.contrib.auth import authenticate, login, get_user_model, logout from django.contrib.auth import views as auth_views from django.contrib.auth.models import User @@ -1157,6 +1157,40 @@ class AddSolutionView(LoginRequiredMixin, FormView): form_class = f.VlozReseniForm success_url = '/' +class SubmitSolutionView(LoginRequiredMixin, CreateView): + model = s.Reseni + template_name = 'seminar/nahraj_reseni.html' + form_class = f.NahrajReseniForm + + def get_context_data(self,**kwargs): + data = super().get_context_data(**kwargs) + if self.request.POST: + data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) + else: + data['prilohy'] = f.ReseniSPrilohamiFormSet() + return data + + # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni + # Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset + def form_valid(self,form): + context = self.get_context_data() + prilohy = context['prilohy'] + with transaction.atomic(): + self.object = form.save() + self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user)) + self.object.cas_doruceni = datetime.now() + self.object.forma = s.Reseni.FORMA_UPLOAD + if prilohy.is_valid(): + prilohy.instance = self.object + prilohy.save() + else: + raise Exception("Uploadovane soubory nebyly validni") + + return HttpResponseRedirect(self.get_success_url()) + + + + def resetPasswordView(request): pass @@ -1349,6 +1383,19 @@ class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetVie ) return qs +class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + nastaveni = get_object_or_404(Nastaveni) + rocnik = nastaveni.aktualni_rocnik + temata = s.Tema.objects.filter(rocnik=rocnik, stav=s.Problem.STAV_ZADANY) + ulohy = s.Uloha.objects.filter(cislo_deadline__rocnik = rocnik) + ulohy.union(temata) + qs = ulohy + if self.q: + qs = qs.filter( + Q(nazev__startswith=self.q)) + return qs + # Ceka na autocomplete v3 # class OrganizatorAutocomplete(autocomplete.Select2QuerySetView):