Skip to content

Commit

Permalink
Merge pull request #692 from drnlm/feature/update_validation_info_in_…
Browse files Browse the repository at this point in the history
…schedule_editor

Feature/update validation info in schedule editor
  • Loading branch information
drnlm authored Dec 1, 2023
2 parents fbb744b + c09599e commit 3c1ea8e
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 35 deletions.
22 changes: 15 additions & 7 deletions wafer/schedule/templates/wafer.schedule/edit_schedule.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
{% extends "wafer/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}Edit Schedule - {{ WAFER_CONFERENCE_NAME }}{% endblock %}
{% block title %}{% trans "Edit Schedule" %} - {{ WAFER_CONFERENCE_NAME }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
{% if validation_errors %}
<div class="messages alert alert-danger">
<div class="messages alert alert-danger"
{% comment %}
We create this unconditionally, and hide it if there are no validation
errors, so it can be updated as required by the editor
{% endcomment %}
{% if not validation_errors %}
hidden
{% endif %}
id="validationMessages">
<p><strong>{% trans "Validation errors:" %}</strong></p>
<ul>
{% for validation_error in validation_errors %}
<li>{{ validation_error }}</li>
{% endfor %}
{% if validation_errors %}
{% for validation_error in validation_errors %}
<li>{{ validation_error }}</li>
{% endfor %}
{% endif %}
</ul>
</div>
{% endif %}
</div>
<div class="col-md-8">
<div class="dropdown float-end">
Expand Down
40 changes: 16 additions & 24 deletions wafer/schedule/tests/test_schedule_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,12 +452,9 @@ def test_drag_over_page(self):
after.append(x.text)
self.assertEqual(before, after)

@expectedFailure
def test_adding_clash(self):
"""Test that introducing a speaker clash causes the
error section to be updated"""
# Expected to fail -see https://github.com/CTPUG/wafer/issues/158
# Create initial schedule
item1 = ScheduleItem.objects.create(venue=self.venues[0],
talk_id=self.talk1.pk)
item1.slots.add(self.block1_slots[0])
Expand All @@ -468,8 +465,8 @@ def test_adding_clash(self):
item2.save()
self._start()
# Verify that there are no validation errors
with self.assertRaises(NoSuchElementException):
self.driver.find_element(By.TAG_NAME, "strong")
validation = self.driver.find_element(By.CLASS_NAME, "alert-danger")
self.assertFalse(validation.is_displayed())
# Drag a talk into a clashing slot
target = self.driver.find_element(By.ID, f"scheduleItem{item2.pk}")
talks_link = self.driver.find_element(By.PARTIAL_LINK_TEXT, "Unassigned Talks")
Expand All @@ -495,21 +492,14 @@ def test_adding_clash(self):
actions.drag_and_drop(source, target)
actions.pause(0.5)
actions.perform()
# Verify errors are present
# FIXME: The schedule editor doesn't currently update this
try:
vaidation = self.driver.find_element(By.TAG_NAME, "strong")
except NoSuchElementException:
vaidation = None
self.assertNotNone(vaidation)
self.assertIn('Validation errors:', validation.text)
# Verify error block is displayed
self.assertTrue(validation.is_displayed())
error_item = validation.find_element(By.TAG_NAME, "li")
self.assertIn('Common speaker', error_item.text)

@expectedFailure
def test_removing_clash(self):
"""Test that removing a speaker clash causes the
error section to be cleared"""
# Expected to fail -see https://github.com/CTPUG/wafer/issues/158
# Create initial schedule
item1 = ScheduleItem.objects.create(venue=self.venues[0],
talk_id=self.talk1.pk)
item1.slots.add(self.block1_slots[0])
Expand All @@ -519,9 +509,11 @@ def test_removing_clash(self):
item2.slots.add(self.block1_slots[0])
item2.save()
self._start()
# Verify that there are no validation errors
validation = self.driver.find_element(By.TAG_NAME, "strong")
self.assertIn('Validation errors:', validation.text)
# Verify that there are validation errors
validation = self.driver.find_element(By.CLASS_NAME, "alert-danger")
self.assertTrue(validation.is_displayed())
error_item = validation.find_element(By.TAG_NAME, "li")
self.assertIn('Common speaker', error_item.text)
# Delete the clashing talk
target = self.driver.find_element(By.ID, f"scheduleItem{item2.pk}")
close = target.find_element(By.CLASS_NAME, 'close')
Expand All @@ -535,16 +527,16 @@ def test_removing_clash(self):
break
# Verify we've deleted the clashing talk
found = None
# Find the second talk and verify that talk 1 is not in
# the Unassigned Talk list
# Find the deleted talk in the Unassigned Talk list
for x in tab_pane.find_elements(By.CLASS_NAME, 'draggable'):
if self.talk4.title in x.text:
found = True
self.assertTrue(found)
# Verify errors are gone
# FIXME: The schedule editor doesn't currently update this
# Verify errors are hidden
self.assertFalse(validation.is_displayed())
# Check that the list has been removed
with self.assertRaises(NoSuchElementException):
self.driver.find_element(By.TAG_NAME, "strong")
validation.find_element(By.TAG_NAME,"li")


class ChromeScheduleEditorTests(EditorTestsMixin, ChromeTestRunner):
Expand Down
98 changes: 95 additions & 3 deletions wafer/schedule/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from wafer.schedule.admin import (find_clashes, find_invalid_venues, validate_items,
find_duplicate_schedule_items, find_speaker_clashes,
prefetch_schedule_items)
from wafer.schedule.tests.test_views import make_pages, make_items
from wafer.schedule.tests.test_views import make_pages, make_items, create_client


def get_new_result(result, old_result):
Expand All @@ -24,7 +24,6 @@ def get_new_result(result, old_result):
class ScheduleValidationTests(TestCase):
"""Test that various validators pick up the correct errors"""


def setUp(self):
"""Create some blocks, slots and talks to work with"""
timezone.activate('UTC')
Expand Down Expand Up @@ -265,7 +264,7 @@ def test_find_speaker_clashes(self):
self.pages[7].people.add(self.author3)
self.pages[7].people.add(self.author2)
self.pages[7].save()

all_items = prefetch_schedule_items()
result = list(find_speaker_clashes(all_items))

Expand All @@ -291,3 +290,96 @@ def test_find_speaker_clashes(self):
self.assertIn(self.slots[3], new_result[0][0])
self.assertIn(self.items[6], new_result[0][1])
self.assertIn(self.items[7], new_result[0][1])


class ScheduleValidationApiTests(TestCase):
"""Test that validation status can be checked via the api"""

def setUp(self):
"""Create some blocks, slots and talks to work with"""
timezone.activate('UTC')
self.block1 = ScheduleBlock.objects.create(
start_time=D.datetime(2013, 9, 22, 9, 0, 0,
tzinfo=D.timezone.utc),
end_time=D.datetime(2013, 9, 22, 19, 0, 0,
tzinfo=D.timezone.utc))

self.venue1 = Venue.objects.create(order=1, name='Venue 1')
self.venue2 = Venue.objects.create(order=2, name='Venue 2')
self.venue1.blocks.add(self.block1)
self.venue2.blocks.add(self.block1)

start1 = D.datetime(2013, 9, 22, 10, 0, 0,
tzinfo=D.timezone.utc)
start2 = D.datetime(2013, 9, 22, 11, 0, 0,
tzinfo=D.timezone.utc)
start3 = D.datetime(2013, 9, 22, 12, 0, 0,
tzinfo=D.timezone.utc)
self.slots = []
self.slots.append(Slot.objects.create(start_time=start1,
end_time=start2))
self.slots.append(Slot.objects.create(start_time=start2,
end_time=start3))
self.pages = make_pages(4)
venues = [self.venue1, self.venue2] * 2
self.items = make_items(venues, self.pages)

for index, item in enumerate(self.items):
item.slots.add(self.slots[index // 2])

self.author1 = create_user('Author 1')
self.author2 = create_user('Author 2')

self.talk1 = create_talk('Talk 1', ACCEPTED, user=self.author1)
self.talk2 = create_talk('Talk 2', ACCEPTED, user=self.author2)
self.talk3 = create_talk('Talk 3', ACCEPTED, user=self.author1)

def test_normal_permissions(self):
"""Check that we don't allow non-superuser access"""
c = create_client('ordinary', superuser=False)
response = c.get('/schedule/api/validate/')
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data, {
"detail": "You do not have permission to perform this action.",
})

def test_no_errors(self):
"""Check that we get no errors on a valid schedule"""
c = create_client('super', superuser=True)
response = c.get('/schedule/api/validate/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'Validation Status': []})

def test_find_duplicates(self):
"""Test that duplicates are reported"""
# Create a duplicate item
duplicate_item = ScheduleItem.objects.create(venue=self.venue2,
details="Duplicate item",
page_id=self.pages[0].pk)
duplicate_item.slots.add(self.slots[1])
c = create_client('super', superuser=True)
response = c.get('/schedule/api/validate/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['Validation Status']), 1)
self.assertIn('Clashes found in schedule', response.data['Validation Status'][0])

def test_find_speaker_clashes(self):
"""Test that speaker clashes are reported"""
# Check two talks witht the same author at the same time fails
self.items[0].page_id = None
self.items[0].talk_id = self.talk1.pk
self.items[0].save()

self.items[2].page_id = None
self.items[2].talk_id = self.talk2.pk
self.items[2].save()

self.items[1].page_id = None
self.items[1].talk_id = self.talk3.pk
self.items[1].save()
c = create_client('super', superuser=True)
response = c.get('/schedule/api/validate/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['Validation Status']), 1)
self.assertIn('Common speaker in simultaneous schedule items',
response.data['Validation Status'][0])
3 changes: 2 additions & 1 deletion wafer/schedule/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from wafer.schedule.views import (
CurrentView, ScheduleView, ScheduleItemViewSet, ScheduleXmlView,
VenueView, ICalView, JsonDataView)
VenueView, ICalView, JsonDataView, get_validation_info)

router = routers.DefaultRouter()
router.register(r'scheduleitems', ScheduleItemViewSet)
Expand All @@ -17,5 +17,6 @@
name='wafer_pentabarf_xml'),
re_path(r'^schedule\.ics$', ICalView.as_view(), name="schedule.ics"),
re_path(r'^schedule\.json$', JsonDataView.as_view(), name="schedule.json"),
re_path(r'^api/validate', get_validation_info),
re_path(r'^api/', include(router.urls)),
]
12 changes: 12 additions & 0 deletions wafer/schedule/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from bakery.views import BuildableDetailView, BuildableTemplateView, BuildableMixin
from rest_framework import viewsets
from rest_framework.permissions import IsAdminUser
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response

from wafer import __version__
from wafer.pages.models import Page
from wafer.schedule.models import (
Expand Down Expand Up @@ -345,6 +348,15 @@ def get_context_data(self, **kwargs):
return context


@api_view(['GET'])
@permission_classes([IsAdminUser])
def get_validation_info(request):
"""API wrapper around validate schedule for use in the schedule
editor"""
errors = validate_schedule()
return Response({'Validation Status': errors})


class ScheduleItemViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
Expand Down
32 changes: 32 additions & 0 deletions wafer/static/js/edit_schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@
e.target.classList.remove('over');
}

function updateValidation(validation_response) {
// update the information in the validation errors section
// if required
var messageArea = document.getElementById("validationMessages");
var list = messageArea.children[1];

// Clear any current entries
while (list.hasChildNodes()) {
list.removeChild(list.firstChild);
}

var errors = validation_response['Validation Status'];
if (errors.length)
{
messageArea.hidden = false;
for (let i = 0; i < errors.length; i++) {
var node = document.createElement("li");
var text = document.createTextNode(errors[i]);
node.appendChild(text);
list.appendChild(node);
}
}
else
{
messageArea.hidden = true;
}
}

function handleItemUpdate(data) {
console.log(data);
var scheduleItemId = data.id;
Expand Down Expand Up @@ -90,11 +118,15 @@
closeButton.appendChild(buttonSpan);
closeButton.addEventListener('click', handleClickDelete, false);
newItem.insertBefore(closeButton, newItem.childNodes[0]);

$.get('/schedule/api/validate/', updateValidation);
}


function handleItemDelete() {
console.log(this);

$.get('/schedule/api/validate/', updateValidation);
}

function handleDrop(e) {
Expand Down

0 comments on commit 3c1ea8e

Please sign in to comment.