diff --git a/course/calendar.py b/course/calendar.py index 7a06232f7526378dd59c98e4ff9c7321488b86b5..9631fc9cf0e8d47fb4c498a2cbf76867e08b5bff 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -41,11 +41,12 @@ from crispy_forms.layout import Submit import datetime from bootstrap3_datetime.widgets import DateTimePicker -from relate.utils import StyledForm, as_local_time, string_concat +from relate.utils import StyledForm, as_local_time, string_concat, StyledModelForm from course.constants import ( participation_permission as pperm, ) from course.models import Event +from django.shortcuts import get_object_or_404 # {{{ creation @@ -410,6 +411,222 @@ def view_calendar(pctx): "events_json": dumps(events_json), "event_info_list": event_info_list, "default_date": default_date.isoformat(), + "edit_view": False + }) + + +class EditEventForm(StyledModelForm): + class Meta: + model = Event + fields = ['kind', 'ordinal', 'time', + 'end_time', 'all_day', 'shown_in_calendar'] + widgets = { + "time": DateTimePicker(options={"format": "YYYY-MM-DD HH:mm"}), + "end_time": DateTimePicker(options={"format": "YYYY-MM-DD HH:mm"}), + } + + +@login_required +@course_view +def edit_calendar(pctx): + if not pctx.has_permission(pperm.edit_events): + raise PermissionDenied(_("may not edit events")) + + from course.content import markup_to_html, parse_date_spec + + from course.views import get_now_or_fake_time + now = get_now_or_fake_time(pctx.request) + request = pctx.request + + edit_existing_event_flag = False + id_to_edit = None + edit_event_form = EditEventForm() + default_date = now.date() + + if request.method == "POST": + if 'id_to_delete' in request.POST: + event_to_delete = get_object_or_404(Event, + id=request.POST['id_to_delete']) + default_date = event_to_delete.time.date() + try: + with transaction.atomic(): + event_to_delete.delete() + messages.add_message(request, messages.SUCCESS, + _("Event deleted.")) + except Exception as e: + messages.add_message(request, messages.ERROR, + string_concat( + _("No event deleted"), + ": %(err_type)s: %(err_str)s") + % { + "err_type": type(e).__name__, + "err_str": str(e)}) + + elif 'id_to_edit' in request.POST: + id_to_edit = request.POST['id_to_edit'] + exsting_event_form = get_object_or_404(Event, + id=id_to_edit) + edit_event_form = EditEventForm(instance=exsting_event_form) + edit_existing_event_flag = True + + else: + init_event = Event(course=pctx.course) + is_editing_existing_event = 'existing_event_to_save' in request.POST + + if is_editing_existing_event: + init_event = get_object_or_404(Event, + id=request.POST['existing_event_to_save']) + form_event = EditEventForm(request.POST, instance=init_event) + + if form_event.is_valid(): + kind = form_event.cleaned_data['kind'] + ordinal = form_event.cleaned_data['ordinal'] + try: + with transaction.atomic(): + form_event.save() + except Exception as e: + if isinstance(e, IntegrityError): + if ordinal is not None: + ordinal = str(int(ordinal)) + else: + ordinal = _("(no ordinal)") + e = EventAlreadyExists( + _("'%(event_kind)s %(event_ordinal)s' already exists") + % {'event_kind': kind, + 'event_ordinal': ordinal}) + + if is_editing_existing_event: + msg = _("Event not updated.") + else: + msg = _("No event created.") + + messages.add_message(request, messages.ERROR, + string_concat( + "%(err_type)s: %(err_str)s. ", msg) + % { + "err_type": type(e).__name__, + "err_str": str(e)}) + else: + if is_editing_existing_event: + messages.add_message(request, messages.SUCCESS, + _("Event updated.")) + else: + messages.add_message(request, messages.SUCCESS, + _("Event created.")) + default_date = form_event.cleaned_data['time'].date() + events_json = [] + + from course.content import get_raw_yaml_from_repo + try: + event_descr = get_raw_yaml_from_repo(pctx.repo, + pctx.course.events_file, pctx.course_commit_sha) + except ObjectDoesNotExist: + event_descr = {} + + event_kinds_desc = event_descr.get("event_kinds", {}) + event_info_desc = event_descr.get("events", {}) + + event_info_list = [] + + events = sorted( + Event.objects + .filter( + course=pctx.course, + ), + key=lambda evt: ( + -evt.time.year, -evt.time.month, -evt.time.day, + evt.time.hour, evt.time.minute, evt.time.second)) + + for event in events: + kind_desc = event_kinds_desc.get(event.kind) + + human_title = six.text_type(event) + + event_json = { + "id": event.id, + "start": event.time.isoformat(), + "allDay": event.all_day, + } + if event.end_time is not None: + event_json["end"] = event.end_time.isoformat() + + if kind_desc is not None: + if "color" in kind_desc: + event_json["color"] = kind_desc["color"] + if "title" in kind_desc: + if event.ordinal is not None: + human_title = kind_desc["title"].format(nr=event.ordinal) + else: + human_title = kind_desc["title"] + + description = None + show_description = True + event_desc = event_info_desc.get(six.text_type(event)) + if event_desc is not None: + if "description" in event_desc: + description = markup_to_html( + pctx.course, pctx.repo, pctx.course_commit_sha, + event_desc["description"]) + + if "title" in event_desc: + human_title = event_desc["title"] + + if "color" in event_desc: + event_json["color"] = event_desc["color"] + + if "show_description_from" in event_desc: + ds = parse_date_spec( + pctx.course, event_desc["show_description_from"]) + if now < ds: + show_description = False + + if "show_description_until" in event_desc: + ds = parse_date_spec( + pctx.course, event_desc["show_description_until"]) + if now > ds: + show_description = False + + event_json["title"] = human_title + + if show_description and description: + event_json["url"] = "#event-%d" % event.id + + start_time = event.time + end_time = event.end_time + + if event.all_day: + start_time = start_time.date() + if end_time is not None: + local_end_time = as_local_time(end_time) + end_midnight = datetime.time(tzinfo=local_end_time.tzinfo) + if local_end_time.time() == end_midnight: + end_time = (end_time - datetime.timedelta(days=1)).date() + else: + end_time = end_time.date() + + event_info_list.append( + EventInfo( + id=event.id, + human_title=human_title, + start_time=start_time, + end_time=end_time, + description=description + )) + + events_json.append(event_json) + + if pctx.course.end_date is not None and default_date > pctx.course.end_date: + default_date = pctx.course.end_date + + from json import dumps + return render_course_page(pctx, "course/calendar.html", { + "form": edit_event_form, + "events_json": dumps(events_json), + "event_info_list": event_info_list, + "default_date": default_date.isoformat(), + "edit_existing_event_flag": edit_existing_event_flag, + "id_to_edit": id_to_edit, + "edit_view": True }) # }}} diff --git a/course/migrations/0112_add_help_text_for_event_shown_in_calendar.py b/course/migrations/0112_add_help_text_for_event_shown_in_calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..fab83d0649db2d5185618f9aaaf4808f2b88d572 --- /dev/null +++ b/course/migrations/0112_add_help_text_for_event_shown_in_calendar.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-09-25 10:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0111_alter_git_source_in_course_to_a_required_field'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='shown_in_calendar', + field=models.BooleanField(default=True, help_text="Shown in students' calendar", verbose_name='Shown in calendar'), + ), + ] diff --git a/course/models.py b/course/models.py index a1827c139e26bef34fabcd5bbf0b644fed8aa165..6ce7efd4efa8ed59625b95aaa3ad7f81980e257e 100644 --- a/course/models.py +++ b/course/models.py @@ -307,6 +307,7 @@ class Event(models.Model): verbose_name=_('All day')) shown_in_calendar = models.BooleanField(default=True, + help_text=_("Shown in students' calendar"), verbose_name=_('Shown in calendar')) class Meta: @@ -321,6 +322,18 @@ class Event(models.Model): else: return self.kind + def save(self, *args, **kwargs): + # When ordinal is Null, unique_together failed to identify duplicate entries + if not self.ordinal: + if not self.pk: + object_exist = bool( + Event.objects.filter( + kind=self.kind, ordinal__isnull=True).count()) + if object_exist: + from django.db import IntegrityError + raise IntegrityError() + super(Event, self).save(*args, **kwargs) + if six.PY3: __str__ = __unicode__ diff --git a/course/templates/course/calendar.html b/course/templates/course/calendar.html index 2fcc66e855c20daa6adbb28b3a568ab311d0a929..104d11c387af0698b90f3f1be98c129bf0a64854 100644 --- a/course/templates/course/calendar.html +++ b/course/templates/course/calendar.html @@ -19,30 +19,30 @@ {% block content %}

{{ course.number}} {% trans "Calendar" %}

- + {% if pperm.edit_events %} +
+ + {% if edit_view %} + {% trans "Switch to Student View" %} + {% else %} + {% trans "Switch to Edit View" %} + {% endif %} + + {% if edit_view %} + + {% endif %} +
+ {% endif %}
+ {% trans "Note" %}: {% if edit_view %}{% trans "Different from the students' calendar, this calender shows all events. " %}{% endif %}{% trans "Some calendar entries are clickable and link to entries below." %} - - -{% blocktrans trimmed %} - Note: Some calendar entries are clickable and link to entries - below. -{% endblocktrans %}
{% for event_info in event_info_list %} @@ -58,5 +58,126 @@ {% endfor%}
+ {% if edit_view %} +