2015-07-18 07:09:17 +00:00
|
|
|
from __future__ import unicode_literals
|
2014-09-21 01:20:04 +00:00
|
|
|
import json
|
2016-12-23 09:12:38 +00:00
|
|
|
import pytz
|
2014-09-21 01:20:04 +00:00
|
|
|
|
|
|
|
from django.core.urlresolvers import reverse
|
2012-10-09 19:07:55 +00:00
|
|
|
from django.http import Http404, HttpResponse
|
2012-08-31 04:55:37 +00:00
|
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
2012-10-09 19:07:55 +00:00
|
|
|
from django.template import loader, Context
|
2016-12-23 09:12:38 +00:00
|
|
|
from django.conf import settings
|
2012-08-30 06:21:48 +00:00
|
|
|
|
2015-06-15 09:55:54 +00:00
|
|
|
from django.contrib.auth.models import User
|
2014-02-28 15:55:54 +00:00
|
|
|
from django.contrib import messages
|
2014-09-21 01:20:04 +00:00
|
|
|
from django.contrib.sites.models import Site
|
2012-08-31 05:16:30 +00:00
|
|
|
|
2016-12-22 01:00:23 +00:00
|
|
|
from django_ical.views import ICalFeed
|
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
from account.decorators import login_required
|
|
|
|
|
2014-02-28 15:55:54 +00:00
|
|
|
from symposion.schedule.forms import SlotEditForm, ScheduleSectionForm
|
2015-06-15 09:55:54 +00:00
|
|
|
from symposion.schedule.models import Schedule, Day, Slot, Presentation, Session, SessionRole
|
2012-08-30 06:52:50 +00:00
|
|
|
from symposion.schedule.timetable import TimeTable
|
2012-08-30 06:21:48 +00:00
|
|
|
|
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
def fetch_schedule(slug):
|
2012-08-30 06:21:48 +00:00
|
|
|
qs = Schedule.objects.all()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-30 06:23:49 +00:00
|
|
|
if slug is None:
|
2012-09-21 02:38:24 +00:00
|
|
|
if qs.count() > 1:
|
|
|
|
raise Http404()
|
2012-08-30 06:21:48 +00:00
|
|
|
schedule = next(iter(qs), None)
|
|
|
|
if schedule is None:
|
|
|
|
raise Http404()
|
|
|
|
else:
|
2012-09-20 02:03:30 +00:00
|
|
|
schedule = get_object_or_404(qs, section__slug=slug)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
return schedule
|
|
|
|
|
|
|
|
|
2012-12-20 06:49:32 +00:00
|
|
|
def schedule_conference(request):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2016-03-15 16:08:58 +00:00
|
|
|
if request.user.is_staff:
|
|
|
|
schedules = Schedule.objects.filter(hidden=False)
|
|
|
|
else:
|
|
|
|
schedules = Schedule.objects.filter(published=True, hidden=False)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-12-20 06:49:32 +00:00
|
|
|
sections = []
|
|
|
|
for schedule in schedules:
|
|
|
|
days_qs = Day.objects.filter(schedule=schedule)
|
|
|
|
days = [TimeTable(day) for day in days_qs]
|
|
|
|
sections.append({
|
|
|
|
"schedule": schedule,
|
|
|
|
"days": days,
|
|
|
|
})
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2016-12-24 04:24:31 +00:00
|
|
|
day_switch = request.GET.get('day', None)
|
2012-12-20 06:49:32 +00:00
|
|
|
ctx = {
|
|
|
|
"sections": sections,
|
2016-12-24 04:24:31 +00:00
|
|
|
"day_switch": day_switch
|
2012-12-20 06:49:32 +00:00
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/schedule_conference.html", ctx)
|
2012-12-20 06:49:32 +00:00
|
|
|
|
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
def schedule_detail(request, slug=None):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
schedule = fetch_schedule(slug)
|
2013-01-29 08:13:41 +00:00
|
|
|
if not schedule.published and not request.user.is_staff:
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-21 01:42:03 +00:00
|
|
|
days_qs = Day.objects.filter(schedule=schedule)
|
|
|
|
days = [TimeTable(day) for day in days_qs]
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-30 06:21:48 +00:00
|
|
|
ctx = {
|
|
|
|
"schedule": schedule,
|
2012-09-21 01:42:03 +00:00
|
|
|
"days": days,
|
2012-08-30 06:21:48 +00:00
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/schedule_detail.html", ctx)
|
2012-08-30 06:21:48 +00:00
|
|
|
|
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
def schedule_list(request, slug=None):
|
|
|
|
schedule = fetch_schedule(slug)
|
2016-02-20 13:20:09 +00:00
|
|
|
if not schedule.published and not request.user.is_staff:
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
presentations = Presentation.objects.filter(section=schedule.section)
|
2012-12-20 06:53:22 +00:00
|
|
|
presentations = presentations.exclude(cancelled=True)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2016-09-18 01:17:15 +00:00
|
|
|
if not request.user.is_staff:
|
|
|
|
presentations = presentations.exclude(unpublish=True)
|
|
|
|
|
2012-08-31 05:50:00 +00:00
|
|
|
ctx = {
|
2012-09-20 02:06:15 +00:00
|
|
|
"schedule": schedule,
|
2012-08-31 05:50:00 +00:00
|
|
|
"presentations": presentations,
|
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/schedule_list.html", ctx)
|
2012-08-31 05:50:00 +00:00
|
|
|
|
|
|
|
|
2012-10-09 19:07:55 +00:00
|
|
|
def schedule_list_csv(request, slug=None):
|
|
|
|
schedule = fetch_schedule(slug)
|
2016-02-20 13:20:09 +00:00
|
|
|
if not schedule.published and not request.user.is_staff:
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-10-09 19:07:55 +00:00
|
|
|
presentations = Presentation.objects.filter(section=schedule.section)
|
2016-09-18 01:17:15 +00:00
|
|
|
presentations = presentations.exclude(cancelled=True)
|
|
|
|
if not request.user.is_staff:
|
|
|
|
presentations = presentations.exclude(unpublish=True)
|
|
|
|
presentations = presentations.order_by("id")
|
2014-01-15 14:05:08 +00:00
|
|
|
response = HttpResponse(content_type="text/csv")
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-10-09 19:07:55 +00:00
|
|
|
if slug:
|
|
|
|
file_slug = slug
|
|
|
|
else:
|
|
|
|
file_slug = "presentations"
|
|
|
|
response["Content-Disposition"] = 'attachment; filename="%s.csv"' % file_slug
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
response.write(loader.get_template("symposion/schedule/schedule_list.csv").render(Context({
|
2012-10-09 19:07:55 +00:00
|
|
|
"presentations": presentations,
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-10-09 19:07:55 +00:00
|
|
|
})))
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2012-08-31 05:16:30 +00:00
|
|
|
@login_required
|
2012-08-30 06:21:48 +00:00
|
|
|
def schedule_edit(request, slug=None):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-31 05:16:30 +00:00
|
|
|
if not request.user.is_staff:
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
schedule = fetch_schedule(slug)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2014-02-28 15:55:54 +00:00
|
|
|
if request.method == "POST":
|
|
|
|
form = ScheduleSectionForm(
|
|
|
|
request.POST, request.FILES, schedule=schedule
|
|
|
|
)
|
|
|
|
if form.is_valid():
|
|
|
|
if 'submit' in form.data:
|
|
|
|
msg = form.build_schedule()
|
|
|
|
elif 'delete' in form.data:
|
|
|
|
msg = form.delete_schedule()
|
|
|
|
messages.add_message(request, msg[0], msg[1])
|
|
|
|
else:
|
|
|
|
form = ScheduleSectionForm(schedule=schedule)
|
2012-08-30 06:51:07 +00:00
|
|
|
days_qs = Day.objects.filter(schedule=schedule)
|
|
|
|
days = [TimeTable(day) for day in days_qs]
|
2012-08-30 06:21:48 +00:00
|
|
|
ctx = {
|
|
|
|
"schedule": schedule,
|
2012-08-30 06:51:07 +00:00
|
|
|
"days": days,
|
2014-02-28 15:55:54 +00:00
|
|
|
"form": form
|
2012-08-30 06:21:48 +00:00
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/schedule_edit.html", ctx)
|
2012-08-31 04:55:37 +00:00
|
|
|
|
|
|
|
|
2012-08-31 05:16:30 +00:00
|
|
|
@login_required
|
2012-09-20 02:03:30 +00:00
|
|
|
def schedule_slot_edit(request, slug, slot_pk):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-31 05:16:30 +00:00
|
|
|
if not request.user.is_staff:
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-20 02:03:30 +00:00
|
|
|
slot = get_object_or_404(Slot, day__schedule__section__slug=slug, pk=slot_pk)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-14 05:17:32 +00:00
|
|
|
if request.method == "POST":
|
2012-10-26 21:32:03 +00:00
|
|
|
form = SlotEditForm(request.POST, slot=slot)
|
2012-09-14 05:17:32 +00:00
|
|
|
if form.is_valid():
|
2012-10-26 21:32:03 +00:00
|
|
|
save = False
|
|
|
|
if "content_override" in form.cleaned_data:
|
|
|
|
slot.content_override = form.cleaned_data["content_override"]
|
|
|
|
save = True
|
|
|
|
if "presentation" in form.cleaned_data:
|
|
|
|
presentation = form.cleaned_data["presentation"]
|
|
|
|
if presentation is None:
|
|
|
|
slot.unassign()
|
|
|
|
else:
|
|
|
|
slot.assign(presentation)
|
|
|
|
if save:
|
|
|
|
slot.save()
|
2012-12-20 06:54:38 +00:00
|
|
|
return redirect("schedule_edit", slug)
|
2012-09-14 05:17:32 +00:00
|
|
|
else:
|
2012-10-26 21:32:03 +00:00
|
|
|
form = SlotEditForm(slot=slot)
|
2012-09-14 05:17:32 +00:00
|
|
|
ctx = {
|
2012-09-20 02:03:30 +00:00
|
|
|
"slug": slug,
|
2012-09-14 05:17:32 +00:00
|
|
|
"form": form,
|
|
|
|
"slot": slot,
|
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/_slot_edit.html", ctx)
|
2012-09-21 02:38:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
def schedule_presentation_detail(request, pk):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-21 02:38:24 +00:00
|
|
|
presentation = get_object_or_404(Presentation, pk=pk)
|
2016-09-18 01:05:01 +00:00
|
|
|
|
2013-01-03 08:44:49 +00:00
|
|
|
if presentation.slot:
|
2016-09-18 01:05:01 +00:00
|
|
|
# 1) Schedule from presentation's slot
|
2013-01-03 08:44:49 +00:00
|
|
|
schedule = presentation.slot.day.schedule
|
|
|
|
else:
|
2016-09-18 01:05:01 +00:00
|
|
|
# 2) Fall back to the schedule for this proposal
|
|
|
|
schedule = presentation.proposal.kind.section.schedule
|
|
|
|
|
|
|
|
if not request.user.is_staff:
|
|
|
|
# 3) Is proposal unpublished?
|
|
|
|
if presentation.unpublish or not (schedule and schedule.published):
|
|
|
|
raise Http404()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-21 02:38:24 +00:00
|
|
|
ctx = {
|
|
|
|
"presentation": presentation,
|
2013-01-03 08:44:49 +00:00
|
|
|
"schedule": schedule,
|
2012-09-21 02:38:24 +00:00
|
|
|
}
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/presentation_detail.html", ctx)
|
2013-09-29 21:02:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
def schedule_json(request):
|
2014-09-22 02:11:12 +00:00
|
|
|
slots = Slot.objects.filter(
|
|
|
|
day__schedule__published=True,
|
|
|
|
day__schedule__hidden=False
|
|
|
|
).order_by("start")
|
2014-09-21 01:20:04 +00:00
|
|
|
|
|
|
|
protocol = request.META.get('HTTP_X_FORWARDED_PROTO', 'http')
|
2013-09-29 21:02:01 +00:00
|
|
|
data = []
|
|
|
|
for slot in slots:
|
2014-09-21 01:20:04 +00:00
|
|
|
slot_data = {
|
|
|
|
"room": ", ".join(room["name"] for room in slot.rooms.values()),
|
|
|
|
"rooms": [room["name"] for room in slot.rooms.values()],
|
2014-09-22 02:25:18 +00:00
|
|
|
"start": slot.start_datetime.isoformat(),
|
|
|
|
"end": slot.end_datetime.isoformat(),
|
2014-09-21 01:20:04 +00:00
|
|
|
"duration": slot.length_in_minutes,
|
|
|
|
"kind": slot.kind.label,
|
|
|
|
"section": slot.day.schedule.section.slug,
|
2014-09-28 17:52:41 +00:00
|
|
|
"conf_key": slot.pk,
|
|
|
|
# TODO: models should be changed.
|
|
|
|
# these are model features from other conferences that have forked symposion
|
|
|
|
# these have been used almost everywhere and are good candidates for
|
|
|
|
# base proposals
|
|
|
|
"license": "CC BY",
|
|
|
|
"tags": "",
|
|
|
|
"released": True,
|
|
|
|
"contact": [],
|
2014-09-21 01:20:04 +00:00
|
|
|
}
|
|
|
|
if hasattr(slot.content, "proposal"):
|
2016-12-10 03:48:30 +00:00
|
|
|
if slot.content.unpublish and not request.user.is_staff:
|
2016-09-18 01:17:15 +00:00
|
|
|
continue
|
|
|
|
|
2014-09-21 01:20:04 +00:00
|
|
|
slot_data.update({
|
2013-09-29 21:02:01 +00:00
|
|
|
"name": slot.content.title,
|
|
|
|
"authors": [s.name for s in slot.content.speakers()],
|
2014-09-21 01:20:04 +00:00
|
|
|
"contact": [
|
|
|
|
s.email for s in slot.content.speakers()
|
2016-12-30 08:21:36 +00:00
|
|
|
] if request.user.has_perm('symposion_speakers.can_view_contact_details') or request.user.is_staff else ["redacted"],
|
2016-12-10 03:48:30 +00:00
|
|
|
"abstract": slot.content.abstract,
|
2014-09-28 17:52:41 +00:00
|
|
|
"conf_url": "%s://%s%s" % (
|
2014-09-21 01:20:04 +00:00
|
|
|
protocol,
|
2013-09-29 21:02:01 +00:00
|
|
|
Site.objects.get_current().domain,
|
|
|
|
reverse("schedule_presentation_detail", args=[slot.content.pk])
|
|
|
|
),
|
2014-09-22 02:25:18 +00:00
|
|
|
"cancelled": slot.content.cancelled,
|
2014-09-21 01:20:04 +00:00
|
|
|
})
|
2013-09-29 21:02:01 +00:00
|
|
|
else:
|
2014-09-21 01:20:04 +00:00
|
|
|
slot_data.update({
|
2016-12-10 03:48:30 +00:00
|
|
|
"name": slot.content_override if slot.content_override else "Slot",
|
2014-09-21 01:20:04 +00:00
|
|
|
})
|
2013-09-29 21:02:01 +00:00
|
|
|
data.append(slot_data)
|
|
|
|
|
|
|
|
return HttpResponse(
|
2016-12-30 08:21:36 +00:00
|
|
|
json.dumps({"schedule": data}, indent=2),
|
2013-09-29 21:02:01 +00:00
|
|
|
content_type="application/json"
|
|
|
|
)
|
2015-06-15 09:55:54 +00:00
|
|
|
|
2016-12-22 01:00:23 +00:00
|
|
|
class EventFeed(ICalFeed):
|
|
|
|
|
|
|
|
product_id = '-//linux.conf.au/schedule//EN'
|
2016-12-23 09:12:38 +00:00
|
|
|
timezone = settings.TIME_ZONE
|
2016-12-22 01:00:23 +00:00
|
|
|
filename = 'conference.ics'
|
|
|
|
|
|
|
|
def items(self):
|
|
|
|
return Slot.objects.filter(
|
|
|
|
day__schedule__published=True,
|
|
|
|
day__schedule__hidden=False
|
|
|
|
).order_by("start")
|
|
|
|
|
|
|
|
def item_title(self, item):
|
|
|
|
if hasattr(item.content, 'proposal'):
|
|
|
|
return item.content.title
|
|
|
|
else:
|
|
|
|
item.content_override if item.content_override else "Slot"
|
|
|
|
|
|
|
|
def item_description(self, item):
|
|
|
|
if hasattr(item.content, 'proposal'):
|
|
|
|
return item.content.abstract
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def item_start_datetime(self, item):
|
2016-12-23 09:12:38 +00:00
|
|
|
return pytz.timezone(settings.TIME_ZONE).localize(item.start_datetime)
|
2016-12-22 01:00:23 +00:00
|
|
|
|
|
|
|
def item_end_datetime(self, item):
|
2016-12-23 09:12:38 +00:00
|
|
|
return pytz.timezone(settings.TIME_ZONE).localize(item.end_datetime)
|
2016-12-22 01:00:23 +00:00
|
|
|
|
|
|
|
def item_location(self, item):
|
|
|
|
return ", ".join(room["name"] for room in item.rooms.values())
|
|
|
|
|
|
|
|
def item_link(self, item):
|
|
|
|
if hasattr(item.content, 'proposal'):
|
|
|
|
return 'http://%s%s' % (
|
|
|
|
Site.objects.get_current().domain,
|
|
|
|
reverse('schedule_presentation_detail', args=[item.content.pk])
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return 'http://%s' % Site.objects.get_current().domain
|
2015-06-15 09:55:54 +00:00
|
|
|
|
|
|
|
def session_list(request):
|
|
|
|
sessions = Session.objects.all().order_by('pk')
|
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/session_list.html", {
|
2015-06-15 09:55:54 +00:00
|
|
|
"sessions": sessions,
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
def session_staff_email(request):
|
|
|
|
|
|
|
|
if not request.user.is_staff:
|
|
|
|
return redirect("schedule_session_list")
|
|
|
|
|
|
|
|
data = "\n".join(user.email for user in User.objects.filter(sessionrole__isnull=False).distinct())
|
|
|
|
|
|
|
|
return HttpResponse(data, content_type="text/plain;charset=UTF-8")
|
|
|
|
|
|
|
|
|
|
|
|
def session_detail(request, session_id):
|
|
|
|
|
|
|
|
session = get_object_or_404(Session, id=session_id)
|
|
|
|
|
|
|
|
chair = None
|
|
|
|
chair_denied = False
|
|
|
|
chairs = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR).exclude(status=False)
|
|
|
|
if chairs:
|
|
|
|
chair = chairs[0].user
|
|
|
|
else:
|
|
|
|
if request.user.is_authenticated():
|
|
|
|
# did the current user previously try to apply and got rejected?
|
|
|
|
if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_CHAIR, status=False):
|
|
|
|
chair_denied = True
|
|
|
|
|
|
|
|
runner = None
|
|
|
|
runner_denied = False
|
|
|
|
runners = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER).exclude(status=False)
|
|
|
|
if runners:
|
|
|
|
runner = runners[0].user
|
|
|
|
else:
|
|
|
|
if request.user.is_authenticated():
|
|
|
|
# did the current user previously try to apply and got rejected?
|
|
|
|
if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_RUNNER, status=False):
|
|
|
|
runner_denied = True
|
|
|
|
|
|
|
|
if request.method == "POST" and request.user.is_authenticated():
|
|
|
|
if not hasattr(request.user, "profile") or not request.user.profile.is_complete:
|
|
|
|
response = redirect("profile_edit")
|
|
|
|
response["Location"] += "?next=%s" % request.path
|
|
|
|
return response
|
|
|
|
|
|
|
|
role = request.POST.get("role")
|
|
|
|
if role == "chair":
|
|
|
|
if chair is None and not chair_denied:
|
|
|
|
SessionRole(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user).save()
|
|
|
|
elif role == "runner":
|
|
|
|
if runner is None and not runner_denied:
|
|
|
|
SessionRole(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user).save()
|
|
|
|
elif role == "un-chair":
|
|
|
|
if chair == request.user:
|
|
|
|
session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user)
|
|
|
|
if session_role:
|
|
|
|
session_role[0].delete()
|
|
|
|
elif role == "un-runner":
|
|
|
|
if runner == request.user:
|
|
|
|
session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user)
|
|
|
|
if session_role:
|
|
|
|
session_role[0].delete()
|
|
|
|
|
|
|
|
return redirect("schedule_session_detail", session_id)
|
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
return render(request, "symposion/schedule/session_detail.html", {
|
2015-06-15 09:55:54 +00:00
|
|
|
"session": session,
|
|
|
|
"chair": chair,
|
|
|
|
"chair_denied": chair_denied,
|
|
|
|
"runner": runner,
|
|
|
|
"runner_denied": runner_denied,
|
|
|
|
})
|