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 %}
{% for event_info in event_info_list %}
@@ -58,5 +58,126 @@
{% endfor%}
+ {% if edit_view %}
+
+
+
+
+ {% endif %}
{% endblock %}
+{% block page_bottom_javascript_extra %}
+
+ {{ block.super }}
+{% endblock %}
\ No newline at end of file
diff --git a/course/templates/course/course-base.html b/course/templates/course/course-base.html
index ff0e31a0e0aaeb072de2a0bfeae4a2dcc78df0e6..a3c11a30f0e7769b5dce83ddda0943e7f34db485 100644
--- a/course/templates/course/course-base.html
+++ b/course/templates/course/course-base.html
@@ -128,9 +128,10 @@
{% if pperm.edit_events %}
- {% if user.is_staff %}
-
{% trans "Edit events" %}
+ {% if perms.course.add_event and perms.course.change_event and perms.course.delete_event %}
+
{% trans "Edit events (admin)" %}
{% endif %}
+
{% trans "Edit events (calendar)" %}
{% trans "Create recurring events" %}
{% trans "Renumber events" %}
{% endif %}
diff --git a/relate/urls.py b/relate/urls.py
index ddef2e932cd69b72e624c97b385409612fb6a5d3..48f9cefd1a5349dfe3041b2557f092e3bc6461e5 100644
--- a/relate/urls.py
+++ b/relate/urls.py
@@ -346,6 +346,12 @@ urlpatterns = [
course.calendar.view_calendar,
name="relate-view_calendar"),
+ url(r"^course"
+ "/" + COURSE_ID_REGEX +
+ "/calendar-edit/$",
+ course.calendar.edit_calendar,
+ name="relate-edit_calendar"),
+
# }}}
# {{{ versioning
diff --git a/tests/test_calendar.py b/tests/test_calendar.py
new file mode 100644
index 0000000000000000000000000000000000000000..84229d16fc148f86ef7d0523ee2c611f5ff07a64
--- /dev/null
+++ b/tests/test_calendar.py
@@ -0,0 +1,554 @@
+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 django.contrib import messages
+from django.utils.timezone import now
+
+from course.models import Event
+from .base_test_mixins import (
+ SingleCourseTestMixin, FallBackStorageMessageTestMixin)
+try:
+ from unittest import mock
+except ImportError:
+ 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_KIND = "test_open_no_ordinal"
+HIDDEN_EVENT_NO_ORDINAL_KIND = "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_KIND,
+ "shown_in_calendar": True, "time": now()},
+ {"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
+ 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"
+
+MESSAGE_EVENT_CREATED_TEXT = "Event created."
+MESSAGE_EVENT_NOT_CREATED_TEXT = "No event created."
+MESSAGE_PREFIX_EVENT_ALREADY_EXIST_FAILURE_TEXT = "EventAlreadyExists:"
+MESSAGE_PREFIX_EVENT_NOT_DELETED_FAILURE_TEXT = "No event deleted:"
+MESSAGE_EVENT_DELETED_TEXT = "Event deleted."
+MESSAGE_EVENT_UPDATED_TEXT = "Event updated."
+MESSAGE_EVENT_NOT_UPDATED_TEXT = "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 CalendarTestMixin(object):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ super(CalendarTestMixin, cls).setUpTestData()
+
+ # superuser was previously removed from participation, now we add him back
+ from course.constants import participation_status
+
+ cls.create_participation(
+ cls.course,
+ cls.superuser,
+ 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 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"])),
+ expected_shown_events_count)
+
+ def assertTotalEventsCountEqual(self, expected_total_events_count): # noqa
+ self.assertEqual(Event.objects.count(), expected_total_events_count)
+
+
+class CalendarTest(CalendarTestMixin, SingleCourseTestMixin,
+ FallBackStorageMessageTestMixin, TestCase):
+
+ 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)
+ self.assertRegexpMatches
+
+ # 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 event already exist
+ post_data = {
+ "kind": SHOWN_EVENT_KIND,
+ "ordinal": "3",
+ "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)
+ expected_regex = (
+ "%s.+%s" % (
+ MESSAGE_PREFIX_EVENT_ALREADY_EXIST_FAILURE_TEXT,
+ MESSAGE_EVENT_NOT_CREATED_TEXT))
+ self.assertResponseMessagesEqualRegex(
+ resp, [expected_regex])
+ self.assertResponseMessageLevelsEqual(
+ resp, [messages.ERROR])
+ 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': ['']
+ }
+
+ resp = self.c.post(
+ reverse("relate-edit_calendar", args=[self.course.identifier]),
+ post_data
+ )
+ self.assertEqual(resp.status_code, 200)
+ expected_regex = (
+ "%s.+%s" % (
+ MESSAGE_PREFIX_EVENT_ALREADY_EXIST_FAILURE_TEXT,
+ MESSAGE_EVENT_NOT_CREATED_TEXT))
+ self.assertResponseMessagesEqualRegex(
+ resp, [expected_regex])
+ self.assertResponseMessageLevelsEqual(
+ resp, [messages.ERROR])
+ 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": 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.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_CREATED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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': ['']
+ }
+ resp = self.c.post(
+ reverse("relate-edit_calendar", args=[self.course.identifier]),
+ post_data
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_CREATED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_DELETED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_DELETED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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)
+
+ @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
+ )
+ expectec_regex = "%s.+" % MESSAGE_PREFIX_EVENT_NOT_DELETED_FAILURE_TEXT
+ self.assertResponseMessagesEqualRegex(resp, expectec_regex)
+ self.assertResponseMessageLevelsEqual(resp, [messages.ERROR])
+ 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)
+ 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": 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.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_UPDATED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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_no_ordinal_event_success(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_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, 200)
+ self.assertResponseMessagesEqual(resp, [MESSAGE_EVENT_UPDATED_TEXT])
+ self.assertResponseMessageLevelsEqual(resp, [messages.SUCCESS])
+ 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 = {
+ "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": 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, 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": 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, 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": 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, 403)
+ self.assertEqual(Event.objects.filter(kind=FAILURE_EVENT_KIND).count(), 0)