From 1d613f6415bc3679c82d5f7e8a54425e1a2a61ba Mon Sep 17 00:00:00 2001 From: luh Date: Sat, 19 Aug 2017 21:01:46 -0500 Subject: [PATCH 01/24] add interface --- course/calendar.py | 122 +++++++++++++++++++++ course/templates/course/calender_edit.html | 81 ++++++++++++++ relate/urls.py | 6 + 3 files changed, 209 insertions(+) create mode 100644 course/templates/course/calender_edit.html diff --git a/course/calendar.py b/course/calendar.py index 7a06232f..c76e332d 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -412,6 +412,128 @@ def view_calendar(pctx): "default_date": default_date.isoformat(), }) +@course_view +def edit_calendar(pctx): + 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) + + if not pctx.has_permission(pperm.view_calendar): + raise PermissionDenied(_("may not view calendar")) + + 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, + shown_in_calendar=True), + 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) + + default_date = now.date() + 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/calender_edit.html", { + "events_json": dumps(events_json), + "event_info_list": event_info_list, + "default_date": default_date.isoformat(), + }) + # }}} # vim: foldmethod=marker diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html new file mode 100644 index 00000000..4d88769c --- /dev/null +++ b/course/templates/course/calender_edit.html @@ -0,0 +1,81 @@ +{% extends "course/course-base.html" %} +{% load i18n %} + +{% load static %} + +{% block title %} + {{ course.number}} + {% trans "Calendar" %} - {% trans "RELATE" %} +{% endblock %} + +{%block header_extra %} + + + + {# load calendar with local language #} + {% get_current_language as LANGUAGE_CODE %} + +{% endblock %} + +{% block content %} + + + + + + +
+ +
+ +
+ +
+
+
+ +
+ + + + + + + + + +{% blocktrans trimmed %} + Note: Some calendar entries are clickable and link to entries + below. +{% endblocktrans %} + +
+ {% for event_info in event_info_list %} +
+
+ {{ event_info.human_title }} + ({{ event_info.start_time }}{% if event_info.end_time %} - {{ event_info.end_time }}{% endif %}) +
+
+ {{ event_info.description|safe }} +
+
+ {% endfor%} +
+ +{% endblock %} + diff --git a/relate/urls.py b/relate/urls.py index a9cc437f..d72e523e 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -337,6 +337,12 @@ urlpatterns = [ "/calendar/$", course.calendar.view_calendar, name="relate-view_calendar"), + + url(r"^course" + "/" + COURSE_ID_REGEX + + "/calendar-edit/$", + course.calendar.edit_calendar, + name="relate-view_calendar"), # }}} -- GitLab From 6fdbff31934485ca26c7b7e1f54a657837264711 Mon Sep 17 00:00:00 2001 From: luh Date: Sat, 19 Aug 2017 23:03:10 -0500 Subject: [PATCH 02/24] add modal box form --- course/calendar.py | 75 +++++++++- course/templates/course/calender_edit.html | 161 ++++++++++++++++++++- 2 files changed, 226 insertions(+), 10 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index c76e332d..9e76b91d 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -412,6 +412,76 @@ def view_calendar(pctx): "default_date": default_date.isoformat(), }) +class EditEventForm(StyledForm): + kind = forms.CharField(required=True, + help_text=_("Should be lower_case_with_underscores, no spaces " + "allowed."), + label=pgettext_lazy("Kind of event", "Kind of event")) + time = forms.DateTimeField( + widget=DateTimePicker( + options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}), + label=pgettext_lazy("Starting time of event", "Starting time")) + duration_in_minutes = forms.FloatField(required=False, + label=_("Duration in minutes")) + all_day = forms.BooleanField( + required=False, + initial=False, + label=_("All-day event"), + help_text=_("Only affects the rendering in the class calendar, " + "in that a start time is not shown")) + shown_in_calendar = forms.BooleanField( + required=False, + initial=True, + label=_('Shown in calendar')) + interval = forms.ChoiceField(required=True, + choices=( + ("weekly", _("Weekly")), + ("biweekly", _("Bi-Weekly")), + ), + label=pgettext_lazy("Interval of recurring events", "Interval")) + starting_ordinal = forms.IntegerField(required=False, + label=pgettext_lazy( + "Starting ordinal of recurring events", "Starting ordinal")) + count = forms.IntegerField(required=True, + label=pgettext_lazy("Count of recurring events", "Count")) + + def __init__(self, *args, **kwargs): + super(EditEventForm, self).__init__(*args, **kwargs) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@login_required @course_view def edit_calendar(pctx): from course.content import markup_to_html, parse_date_spec @@ -419,8 +489,8 @@ def edit_calendar(pctx): from course.views import get_now_or_fake_time now = get_now_or_fake_time(pctx.request) - if not pctx.has_permission(pperm.view_calendar): - raise PermissionDenied(_("may not view calendar")) + if not pctx.has_permission(pperm.edit_events): + raise PermissionDenied(_("may not edit events")) events_json = [] @@ -529,6 +599,7 @@ def edit_calendar(pctx): from json import dumps return render_course_page(pctx, "course/calender_edit.html", { + "form": EditEventForm(), "events_json": dumps(events_json), "event_info_list": event_info_list, "default_date": default_date.isoformat(), diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html index 4d88769c..9d1dd0a3 100644 --- a/course/templates/course/calender_edit.html +++ b/course/templates/course/calender_edit.html @@ -15,19 +15,92 @@ {# load calendar with local language #} {% get_current_language as LANGUAGE_CODE %} + + {% endblock %} -{% block content %} - + + + + +{% block content %} -
- +
@@ -38,10 +111,6 @@ - - - - + + + + + + + + + {% blocktrans trimmed %} Note: Some calendar entries are clickable and link to entries below. @@ -77,5 +217,10 @@ {% endfor%}
+ + + + + {% endblock %} -- GitLab From fb9aa4dd2b61759c22c4e7f1966493a055033ca5 Mon Sep 17 00:00:00 2001 From: luh Date: Sat, 19 Aug 2017 23:47:25 -0500 Subject: [PATCH 03/24] investigating widget --- course/calendar.py | 69 ++++------------------------------------------ relate/urls.py | 2 +- 2 files changed, 6 insertions(+), 65 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index 9e76b91d..238e35de 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -41,7 +41,7 @@ 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, ) @@ -412,75 +412,16 @@ def view_calendar(pctx): "default_date": default_date.isoformat(), }) -class EditEventForm(StyledForm): - kind = forms.CharField(required=True, - help_text=_("Should be lower_case_with_underscores, no spaces " - "allowed."), - label=pgettext_lazy("Kind of event", "Kind of event")) - time = forms.DateTimeField( - widget=DateTimePicker( - options={"format": "YYYY-MM-DD HH:mm", "sideBySide": True}), - label=pgettext_lazy("Starting time of event", "Starting time")) - duration_in_minutes = forms.FloatField(required=False, - label=_("Duration in minutes")) - all_day = forms.BooleanField( - required=False, - initial=False, - label=_("All-day event"), - help_text=_("Only affects the rendering in the class calendar, " - "in that a start time is not shown")) - shown_in_calendar = forms.BooleanField( - required=False, - initial=True, - label=_('Shown in calendar')) - interval = forms.ChoiceField(required=True, - choices=( - ("weekly", _("Weekly")), - ("biweekly", _("Bi-Weekly")), - ), - label=pgettext_lazy("Interval of recurring events", "Interval")) - starting_ordinal = forms.IntegerField(required=False, - label=pgettext_lazy( - "Starting ordinal of recurring events", "Starting ordinal")) - count = forms.IntegerField(required=True, - label=pgettext_lazy("Count of recurring events", "Count")) +class EditEventForm(StyledModelForm): + class Meta: + model = Event + fields = ['course', 'kind', 'ordinal','time','end_time','all_day'] def __init__(self, *args, **kwargs): super(EditEventForm, self).__init__(*args, **kwargs) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @login_required @course_view def edit_calendar(pctx): diff --git a/relate/urls.py b/relate/urls.py index d72e523e..cba5bf46 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -342,7 +342,7 @@ urlpatterns = [ "/" + COURSE_ID_REGEX + "/calendar-edit/$", course.calendar.edit_calendar, - name="relate-view_calendar"), + name="relate-edit_calendar"), # }}} -- GitLab From e429bbc2d6268eca5b1ef75bd86a58562cccdcdc Mon Sep 17 00:00:00 2001 From: luh Date: Sun, 20 Aug 2017 00:53:29 -0500 Subject: [PATCH 04/24] resolve widget issue --- course/calendar.py | 6 +- course/templates/course/calender_edit.html | 147 +++++++++++---------- relate/settings.py | 1 + 3 files changed, 83 insertions(+), 71 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index 238e35de..72624d40 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -412,14 +412,16 @@ def view_calendar(pctx): "default_date": default_date.isoformat(), }) + class EditEventForm(StyledModelForm): class Meta: model = Event - fields = ['course', 'kind', 'ordinal','time','end_time','all_day'] + fields = ['kind', 'ordinal', 'time', 'end_time', 'all_day'] def __init__(self, *args, **kwargs): super(EditEventForm, self).__init__(*args, **kwargs) - + self.fields['time'].widget.attrs = {'id': 'start_time'} + self.fields['end_time'].widget.attrs = {'id': 'end_time'} @login_required diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html index 9d1dd0a3..8b0a8f2f 100644 --- a/course/templates/course/calender_edit.html +++ b/course/templates/course/calender_edit.html @@ -15,6 +15,12 @@ {# load calendar with local language #} {% get_current_language as LANGUAGE_CODE %} + + + + + + {% endblock %} - - - - - - - {% block content %} -
-
- +
-
+
- - - - {% blocktrans trimmed %} Note: Some calendar entries are clickable and link to entries below. @@ -157,79 +144,55 @@
-- GitLab From e89a645f4913adc8cbaba5a54219c29fd8bc3a19 Mon Sep 17 00:00:00 2001 From: paulluh Date: Mon, 21 Aug 2017 01:04:01 -0500 Subject: [PATCH 07/24] next step is edit event --- course/calendar.py | 1 - 1 file changed, 1 deletion(-) diff --git a/course/calendar.py b/course/calendar.py index 035ecdc0..00044bbc 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -441,7 +441,6 @@ def edit_calendar(pctx): if request.method == "POST": if 'id_to_delete' in request.POST: Event.objects.filter(id=request.POST['id_to_delete']).delete() - print("success") return HttpResponse("deleted successful") else: -- GitLab From d8e77cfc9af116741651efdc60f69130ab50ce8b Mon Sep 17 00:00:00 2001 From: paulluh Date: Mon, 21 Aug 2017 01:20:20 -0500 Subject: [PATCH 08/24] fixed styling issue --- course/calendar.py | 7 ++++--- relate/urls.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index 00044bbc..111018b8 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -417,7 +417,8 @@ def view_calendar(pctx): class EditEventForm(StyledModelForm): class Meta: model = Event - fields = ['kind', 'ordinal', 'time', 'end_time', 'all_day','shown_in_calendar'] + fields = ['kind', 'ordinal', 'time', + 'end_time', 'all_day', 'shown_in_calendar'] def __init__(self, *args, **kwargs): super(EditEventForm, self).__init__(*args, **kwargs) @@ -450,7 +451,7 @@ def edit_calendar(pctx): kind = form_event.cleaned_data['kind'] ordinal = form_event.cleaned_data['ordinal'] print() - try: + try: form_event.save() except IntegrityError: e = EventAlreadyExists( @@ -575,7 +576,7 @@ def edit_calendar(pctx): "events_json": dumps(events_json), "event_info_list": event_info_list, "default_date": default_date.isoformat(), - }) + }) # }}} diff --git a/relate/urls.py b/relate/urls.py index cba5bf46..cbe34ca7 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -337,12 +337,12 @@ urlpatterns = [ "/calendar/$", course.calendar.view_calendar, name="relate-view_calendar"), - + url(r"^course" "/" + COURSE_ID_REGEX + "/calendar-edit/$", course.calendar.edit_calendar, - name="relate-edit_calendar"), + name="relate-edit_calendar"), # }}} -- GitLab From 8d32297f609175547a1d551de46cea5dbca16355 Mon Sep 17 00:00:00 2001 From: paulluh Date: Mon, 21 Aug 2017 16:56:30 -0500 Subject: [PATCH 09/24] configure exception interface --- course/calendar.py | 10 +++++++++- course/templates/course/calender_edit.html | 7 +------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index 111018b8..e1a855c7 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -461,10 +461,18 @@ def edit_calendar(pctx): messages.add_message(request, messages.ERROR, string_concat( "%(err_type)s: %(err_str)s. ", - _("No events created.")) + _("No event created.")) % { "err_type": type(e).__name__, "err_str": str(e)}) + except Exception as e: + messages.add_message(request, messages.ERROR, + string_concat( + "%(err_type)s: %(err_str)s. ", + _("No event created.")) + % { + "err_type": type(e).__name__, + "err_str": str(e)}) events_json = [] from course.content import get_raw_yaml_from_repo diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html index 586d2a85..b9c75487 100644 --- a/course/templates/course/calender_edit.html +++ b/course/templates/course/calender_edit.html @@ -140,16 +140,11 @@ }, success: function(result) { window.location = ""; - - } + } }); }) } - - }) - - }); -- GitLab From 86281bf363f7618371c5aa6c12476e962f9e7389 Mon Sep 17 00:00:00 2001 From: paulluh Date: Fri, 25 Aug 2017 14:34:53 -0500 Subject: [PATCH 10/24] adding for editing events --- course/calendar.py | 44 ++++- course/templates/course/calender_edit.html | 199 +++++++++------------ 2 files changed, 119 insertions(+), 124 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index e1a855c7..61c1a253 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -47,6 +47,7 @@ from course.constants import ( ) from course.models import Event from django.http import HttpResponse +from django.shortcuts import get_object_or_404 # {{{ creation @@ -419,6 +420,9 @@ class EditEventForm(StyledModelForm): model = Event fields = ['kind', 'ordinal', 'time', 'end_time', 'all_day', 'shown_in_calendar'] + help_texts = { + 'shown_in_calendar': ('Shown in students\' calendar') + } def __init__(self, *args, **kwargs): super(EditEventForm, self).__init__(*args, **kwargs) @@ -439,18 +443,37 @@ def edit_calendar(pctx): 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.objects.filter(id=request.POST['id_to_delete']).delete() - return HttpResponse("deleted successful") + event_to_delete = get_object_or_404(Event, + id=request.POST['id_to_delete']) + default_date = event_to_delete.time + event_to_delete.delete() + messages.add_message(request, messages.SUCCESS, + _("Event deleted.")) + + elif 'id_to_edit' in request.POST: + exsting_event_form = get_object_or_404(Event, + id=request.POST['id_to_edit']) + edit_event_form = EditEventForm(instance=exsting_event_form) + edit_existing_event_flag = True else: init_event = Event(course=pctx.course) + + if 'existing_event_to_save' in request.POST: + 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'] - print() try: form_event.save() except IntegrityError: @@ -473,6 +496,14 @@ def edit_calendar(pctx): % { "err_type": type(e).__name__, "err_str": str(e)}) + else: + if 'existing_event_to_save' in request.POST: + messages.add_message(request, messages.SUCCESS, + _("Event updated.")) + else: + messages.add_message(request, messages.SUCCESS, + _("Event created.")) + default_date = form_event.cleaned_data['time'] events_json = [] from course.content import get_raw_yaml_from_repo @@ -491,7 +522,7 @@ def edit_calendar(pctx): Event.objects .filter( course=pctx.course, - shown_in_calendar=True), + ), key=lambda evt: ( -evt.time.year, -evt.time.month, -evt.time.day, evt.time.hour, evt.time.minute, evt.time.second)) @@ -574,16 +605,17 @@ def edit_calendar(pctx): events_json.append(event_json) - default_date = now.date() 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/calender_edit.html", { - "form": EditEventForm(), + "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, }) # }}} diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html index b9c75487..8d26d16b 100644 --- a/course/templates/course/calender_edit.html +++ b/course/templates/course/calender_edit.html @@ -20,85 +20,24 @@ +{% endblock %} + + - -{% endblock %} {% block content %} +{% blocktrans trimmed %} + Note: Different from the students' calendar, this calender shows all events. +{% endblocktrans %} + + + +
- +
@@ -127,24 +66,19 @@ element.find('.fc-title').append( ''); element.find("#edit").click(function() { - + edit_existing_event(event.id); }) element.find("#remove").click(function() { - console.log(event.id) - $.ajax({ - type: 'POST', - data:{ - id_to_delete :event.id, - csrfmiddlewaretoken: '{{ csrf_token }}' - }, - success: function(result) { - window.location = ""; - } - }); + delete_existing_event(event.id); }) } - }) + }); + + {% if edit_existing_event_flag %} + $('#editModal').modal('show'); + {% endif %} + }); @@ -168,40 +102,44 @@ {% endfor%}
- +
-- GitLab From ffb22f25cfaad126fef01f11bb41fa6eadd64079 Mon Sep 17 00:00:00 2001 From: paulluh Date: Sat, 26 Aug 2017 10:35:05 -0500 Subject: [PATCH 14/24] style --- course/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/calendar.py b/course/calendar.py index 8d6fe470..bb838fed 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -457,7 +457,7 @@ def edit_calendar(pctx): _("Event deleted.")) elif 'id_to_edit' in request.POST: - id_to_edit = request.POST['id_to_edit'] + 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) -- GitLab From 3943313178cb3168aba94a1993b10e86b5b96e45 Mon Sep 17 00:00:00 2001 From: dzhuang Date: Thu, 14 Sep 2017 10:16:58 +0800 Subject: [PATCH 15/24] Added shortcut for relate-edit_calendar --- course/templates/course/course-base.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/course/templates/course/course-base.html b/course/templates/course/course-base.html index 429ae881..894dd906 100644 --- a/course/templates/course/course-base.html +++ b/course/templates/course/course-base.html @@ -125,9 +125,10 @@ {% if pperm.edit_events %} - {% if user.is_staff %} -
  • {% trans "Edit events" %}
  • + {% if user.is_superuser %} +
  • {% trans "Edit events (admin)" %}
  • {% endif %} +
  • {% trans "Edit events (calendar)" %}
  • {% trans "Create recurring events" %}
  • {% trans "Renumber events" %}
  • {% endif %} -- GitLab From 8f2a7212a49f1a827636c6550ac21d7ec091a6ff Mon Sep 17 00:00:00 2001 From: dzhuang Date: Thu, 14 Sep 2017 13:47:53 +0800 Subject: [PATCH 16/24] Re-organize calendar view. --- course/calendar.py | 11 +- course/templates/course/calendar.html | 145 ++++++++++++++--- course/templates/course/calender_edit.html | 181 --------------------- 3 files changed, 129 insertions(+), 208 deletions(-) delete mode 100644 course/templates/course/calender_edit.html diff --git a/course/calendar.py b/course/calendar.py index bb838fed..faed041b 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -411,6 +411,7 @@ def view_calendar(pctx): "events_json": dumps(events_json), "event_info_list": event_info_list, "default_date": default_date.isoformat(), + "edit_view": False }) @@ -432,14 +433,13 @@ class EditEventForm(StyledModelForm): @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) - - if not pctx.has_permission(pperm.edit_events): - raise PermissionDenied(_("may not edit events")) - request = pctx.request edit_existing_event_flag = False @@ -609,13 +609,14 @@ def edit_calendar(pctx): default_date = pctx.course.end_date from json import dumps - return render_course_page(pctx, "course/calender_edit.html", { + 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/templates/course/calendar.html b/course/templates/course/calendar.html index ec4f99fa..8cfc99ca 100644 --- a/course/templates/course/calendar.html +++ b/course/templates/course/calendar.html @@ -8,41 +8,45 @@ {% trans "Calendar" %} - {% trans "RELATE" %} {% endblock %} -{%block header_extra %} +{% block header_extra %} {# load calendar with local language #} {% get_current_language as LANGUAGE_CODE %} + {% if edit_view %} + + + {% endif %} {% endblock %} {% block content %}

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

    - + {% if pperm.edit_events %} + + {% 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 +62,102 @@ {% endfor%}
    + {% if edit_view %} + +
    + {% csrf_token %} + +
    +
    + {% csrf_token %} + +
    + {% endif %} {% endblock %} +{% block page_bottom_javascript_extra %} + + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/course/templates/course/calender_edit.html b/course/templates/course/calender_edit.html deleted file mode 100644 index 9a17b712..00000000 --- a/course/templates/course/calender_edit.html +++ /dev/null @@ -1,181 +0,0 @@ -{% extends "course/course-base.html" %} -{% load i18n %} - -{% load static %} - -{% block title %} - {{ course.number}} - {% trans "Calendar" %} - {% trans "RELATE" %} -{% endblock %} - -{%block header_extra %} - - - - {# load calendar with local language #} - {% get_current_language as LANGUAGE_CODE %} - - - - - - -{% endblock %} - - - - - -{% block content %} - -{% blocktrans trimmed %} - Note: Different from the students' calendar, this calender shows all events. -{% endblocktrans %} - - - - -
    -
    - -
    - -
    -
    -
    - -
    - - - - -{% blocktrans trimmed %} - Note: Some calendar entries are clickable and link to entries - below. -{% endblocktrans %} - -
    - {% for event_info in event_info_list %} -
    -
    - {{ event_info.human_title }} - ({{ event_info.start_time }}{% if event_info.end_time %} - {{ event_info.end_time }}{% endif %}) -
    -
    - {{ event_info.description|safe }} -
    -
    - {% endfor%} -
    - - {% endif %} @@ -88,14 +88,14 @@ {% endif %} - - + +
    {% csrf_token %} @@ -121,8 +121,8 @@ events: {{ events_json|safe }}, {% if edit_view %} eventRender: function(event, element) { - element.find('.fc-title').append( ''); - element.find('.fc-title').append( ''); + element.find('.fc-title').append( ''); + element.find('.fc-title').append( ' '); element.find("#edit").click(function() { edit_existing_event(event.id); }); @@ -136,28 +136,28 @@ {% if edit_existing_event_flag %} $('#editModal').modal('show'); {% endif %} + {% endif %} + }); - $(function () { - $('#start_time').datetimepicker({"format": "YYYY-MM-DD HH:mm", "sideBySide": true}); - $('#end_time').datetimepicker({"format": "YYYY-MM-DD HH:mm", "sideBySide": true}); - }); + $(function () { + $('#start_time').datetimepicker({"format": "YYYY-MM-DD HH:mm", "sideBySide": true}); + $('#end_time').datetimepicker({"format": "YYYY-MM-DD HH:mm", "sideBySide": true}); + }); - function edit_existing_event(event_id) { - $('#id_to_edit').val(event_id); - document.existing_event_edit_form.submit(); - } + function edit_existing_event(event_id) { + $('#id_to_edit').val(event_id); + document.existing_event_edit_form.submit(); + } - function delete_existing_event(event_id) { - $('#id_to_delete').val(event_id); - document.existing_event_delete_form.submit(); - } + function delete_existing_event(event_id) { + $('#id_to_delete').val(event_id); + document.existing_event_delete_form.submit(); + } - function remove_event_id_field() { - $('#existing_event_to_save').remove() - } + function remove_event_id_field() { + $('#existing_event_to_save').remove() + } - {% endif %} - }); {{ block.super }} {% endblock %} \ No newline at end of file diff --git a/test/base_test_mixins.py b/test/base_test_mixins.py index 0906087d..bf0bcee2 100644 --- a/test/base_test_mixins.py +++ b/test/base_test_mixins.py @@ -190,13 +190,13 @@ class CoursesTestMixinBase(SuperuserCreateMixin): @classmethod def create_participation( cls, course, create_user_kwargs, role_identifier, status): - try: - # TODO: why pop failed here? - password = create_user_kwargs["password"] - except: - raise user, created = get_user_model().objects.get_or_create(**create_user_kwargs) if created: + try: + # TODO: why pop failed here? + password = create_user_kwargs["password"] + except: + raise user.set_password(password) user.save() participation, p_created = Participation.objects.get_or_create( @@ -215,6 +215,29 @@ class CoursesTestMixinBase(SuperuserCreateMixin): cls.c.force_login(cls.superuser) cls.c.post(reverse("relate-set_up_new_course"), create_course_kwargs) + def assertMessageContains(self, resp, expected_message): # noqa + """ + :param resp: response + :param expected_message: message string or list containing message string + """ + if isinstance(expected_message, list): + self.assertTrue(set(expected_message).issubset( + set([m.message for m in list(resp.context['messages'])]))) + if isinstance(expected_message, str): + self.assertIn(expected_message, + [m.message for m in list(resp.context['messages'])]) + + def debug_print_response_messages(self, resp): + """ + For debugging :class:`django.contrib.messages` objects in post response + :param resp: response + """ + print("\n") + print("-----------message start-------------") + for m in list(resp.context['messages']): + print(m.message) + print("-----------message end-------------") + class SingleCourseTestMixin(CoursesTestMixinBase): courses_setup_list = SINGLE_COURSE_SETUP_LIST diff --git a/test/test_calendar.py b/test/test_calendar.py new file mode 100644 index 00000000..4f59556f --- /dev/null +++ b/test/test_calendar.py @@ -0,0 +1,424 @@ +from __future__ import division + +__copyright__ = "Copyright (C) 2017 Dong Zhuang" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import json +from django.test import TestCase +from django.urls import reverse +from course.models import Event +from base_test_mixins import SingleCourseTestMixin +from django.utils.timezone import now + +SHOWN_EVENT_KIND = "test_open_event" +HIDDEN_EVENT_KIND = "test_secret_event" +OPEN_EVENT_NO_ORDINAL = "test_open_no_ordinal" +HIDDEN_EVENT_NO_ORDINAL = "test_secret_no_ordinal" +FAILURE_EVENT_KIND = "never_created_event_kind" + +TEST_EVENTS = ( + {"kind": SHOWN_EVENT_KIND, "ordinal": "1", + "shown_in_calendar": True, "time": now()}, + {"kind": SHOWN_EVENT_KIND, "ordinal": "2", + "shown_in_calendar": True, "time": now()}, + {"kind": SHOWN_EVENT_KIND, "ordinal": "3", + "shown_in_calendar": True, "time": now()}, + {"kind": HIDDEN_EVENT_KIND, "ordinal": "1", + "shown_in_calendar": False, "time": now()}, + {"kind": HIDDEN_EVENT_KIND, "ordinal": "2", + "shown_in_calendar": False, "time": now()}, + {"kind": OPEN_EVENT_NO_ORDINAL, + "shown_in_calendar": True, "time": now()}, + {"kind": HIDDEN_EVENT_NO_ORDINAL, + "shown_in_calendar": False, "time": now()}, +) + +N_TEST_EVENTS = len(TEST_EVENTS) # 7 events +N_HIDDEN_EVENTS = len([event + for event in TEST_EVENTS + if not event["shown_in_calendar"]]) # 3 events +N_SHOWN_EVENTS = N_TEST_EVENTS - N_HIDDEN_EVENTS # 4 events + +# html literals (from template) +MENU_EDIT_EVENTS_ADMIN = "Edit events (admin)" +MENU_VIEW_EVENTS_CALENDAR = "Edit events (calendar)" +MENU_CREATE_RECURRING_EVENTS = "Create recurring events" +MENU_RENUMBER_EVENTS = "Renumber events" + +HTML_SWITCH_TO_STUDENT_VIEW = "Switch to Student View" +HTML_SWITCH_TO_EDIT_VIEW = "Switch to Edit View" +HTML_CREATE_NEW_EVENT_BUTTON_TITLE = "create a new event" +HTML_EVENT_CREATED_MSG = "Event created." + +MSG_EVENT_NOT_CREATED = "No event created." +MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG = "EventAlreadyExists:" +MSG_HTML_EVENT_DELETED = "Event deleted." +MSG_EVENT_UPDATED = "Event updated." +MSG_EVENT_NOT_UPDATED = "Event not updated." + + +class CalendarTest(SingleCourseTestMixin, TestCase): + @classmethod + def setUpTestData(cls): # noqa + super(CalendarTest, cls).setUpTestData() + + # superuser was previously removed from participation, now we add him back + from course.constants import participation_status + + cls.create_participation( + course=cls.course, + create_user_kwargs={"id": cls.superuser.id}, + role_identifier="instructor", + status=participation_status.active) + + for event in TEST_EVENTS: + event.update({ + "course": cls.course, + }) + Event.objects.create(**event) + assert Event.objects.count() == N_TEST_EVENTS + + def assertShownEventsCountEqual(self, resp, expected_shown_events_count): # noqa + self.assertEqual( + len(json.loads(resp.context["events_json"])), + expected_shown_events_count) + + def assertTotalEventsCountEqual(self, expected_total_events_count): # noqa + self.assertEqual(Event.objects.count(), expected_total_events_count) + + def test_superuser_instructor_calendar_get(self): + self.c.force_login(self.superuser) + resp = self.c.get( + reverse("relate-view_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 200) + + # menu items + self.assertContains(resp, MENU_EDIT_EVENTS_ADMIN) + self.assertContains(resp, MENU_VIEW_EVENTS_CALENDAR) + self.assertContains(resp, MENU_CREATE_RECURRING_EVENTS) + self.assertContains(resp, MENU_RENUMBER_EVENTS) + + # rendered page html + self.assertNotContains(resp, HTML_SWITCH_TO_STUDENT_VIEW) + self.assertContains(resp, HTML_SWITCH_TO_EDIT_VIEW) + self.assertNotContains(resp, HTML_CREATE_NEW_EVENT_BUTTON_TITLE) + + # see only shown events + self.assertShownEventsCountEqual(resp, N_SHOWN_EVENTS) + + def test_non_superuser_instructor_calendar_get(self): + self.c.force_login(self.instructor_participation.user) + resp = self.c.get( + reverse("relate-view_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 200) + + # menu items + self.assertNotContains(resp, MENU_EDIT_EVENTS_ADMIN) + self.assertContains(resp, MENU_VIEW_EVENTS_CALENDAR) + self.assertContains(resp, MENU_CREATE_RECURRING_EVENTS) + self.assertContains(resp, MENU_RENUMBER_EVENTS) + + # rendered page html + self.assertNotContains(resp, HTML_SWITCH_TO_STUDENT_VIEW) + self.assertContains(resp, HTML_SWITCH_TO_EDIT_VIEW) + self.assertNotContains(resp, HTML_CREATE_NEW_EVENT_BUTTON_TITLE) + + # see only shown events + self.assertShownEventsCountEqual(resp, N_SHOWN_EVENTS) + + def test_student_calendar_get(self): + self.c.force_login(self.student_participation.user) + resp = self.c.get( + reverse("relate-view_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 200) + + # menu items + self.assertNotContains(resp, MENU_EDIT_EVENTS_ADMIN) + self.assertNotContains(resp, MENU_VIEW_EVENTS_CALENDAR) + self.assertNotContains(resp, MENU_CREATE_RECURRING_EVENTS) + self.assertNotContains(resp, MENU_RENUMBER_EVENTS) + + # rendered page html + self.assertNotContains(resp, HTML_SWITCH_TO_STUDENT_VIEW) + self.assertNotContains(resp, HTML_SWITCH_TO_EDIT_VIEW) + self.assertNotContains(resp, HTML_CREATE_NEW_EVENT_BUTTON_TITLE) + + # see only shown events + self.assertShownEventsCountEqual(resp, N_SHOWN_EVENTS) + + def test_superuser_instructor_calendar_edit_get(self): + self.c.force_login(self.superuser) + resp = self.c.get( + reverse("relate-edit_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 200) + + # menu items + self.assertContains(resp, MENU_EDIT_EVENTS_ADMIN) + self.assertContains(resp, MENU_VIEW_EVENTS_CALENDAR) + self.assertContains(resp, MENU_CREATE_RECURRING_EVENTS) + self.assertContains(resp, MENU_RENUMBER_EVENTS) + + # rendered page html + self.assertNotContains(resp, HTML_SWITCH_TO_EDIT_VIEW) + self.assertContains(resp, HTML_SWITCH_TO_STUDENT_VIEW) + self.assertContains(resp, HTML_CREATE_NEW_EVENT_BUTTON_TITLE) + + # see all events (including hidden ones) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) + + def test_non_superuser_instructor_calendar_edit_get(self): + self.c.force_login(self.instructor_participation.user) + resp = self.c.get( + reverse("relate-edit_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 200) + + # menu items + self.assertNotContains(resp, MENU_EDIT_EVENTS_ADMIN) + self.assertContains(resp, MENU_VIEW_EVENTS_CALENDAR) + self.assertContains(resp, MENU_CREATE_RECURRING_EVENTS) + self.assertContains(resp, MENU_RENUMBER_EVENTS) + + # rendered page html + self.assertNotContains(resp, HTML_SWITCH_TO_EDIT_VIEW) + self.assertContains(resp, HTML_SWITCH_TO_STUDENT_VIEW) + self.assertContains(resp, HTML_CREATE_NEW_EVENT_BUTTON_TITLE) + + # see all events (including hidden ones) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) + + def test_student_calendar_edit_get(self): + self.c.force_login(self.student_participation.user) + resp = self.c.get( + reverse("relate-edit_calendar", args=[self.course.identifier])) + self.assertEqual(resp.status_code, 403) + + def test_instructor_calendar_edit_create_exist_failure(self): + self.c.force_login(self.instructor_participation.user) + # Failing to create events already exist + post_data = { + "kind": SHOWN_EVENT_KIND, + "ordinal": "3", + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": True, + 'submit': [''] + } + + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG) + self.assertContains(resp, MSG_EVENT_NOT_CREATED) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) + + def test_instructor_calendar_edit_post_create_success(self): + # Successfully create new event + self.c.force_login(self.instructor_participation.user) + post_data = { + "kind": SHOWN_EVENT_KIND, + "ordinal": "4", + "time": "2017-09-14 17:14", + "shown_in_calendar": True, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, HTML_EVENT_CREATED_MSG) + self.assertTotalEventsCountEqual(N_TEST_EVENTS + 1) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS + 1) + + def test_instructor_calendar_edit_delete_success(self): + # Successfully remove an existing event + self.c.force_login(self.instructor_participation.user) + id_to_delete = Event.objects.filter(kind=SHOWN_EVENT_KIND).first().id + post_data = { + "id_to_delete": id_to_delete, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, MSG_HTML_EVENT_DELETED) + self.assertMessageContains(resp, [MSG_HTML_EVENT_DELETED]) + self.assertTotalEventsCountEqual(N_TEST_EVENTS - 1) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS - 1) + + def test_instructor_calendar_edit_delete_non_exist(self): + # Successfully remove an existing event + self.c.force_login(self.instructor_participation.user) + post_data = { + "id_to_delete": 1000, # forgive me + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 404) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + + def test_instructor_calendar_edit_update_success(self): + # Successfully update an existing event + self.c.force_login(self.instructor_participation.user) + all_hidden_events = Event.objects.filter(kind=HIDDEN_EVENT_KIND) + hidden_count_before_update = all_hidden_events.count() + shown_count_before_update = ( + Event.objects.filter(kind=SHOWN_EVENT_KIND).count()) + event_to_edit = all_hidden_events.first() + id_to_edit = event_to_edit.id + post_data = { + "existing_event_to_save": id_to_edit, + "kind": SHOWN_EVENT_KIND, + "ordinal": 10, + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": True, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, MSG_EVENT_UPDATED) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) + self.assertEqual( + Event.objects.filter(kind=HIDDEN_EVENT_KIND).count(), + hidden_count_before_update - 1) + self.assertEqual( + Event.objects.filter(kind=SHOWN_EVENT_KIND).count(), + shown_count_before_update + 1) + + def test_instructor_calendar_edit_update_non_exist_id_to_edit_failure(self): + self.c.force_login(self.instructor_participation.user) + id_to_edit = 1000 # forgive me + post_data = { + "id_to_edit": id_to_edit, + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 404) + + post_data = { + "existing_event_to_save": id_to_edit, + "kind": SHOWN_EVENT_KIND, + "ordinal": 1000, + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": True, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 404) + + def test_instructor_calendar_edit_update_overwrite_exist_id_failure(self): + # Failure to update an existing event to overwrite and existing event + self.c.force_login(self.instructor_participation.user) + event_to_edit = Event.objects.filter(kind=HIDDEN_EVENT_KIND).first() + id_to_edit = event_to_edit.id # forgive me + post_data = { + "existing_event_to_save": id_to_edit, + "kind": HIDDEN_EVENT_NO_ORDINAL, + "ordinal": "", + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": False, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG) + self.assertContains(resp, MSG_EVENT_NOT_UPDATED) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + + def test_no_pperm_edit_event_post_create_fail(self): + self.c.force_login(self.student_participation.user) + post_data = { + "kind": FAILURE_EVENT_KIND, + "ordinal": "1", + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": True, + 'submit': [''] + } + + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 403) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + self.assertEqual(Event.objects.filter(kind=FAILURE_EVENT_KIND).count(), 0) + + def test_no_pperm_edit_event_post_delete_fail(self): + self.c.force_login(self.student_participation.user) + id_to_delete = Event.objects.filter(kind=SHOWN_EVENT_KIND).first().id + post_data = { + "id_to_delete": id_to_delete, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 403) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + + def test_no_pperm_edit_event_post_edit(self): + self.c.force_login(self.student_participation.user) + id_to_edit = 1 + self.assertIsNotNone(Event.objects.get(id=id_to_edit)) + post_data = { + "id_to_edit": id_to_edit, + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 403) + + post_data = { + "existing_event_to_save": id_to_edit, + "kind": FAILURE_EVENT_KIND, + "ordinal": 1000, + "time": "2017-09-14 17:14", # forgive me + "shown_in_calendar": True, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 403) + self.assertEqual(Event.objects.filter(kind=FAILURE_EVENT_KIND).count(), 0) -- GitLab From 59700327ec36c29c1a0db321e11020fdbd00dfa1 Mon Sep 17 00:00:00 2001 From: paulluh Date: Wed, 20 Sep 2017 23:02:08 -0500 Subject: [PATCH 18/24] date time comparable issue (fixed) --- course/calendar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index faa0f7d5..bcb1d309 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -451,7 +451,7 @@ def edit_calendar(pctx): 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 + default_date = event_to_delete.time.date() with transaction.atomic(): event_to_delete.delete() messages.add_message(request, messages.SUCCESS, @@ -513,7 +513,7 @@ def edit_calendar(pctx): else: messages.add_message(request, messages.SUCCESS, _("Event created.")) - default_date = form_event.cleaned_data['time'] + default_date = form_event.cleaned_data['time'].date() events_json = [] from course.content import get_raw_yaml_from_repo -- GitLab From b574f6f3c63d4e8f4299cfc1e28dbcd3f94b2513 Mon Sep 17 00:00:00 2001 From: dzhuang Date: Thu, 21 Sep 2017 14:34:47 +0800 Subject: [PATCH 19/24] Added testcases for event_CRUD_2.0. Allow update no-ordinal Events --- course/calendar.py | 56 +++++++------- course/models.py | 12 +-- test/test_calendar.py | 174 +++++++++++++++++++++++++++++++++--------- 3 files changed, 176 insertions(+), 66 deletions(-) diff --git a/course/calendar.py b/course/calendar.py index bcb1d309..9a9e90e7 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -452,10 +452,19 @@ def edit_calendar(pctx): event_to_delete = get_object_or_404(Event, id=request.POST['id_to_delete']) default_date = event_to_delete.time.date() - with transaction.atomic(): - event_to_delete.delete() - messages.add_message(request, messages.SUCCESS, - _("Event deleted.")) + 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'] @@ -466,8 +475,9 @@ def edit_calendar(pctx): else: init_event = Event(course=pctx.course) + is_editing_existing_event = 'existing_event_to_save' in request.POST - if '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) @@ -478,36 +488,30 @@ def edit_calendar(pctx): try: with transaction.atomic(): form_event.save() - except 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 'existing_event_to_save' in request.POST: + 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)}) - except Exception as e: + messages.add_message(request, messages.ERROR, string_concat( - "%(err_type)s: %(err_str)s. ", - _("No event created.")) + "%(err_type)s: %(err_str)s. ", msg) % { "err_type": type(e).__name__, "err_str": str(e)}) else: - if 'existing_event_to_save' in request.POST: + if is_editing_existing_event: messages.add_message(request, messages.SUCCESS, _("Event updated.")) else: diff --git a/course/models.py b/course/models.py index 52e0fd4a..c160c3df 100644 --- a/course/models.py +++ b/course/models.py @@ -294,11 +294,13 @@ class Event(models.Model): def save(self, *args, **kwargs): # When ordinal is Null, unique_together failed to identify duplicate entries if not self.ordinal: - object_exist = bool( - Event.objects.filter(kind=self.kind, ordinal__isnull=True).count()) - if object_exist: - from django.db import IntegrityError - raise IntegrityError() + 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: diff --git a/test/test_calendar.py b/test/test_calendar.py index 4f59556f..3cddf723 100644 --- a/test/test_calendar.py +++ b/test/test_calendar.py @@ -28,11 +28,17 @@ from django.urls import reverse from course.models import Event from base_test_mixins import SingleCourseTestMixin from django.utils.timezone import now +try: + from unittest import mock +except: + import mock + +DATE_TIME_PICKER_TIME_FORMAT = "%Y-%m-%d %H:%M" SHOWN_EVENT_KIND = "test_open_event" HIDDEN_EVENT_KIND = "test_secret_event" -OPEN_EVENT_NO_ORDINAL = "test_open_no_ordinal" -HIDDEN_EVENT_NO_ORDINAL = "test_secret_no_ordinal" +OPEN_EVENT_NO_ORDINAL_KIND = "test_open_no_ordinal" +HIDDEN_EVENT_NO_ORDINAL_KIND = "test_secret_no_ordinal" FAILURE_EVENT_KIND = "never_created_event_kind" TEST_EVENTS = ( @@ -46,12 +52,17 @@ TEST_EVENTS = ( "shown_in_calendar": False, "time": now()}, {"kind": HIDDEN_EVENT_KIND, "ordinal": "2", "shown_in_calendar": False, "time": now()}, - {"kind": OPEN_EVENT_NO_ORDINAL, + {"kind": OPEN_EVENT_NO_ORDINAL_KIND, "shown_in_calendar": True, "time": now()}, - {"kind": HIDDEN_EVENT_NO_ORDINAL, + {"kind": HIDDEN_EVENT_NO_ORDINAL_KIND, "shown_in_calendar": False, "time": now()}, ) +TEST_NOT_EXIST_EVENT = { + "pk": 1000, + "kind": "DOES_NOT_EXIST_KIND", "ordinal": "1", + "shown_in_calendar": True, "time": now()} + N_TEST_EVENTS = len(TEST_EVENTS) # 7 events N_HIDDEN_EVENTS = len([event for event in TEST_EVENTS @@ -70,12 +81,23 @@ HTML_CREATE_NEW_EVENT_BUTTON_TITLE = "create a new event" HTML_EVENT_CREATED_MSG = "Event created." MSG_EVENT_NOT_CREATED = "No event created." -MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG = "EventAlreadyExists:" +MSG_PREFIX_EVENT_ALREADY_EXIST_FAILURE = "EventAlreadyExists:" +MSG_PREFIX_EVENT_NOT_DELETED_FAILURE = "No event deleted:" MSG_HTML_EVENT_DELETED = "Event deleted." MSG_EVENT_UPDATED = "Event updated." MSG_EVENT_NOT_UPDATED = "Event not updated." +def get_object_or_404_side_effect(klass, *args, **kwargs): + """ + Delete an existing object from db after get + """ + from django.shortcuts import get_object_or_404 + obj = get_object_or_404(klass, *args, **kwargs) + obj.delete() + return obj + + class CalendarTest(SingleCourseTestMixin, TestCase): @classmethod def setUpTestData(cls): # noqa @@ -97,6 +119,11 @@ class CalendarTest(SingleCourseTestMixin, TestCase): Event.objects.create(**event) assert Event.objects.count() == N_TEST_EVENTS + def set_course_end_date(self): + from datetime import timedelta + self.course.end_date = now() + timedelta(days=120) + self.course.save() + def assertShownEventsCountEqual(self, resp, expected_shown_events_count): # noqa self.assertEqual( len(json.loads(resp.context["events_json"])), @@ -213,11 +240,31 @@ class CalendarTest(SingleCourseTestMixin, TestCase): def test_instructor_calendar_edit_create_exist_failure(self): self.c.force_login(self.instructor_participation.user) - # Failing to create events already exist + # Failing to create event already exist post_data = { "kind": SHOWN_EVENT_KIND, "ordinal": "3", - "time": "2017-09-14 17:14", # forgive me + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), + "shown_in_calendar": True, + 'submit': [''] + } + + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, MSG_PREFIX_EVENT_ALREADY_EXIST_FAILURE) + self.assertContains(resp, MSG_EVENT_NOT_CREATED) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) + + def test_instructor_calendar_edit_create_exist_no_ordinal_event_faliure(self): + self.c.force_login(self.instructor_participation.user) + # Failing to create event (no ordinal) already exist + post_data = { + "kind": OPEN_EVENT_NO_ORDINAL_KIND, + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), "shown_in_calendar": True, 'submit': [''] } @@ -227,7 +274,7 @@ class CalendarTest(SingleCourseTestMixin, TestCase): post_data ) self.assertEqual(resp.status_code, 200) - self.assertContains(resp, MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG) + self.assertContains(resp, MSG_PREFIX_EVENT_ALREADY_EXIST_FAILURE) self.assertContains(resp, MSG_EVENT_NOT_CREATED) self.assertTotalEventsCountEqual(N_TEST_EVENTS) self.assertShownEventsCountEqual(resp, N_TEST_EVENTS) @@ -238,7 +285,27 @@ class CalendarTest(SingleCourseTestMixin, TestCase): post_data = { "kind": SHOWN_EVENT_KIND, "ordinal": "4", - "time": "2017-09-14 17:14", + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), + "shown_in_calendar": True, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, HTML_EVENT_CREATED_MSG) + self.assertTotalEventsCountEqual(N_TEST_EVENTS + 1) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS + 1) + + def test_instructor_calendar_edit_post_create_for_course_has_end_date(self): + # Successfully create new event + self.set_course_end_date() + self.c.force_login(self.instructor_participation.user) + post_data = { + "kind": SHOWN_EVENT_KIND, + "ordinal": "4", + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), "shown_in_calendar": True, 'submit': [''] } @@ -269,6 +336,25 @@ class CalendarTest(SingleCourseTestMixin, TestCase): self.assertTotalEventsCountEqual(N_TEST_EVENTS - 1) self.assertShownEventsCountEqual(resp, N_TEST_EVENTS - 1) + def test_instructor_calendar_edit_delete_for_course_has_end_date(self): + # Successfully remove an existing event + self.set_course_end_date() + self.c.force_login(self.instructor_participation.user) + id_to_delete = Event.objects.filter(kind=SHOWN_EVENT_KIND).first().id + post_data = { + "id_to_delete": id_to_delete, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, MSG_HTML_EVENT_DELETED) + self.assertMessageContains(resp, [MSG_HTML_EVENT_DELETED]) + self.assertTotalEventsCountEqual(N_TEST_EVENTS - 1) + self.assertShownEventsCountEqual(resp, N_TEST_EVENTS - 1) + def test_instructor_calendar_edit_delete_non_exist(self): # Successfully remove an existing event self.c.force_login(self.instructor_participation.user) @@ -283,6 +369,25 @@ class CalendarTest(SingleCourseTestMixin, TestCase): self.assertEqual(resp.status_code, 404) self.assertTotalEventsCountEqual(N_TEST_EVENTS) + @mock.patch("course.calendar.get_object_or_404", + side_effect=get_object_or_404_side_effect) + def test_instructor_calendar_edit_delete_deleted_event_before_transaction( + self, mocked_get_object_or_404): + # Deleting event which exist when get and was deleted before transaction + self.c.force_login(self.instructor_participation.user) + id_to_delete = Event.objects.filter(kind=SHOWN_EVENT_KIND).first().id + post_data = { + "id_to_delete": id_to_delete, + 'submit': [''] + } + resp = self.c.post( + reverse("relate-edit_calendar", args=[self.course.identifier]), + post_data + ) + self.assertContains(resp, MSG_PREFIX_EVENT_NOT_DELETED_FAILURE) + self.assertEqual(resp.status_code, 200) + self.assertTotalEventsCountEqual(N_TEST_EVENTS - 1) + def test_instructor_calendar_edit_update_success(self): # Successfully update an existing event self.c.force_login(self.instructor_participation.user) @@ -296,7 +401,7 @@ class CalendarTest(SingleCourseTestMixin, TestCase): "existing_event_to_save": id_to_edit, "kind": SHOWN_EVENT_KIND, "ordinal": 10, - "time": "2017-09-14 17:14", # forgive me + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), "shown_in_calendar": True, 'submit': [''] } @@ -315,25 +420,32 @@ class CalendarTest(SingleCourseTestMixin, TestCase): Event.objects.filter(kind=SHOWN_EVENT_KIND).count(), shown_count_before_update + 1) - def test_instructor_calendar_edit_update_non_exist_id_to_edit_failure(self): + def test_instructor_calendar_edit_update_no_ordinal_event_success(self): + # Failure to update an existing event to overwrite and existing event self.c.force_login(self.instructor_participation.user) - id_to_edit = 1000 # forgive me + event_to_edit = Event.objects.filter(kind=HIDDEN_EVENT_KIND).first() + id_to_edit = event_to_edit.id # forgive me post_data = { - "id_to_edit": id_to_edit, + "existing_event_to_save": id_to_edit, + "kind": HIDDEN_EVENT_NO_ORDINAL_KIND, + "ordinal": "", + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), + "shown_in_calendar": False, + 'submit': [''] } resp = self.c.post( reverse("relate-edit_calendar", args=[self.course.identifier]), post_data ) - self.assertEqual(resp.status_code, 404) + self.assertEqual(resp.status_code, 200) + self.assertMessageContains(resp, MSG_EVENT_UPDATED) + self.assertTotalEventsCountEqual(N_TEST_EVENTS) + def test_instructor_calendar_edit_update_non_exist_id_to_edit_failure(self): + self.c.force_login(self.instructor_participation.user) + id_to_edit = 1000 # forgive me post_data = { - "existing_event_to_save": id_to_edit, - "kind": SHOWN_EVENT_KIND, - "ordinal": 1000, - "time": "2017-09-14 17:14", # forgive me - "shown_in_calendar": True, - 'submit': [''] + "id_to_edit": id_to_edit, } resp = self.c.post( reverse("relate-edit_calendar", args=[self.course.identifier]), @@ -341,34 +453,26 @@ class CalendarTest(SingleCourseTestMixin, TestCase): ) self.assertEqual(resp.status_code, 404) - def test_instructor_calendar_edit_update_overwrite_exist_id_failure(self): - # Failure to update an existing event to overwrite and existing event - self.c.force_login(self.instructor_participation.user) - event_to_edit = Event.objects.filter(kind=HIDDEN_EVENT_KIND).first() - id_to_edit = event_to_edit.id # forgive me post_data = { "existing_event_to_save": id_to_edit, - "kind": HIDDEN_EVENT_NO_ORDINAL, - "ordinal": "", - "time": "2017-09-14 17:14", # forgive me - "shown_in_calendar": False, + "kind": SHOWN_EVENT_KIND, + "ordinal": 1000, + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), + "shown_in_calendar": True, 'submit': [''] } resp = self.c.post( reverse("relate-edit_calendar", args=[self.course.identifier]), post_data ) - self.assertEqual(resp.status_code, 200) - self.assertContains(resp, MSG_PREFIX_EVENT_ALREAD_EXIST_FAILURE_MSG) - self.assertContains(resp, MSG_EVENT_NOT_UPDATED) - self.assertTotalEventsCountEqual(N_TEST_EVENTS) + self.assertEqual(resp.status_code, 404) def test_no_pperm_edit_event_post_create_fail(self): self.c.force_login(self.student_participation.user) post_data = { "kind": FAILURE_EVENT_KIND, "ordinal": "1", - "time": "2017-09-14 17:14", # forgive me + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), "shown_in_calendar": True, 'submit': [''] } @@ -412,7 +516,7 @@ class CalendarTest(SingleCourseTestMixin, TestCase): "existing_event_to_save": id_to_edit, "kind": FAILURE_EVENT_KIND, "ordinal": 1000, - "time": "2017-09-14 17:14", # forgive me + "time": now().strftime(DATE_TIME_PICKER_TIME_FORMAT), "shown_in_calendar": True, 'submit': [''] } -- GitLab From 8f937b7b415b09ca55579fbdf41909066caedf9a Mon Sep 17 00:00:00 2001 From: dzhuang Date: Mon, 25 Sep 2017 18:39:07 +0800 Subject: [PATCH 20/24] Remove dependency of eonasdan-bootstrap-datetimepicker --- course/calendar.py | 10 +++------- ...d_help_text_for_event_shown_in_calendar.py | 20 +++++++++++++++++++ course/models.py | 1 + course/templates/course/calendar.html | 4 ---- 4 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 course/migrations/0105_add_help_text_for_event_shown_in_calendar.py diff --git a/course/calendar.py b/course/calendar.py index 9a9e90e7..9631fc9c 100644 --- a/course/calendar.py +++ b/course/calendar.py @@ -420,15 +420,11 @@ class EditEventForm(StyledModelForm): model = Event fields = ['kind', 'ordinal', 'time', 'end_time', 'all_day', 'shown_in_calendar'] - help_texts = { - 'shown_in_calendar': ('Shown in students\' calendar') + widgets = { + "time": DateTimePicker(options={"format": "YYYY-MM-DD HH:mm"}), + "end_time": DateTimePicker(options={"format": "YYYY-MM-DD HH:mm"}), } - def __init__(self, *args, **kwargs): - super(EditEventForm, self).__init__(*args, **kwargs) - self.fields['time'].widget.attrs = {'id': 'start_time'} - self.fields['end_time'].widget.attrs = {'id': 'end_time'} - @login_required @course_view diff --git a/course/migrations/0105_add_help_text_for_event_shown_in_calendar.py b/course/migrations/0105_add_help_text_for_event_shown_in_calendar.py new file mode 100644 index 00000000..87341f6d --- /dev/null +++ b/course/migrations/0105_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', '0104_add_skip_during_manual_grading_permission_to_roles'), + ] + + 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 c160c3df..a11a13d0 100644 --- a/course/models.py +++ b/course/models.py @@ -277,6 +277,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: diff --git a/course/templates/course/calendar.html b/course/templates/course/calendar.html index 2c9f661d..31ac258f 100644 --- a/course/templates/course/calendar.html +++ b/course/templates/course/calendar.html @@ -15,10 +15,6 @@ {# load calendar with local language #} {% get_current_language as LANGUAGE_CODE %} - {% if edit_view %} - - - {% endif %} {% endblock %} {% block content %} -- GitLab From d6c1dd4a563426528c24fc238be3958b41141794 Mon Sep 17 00:00:00 2001 From: dzhuang Date: Thu, 5 Oct 2017 14:46:29 +0800 Subject: [PATCH 21/24] Added confirmation before delete event. --- course/templates/course/calendar.html | 37 +++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/course/templates/course/calendar.html b/course/templates/course/calendar.html index 31ac258f..036fc564 100644 --- a/course/templates/course/calendar.html +++ b/course/templates/course/calendar.html @@ -91,7 +91,6 @@ -
    {% csrf_token %} @@ -117,14 +116,42 @@ events: {{ events_json|safe }}, {% if edit_view %} eventRender: function(event, element) { - element.find('.fc-title').append( ''); + element.find('.fc-title').append( ''); element.find('.fc-title').append( ' '); element.find("#edit").click(function() { edit_existing_event(event.id); }); - element.find("#remove").click(function() { - delete_existing_event(event.id); - }) + element.find("span[data-confirm]").click(function() { + var event_id = $(this).data('event-id'); + if (!$('#deleteConfirmModal').length) { + $('body').append( + ''); + } + var $delete_confirm_modal = $('#deleteConfirmModal'); + $delete_confirm_modal.find('.modal-body').text($(this).attr('data-confirm')); + $('#deleteConfirmOK').data('event-id', event_id).click(function() { + delete_existing_event($(this).data('event-id')); + }); + $delete_confirm_modal.modal({show:true}); + return false; + }); } {% endif %} }); -- GitLab From 61b82acd4715222b12d4f981bd3a4e4d1b901e99 Mon Sep 17 00:00:00 2001 From: paulluh Date: Sun, 15 Oct 2017 11:50:38 -0500 Subject: [PATCH 22/24] change to a responsive modal width --- course/templates/course/calendar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/templates/course/calendar.html b/course/templates/course/calendar.html index 036fc564..f8ea6517 100644 --- a/course/templates/course/calendar.html +++ b/course/templates/course/calendar.html @@ -60,7 +60,7 @@ {% if edit_view %}