SpeakerAvailability, EventSession, autoscheduler, and other goodies (#497)
* fix old bug where the get_days() method would return the wrong number of days, this was not discovered because our bootstrap script has been creating 9 day camps instead of 8 day camps (this has been fixed in a different commit) * remove stray debug print * output camp days in local timezone (CEST usually), not UTC * speakeravailability commit of doom, originally intended for #385 but goes a bit further than that. Adds SpeakerAvailability and EventSession models, and models for the new autoscheduler. Update bootstrap script and more. New conference_autoscheduler dependency. Work in progress, but ready for playing around! * add conference-scheduler to requirements * rework migrations, work at bit with postgres range fields and bounds, change how speakeravailability is saved (continuous ranges instead of 1 hour chunks), add tests for utils/range_fields.py including adding hypothesis to requirements/dev.txt, add a test which runs our bootstrap script * catch name collision in the right place, and load missing postgres extension in the migration * add some verbosity to see what the travis issue might be * manually create btree_gist extension in postgres, not sure why the BtreeGistExtension() operation in program/migrations/0085... isn't working in travis? * create extension in the right database maybe * lets try this then * ok so the problem is not that the btree_gist extension isn't getting loaded, the problem is that GIST indexes do not work with uuid fields in postgres 9.6, lets take another stab at getting pg10 with postgis to work with in travis * lets try normal socket connection * add SPEAKER_AVAILABILITY_DAYCHUNK_HOURS=3 to travis environment_settings.py * rework migrations, change so an autoschedule can work with multiple eventtypes, change AutoSlot model to use a DateTimeRangeField so we can use the database for more efficient lookups, add 'conflicts' self m2m for EventLocation to indicate when a room conflicts with another room, add a support_autoscheduling bool to EventType, add workshops to bootstrap script, add timing output to bootstrap script * update README a bit, move some functionality to model methods, update jquery and jquery.datatables, include datatables in base.html instead of in each page, start adding backoffice schedule management views (unfinished), yolo commit so I can show valberg something * Switch to a more simple way of using the autoscheduler, meaning we can remove the whole autoscheduler app and all models. All autoscheduler code is now in program/autoscheduler.py and a bit in backoffice views. Add more backoffice CRUD views for schedule management. Add datatables moment.js plugin to help table sorting of dates. Add Speaker{Proposal}EventConflict model to allow speakers to inform us which events they want to attend so we dont schedule them at the same time. Add EventTag model. New models not hooked up to anything yet. * handle cases where there is no solution without failing, also dont return anything here * wrong block kiddo * switch from EventInstance to EventSlot as the way we schedule events. Finish backoffice content team views (mostly). Many small changes. Prod will need data migration of EventInstances -> EventSlots when the time comes. * keep speakeravailability stuff a bit more DRY by using the AvailabilityMatrixViewMixin everywhere, add event_duration_minutes to EventSession create/update form, reverse the order we delete/create EventSlot objects when updating an EventSession * go through all views, fix various little bugs here and there * add missing migration * add django-taggit, add tags for Events, add tags in bootstrap script, make AutoScheduler use tags. Add tags in forms and templates. * fix taggit entry in requirements * Fix our iCal view: Add uuid field to Event, add uuid property to EventSlot which calculates a consitent UUID for an event at a start time at a location. Use this as the schedule uuid. While here fix so our iCal export is valid, a few fields were missing, the iCal file now validates 100% OK. * fix our FRAB xml export view * comment the EventSlot.uuid property better * typo in comment * language Co-Authored-By: Benjamin Balder Bach <benjamin@overtag.dk> * language Co-Authored-By: Benjamin Balder Bach <benjamin@overtag.dk> * Update src/backoffice/templates/autoschedule_debug_events.html Co-Authored-By: Benjamin Balder Bach <benjamin@overtag.dk> * add a field to make this form look less weird. No difference in functionality. * remove stray print and refactor this form init a bit * fix ScheduleView * only show slots where all speakers are available when scheduling events manually in backoffice * make event list sortable by video recording column * update description on {speaker|event}proposal models reason field * remove badge showing number of scheduled slots for each event in backoffice eventlist. it was unclear what the number meant and it doesn't really fit * remember to consider events in the same location when deciding whether a slot is available or not * add is_available() method to EventLocation, add clean_location() method to EventSlot, call it from EventSlot.clean(), update a bit of text in eventslotunschedule template * fix EventSession.get_available_slots() so it doesnt return busy slots as available, and since this means we can no longer schedule stuff in the lunchbreak lower the number of talks in the bootstrap script a bit so we have a better chance of having a solvable problem * fix the excludefilter in EventSession.get_available_slots() for real this time, also fix an icon and add link in event schedule template in backoffice * show message when no slots are available for manual scheduling in backoffice * add event_conflicts to SpeakerUpdateView form in backoffice * fix link to speaker object in speakerproposal list in backoffice * allow blank tags * make duration validation depend on the eventtype event_duration_minutes if we have one. fix help_text and label and placeholder for all duration fields * allow music acts up to 180 mins in the bootstrap data * fix wrong eventtype name for recreational events in speakerproposalform * stretch the colspan one cell more * save event_conflicts m2m when submitting speaker and event together * form not self, and add succes message * move js function toggleclass() to bornhack.js and rename to toggle_sa_form_class(), function is used in several templates and was missing when submitting combined proposals * move the no-js removal to the top of ready() function This will allow other javascript initialization (eg. DataTable) to see the elements and initialize accordingly (eg. column width for tables) * Fixed problem with event feedback detail view * Fixed problem with event feedback list view * introduce a get_tzrange_days() function and use that to get the relevant days for the matrix instead of camp.get_days(), thereby fixing some display issues when eventsessions cross dates * show submitting user and link to proposal on backoffice event detail page, change User to Submitter in backoffice speaker list table * show warning by the buttons when a proposal cannot be approved, and show better text on approve/reject buttons * disable js schedule, save m2m, prefetch some stuff * fix broken date header in table * remove use of djangos regular slugify function, use the new utils.slugs.unique_slugify() instead Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org> Co-authored-by: Benjamin Balder Bach <benjamin@overtag.dk> Co-authored-by: Thomas Flummer <tf@flummer.net>
This commit is contained in:
parent
d00b0d8154
commit
eff4bfaf1c
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
.dev
|
||||
.idea/
|
||||
.hypothesis/
|
||||
__pycache__/
|
||||
db.sqlite3
|
||||
*.sw*
|
||||
|
@ -7,3 +8,4 @@ db.sqlite3
|
|||
venv/
|
||||
environment_settings.py
|
||||
elm-stuff/
|
||||
.coverage
|
||||
|
|
|
@ -10,10 +10,12 @@ services:
|
|||
- postgresql
|
||||
|
||||
addons:
|
||||
postgresql: "9.6"
|
||||
postgresql: "10"
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-9.6-postgis-2.5
|
||||
- postgresql-10
|
||||
- postgresql-client-10
|
||||
- postgresql-10-postgis-2.5
|
||||
|
||||
install:
|
||||
- pip install -r src/requirements/dev.txt
|
||||
|
|
|
@ -17,9 +17,9 @@ If you already cloned the repository without --recursive, you can change into th
|
|||
git submodule update --init --recursive
|
||||
|
||||
### Virtualenv
|
||||
Create a Python 3 virtual environment and activate it:
|
||||
Create a Python 3.7 virtual environment and activate it:
|
||||
```
|
||||
$ virtualenv venv -p python3
|
||||
$ virtualenv venv -p python3.7
|
||||
$ source venv/bin/activate
|
||||
```
|
||||
|
||||
|
@ -57,7 +57,7 @@ Install pip packages:
|
|||
|
||||
### Postgres
|
||||
|
||||
You need to have a running Postgres instance (we use Postgres-specific fields and PostGIS/GeoDjango). Install Postgres and PostGIS, and add a database `bornhack` (or whichever you like) with some way for the application to connect to it, for instance adding a user with a password. Connect to the database as a superuser and run `create extension postgis`.
|
||||
You need to have a running Postgres instance (sqlite or mysql or others can't be used, because we use Postgres-specific fields and PostGIS/GeoDjango). Install Postgres and PostGIS, and add a database `bornhack` (or whichever you like) with some way for the application to connect to it, for instance adding a user with a password. Connect to the database as a superuser and run `create extension postgis`. The postgres version in production is 12 and the postgis version in production is 2.5. The minimum postgres version is 10, because we use GIST indexes on uuid fields (for ExclusionConstraints).
|
||||
|
||||
### Configuration file
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
85
src/backoffice/forms.py
Normal file
85
src/backoffice/forms.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
from django import forms
|
||||
from program.models import Event, Speaker
|
||||
|
||||
|
||||
class AutoScheduleValidateForm(forms.Form):
|
||||
schedule = forms.ChoiceField(
|
||||
choices=(
|
||||
(
|
||||
"current",
|
||||
"Validate Current Schedule (Load the AutoScheduler with the currently scheduled Events and validate)",
|
||||
),
|
||||
(
|
||||
"similar",
|
||||
"Validate Similar Schedule (Create and validate a new schedule based on the current schedule)",
|
||||
),
|
||||
("new", "Validate New Schedule (Create and validate a new schedule)"),
|
||||
),
|
||||
help_text="What to validate?",
|
||||
)
|
||||
|
||||
|
||||
class AutoScheduleApplyForm(forms.Form):
|
||||
schedule = forms.ChoiceField(
|
||||
choices=(
|
||||
(
|
||||
"similar",
|
||||
"Apply Similar Schedule (Create and apply a new schedule similar to the current schedule)",
|
||||
),
|
||||
(
|
||||
"new",
|
||||
"Apply New Schedule (Create and apply a new schedule without considering the current schedule)",
|
||||
),
|
||||
),
|
||||
help_text="Which schedule to apply?",
|
||||
)
|
||||
|
||||
|
||||
class EventScheduleForm(forms.Form):
|
||||
""" The EventSlots are added in the view and help_text is not visible, just define the field """
|
||||
|
||||
slot = forms.ChoiceField()
|
||||
|
||||
|
||||
class SpeakerForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Speaker
|
||||
fields = [
|
||||
"name",
|
||||
"email",
|
||||
"biography",
|
||||
"needs_oneday_ticket",
|
||||
"event_conflicts",
|
||||
]
|
||||
|
||||
def __init__(self, camp, matrix={}, *args, **kwargs):
|
||||
"""
|
||||
initialise the form and add availability fields to form
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# do we have a matrix to work with?
|
||||
if not matrix:
|
||||
return
|
||||
# add speaker availability fields
|
||||
for date in matrix.keys():
|
||||
# do we need a column for this day?
|
||||
if not matrix[date]:
|
||||
# nothing on this day, skip it
|
||||
continue
|
||||
# loop over the daychunks for this day
|
||||
for daychunk in matrix[date]:
|
||||
if not matrix[date][daychunk]:
|
||||
# no checkbox needed for this daychunk
|
||||
continue
|
||||
# add the field
|
||||
self.fields[matrix[date][daychunk]["fieldname"]] = forms.BooleanField(
|
||||
required=False
|
||||
)
|
||||
# add it to Meta.fields too
|
||||
self.Meta.fields.append(matrix[date][daychunk]["fieldname"])
|
||||
|
||||
# only show events from this camp
|
||||
self.fields["event_conflicts"].queryset = Event.objects.filter(
|
||||
track__camp=camp, event_type__support_speaker_event_conflicts=True
|
||||
)
|
|
@ -11,12 +11,12 @@
|
|||
<div class="lead">
|
||||
The Content team can approve or reject pending EventFeedback from this page. The feedback will not be visible to the Event owner before it is approved. The Event owner will not be able to see the username of the feedback authors. The feedback author can see when the feedback has been approved or rejected by returning to the feedback page.
|
||||
</div>
|
||||
{% if eventfeedback_list %}
|
||||
{% if event_feedback_list %}
|
||||
<form method="post">
|
||||
{{ formset.management_form }}
|
||||
{% csrf_token %}
|
||||
{% for form, feedback in formset|zip:eventfeedback_list %}
|
||||
{% include "includes/eventfeedback_detail_panel.html" with eventfeedback=feedback event=feedback.event %}
|
||||
{% for form, feedback in formset|zip:event_feedback_list %}
|
||||
{% include "includes/event_feedback_detail_panel.html" with event_feedback=feedback event=feedback.event %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Submit</button>
|
||||
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Approve Public Credit Names</h2>
|
||||
|
@ -15,7 +11,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
|
|
23
src/backoffice/templates/autoschedule_apply.html
Normal file
23
src/backoffice/templates/autoschedule_apply.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Really Apply AutoSchedule?</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>Applying the AutoSchedule will schedule Events in EventSlots to match the result of the AutoScheduler calculation. Any existing autoscheduled Events will be unscheduled.</P>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-check"></i> Apply Schedule
|
||||
</button>
|
||||
<a href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}" class="btn btn-default">
|
||||
<i class="fas fa-undo"></i> Cancel
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
31
src/backoffice/templates/autoschedule_crash_course.html
Normal file
31
src/backoffice/templates/autoschedule_crash_course.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">AutoScheduler Crash Course</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>The AutoScheduler needs a list of <i>AutoScheduler Slots</i> and a list of <i>AutoScheduler Events</i> to do its thing.</p>
|
||||
<p>We create one <i>AutoScheduler Slot</i> for each <i>EventSlot</i> in all <i>EventSessions</i> for the AutoScheduler-enabled <i>EventTypes</i>.</p>
|
||||
<p>We create one <i>AutoScheduler Event</i> for each <i>EventType</i> which support autoscheduling, excluding any Events which have been scheduled manually.</p>
|
||||
<p>All scheduling constraints are attached to <i>AutoScheduler Events</i>, and they come from many sources:
|
||||
<ul>
|
||||
<li><b>Slot Conflicts</b>: If a Slot is unavailable because something else has been scheduled in it the Slot is removed and not considered by the AutoScheduler. The same applies if something has been scheduled in a slot in a conflicting EventLocation.</li>
|
||||
<li><b>SpeakerAvailability</b>: For each Event we mark any Slots as unavailable if one or more speakers for that event are unavailable for any part of the Slot.</li>
|
||||
<li><b>Speaker Conflicts</b>: For each Event we check for each speaker if they are also hosting other Events, and register conflicts as needed, so noone has to be in two places at the same time.</li>
|
||||
<li><b>Speaker<>Event Conflicts</b>: If a speaker has expressed desire to attend other Events we register conflicts as needed, so we don't schedule the speakers events at the same time as something the speaker wants to attend.</li>
|
||||
<li><b>Event Tags</b>: Events which share a tag are scheduled in the same session if possible, and may not overlap. In other words, all events tagged "Django" will be scheduled together if they can, and if that is not possible they will at least be scheduled without overlaps so it is possible to attend all of them.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>We use the term <i>published schedule</i> when referring the currently published Events (meaning the current schedule on the live site). We use the term <i>draft schedule</i> when referring to the "potential" schedule calculated based on the current data (EventSessions and EventSlots, Events, Speakers, SpeakerAvailability, Event conflicts, Speaker<>Event Conflicts, Event tags) in the database.</p>
|
||||
|
||||
<p>The <i>draft schedule</i> can optionally be created similar to the <i>published schedule</i>. This makes the solver keep the number of schedule changes as low as possible.</p>
|
||||
|
||||
<p>It is fully supported to mix manual scheduling with using the AutoScheduler. The AutoScheduler can be used as needed: It can act strictly as a validator for a manually planned schedule. It could also be used for scheduling some events, while manually scheduling others. It can also be used to do the full schedule. The AutoScheduler will avoid scheduling over manually scheduled events, and will consider manually scheduled events when looking to avoid conflicts.</p>
|
||||
|
||||
<a href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> AutoSchedule Management</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
29
src/backoffice/templates/autoschedule_debug_events.html
Normal file
29
src/backoffice/templates/autoschedule_debug_events.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block body %}
|
||||
<p class="lead">The following conflicts have been found between events in the AutoScheduler, events with a red table cell will not be scheduled together.</p>
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back</a></p>
|
||||
<table class="table table-condensed table-bordered">
|
||||
<thead>
|
||||
<th><br></th>
|
||||
{% for autoevent,event in scheduler.autoevents|zip:scheduler.events.all %}
|
||||
<th><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a></th>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for autoevent,event in scheduler.autoevents|zip:scheduler.events.all %}
|
||||
<tr>
|
||||
<th><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a></th>
|
||||
{% for rowautoevent,rowevent in scheduler.autoevents|zip:scheduler.events.all %}
|
||||
{% if rowautoevent in autoevent.unavailability or rowautoevent == autoevent or autoevent in rowautoevent.unavailability %}
|
||||
<td class="text-center danger"><i class="fas fa-times"></i></td>
|
||||
{% else %}
|
||||
<td class="text-center success"><i class="fas fa-check"></i></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back</a></p>
|
||||
{% endblock body %}
|
30
src/backoffice/templates/autoschedule_debug_slots.html
Normal file
30
src/backoffice/templates/autoschedule_debug_slots.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block body %}
|
||||
<p class="lead">The following availability matrix has been generated for the Event/Slot combinations in the AutoScheduler</p>
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back</a></p>
|
||||
<table class="table table-condensed table-bordered">
|
||||
<thead>
|
||||
<th>fedt nok</th>
|
||||
{% for slot in scheduler.autoslots %}
|
||||
<th>{{ slot.starts_at }} (venue {{ slot.venue }}, cap {{ slot.capacity }})</th>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for autoevent, event in scheduler.autoevents|zip:scheduler.events.all %}
|
||||
<tr>
|
||||
<th><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a> (demand {{ autoevent.demand }})</th>
|
||||
{% for slot in scheduler.autoslots %}
|
||||
{% if slot in autoevent.unavailability %}
|
||||
<td class="text-center danger"><i class="fas fa-times" data-toggle='tooltip' data-html=true data-placement='right' title='Event "{{ event.title }}" will not be scheduled in the slot {{ slot.starts_at }}'></i></td>
|
||||
{% else %}
|
||||
<td class="text-center success"><i class="fas fa-check" data-toggle='tooltip' data-html=true data-placement='right' title='Event "{{ event.title }}" might be scheduled in the slot {{ slot.starts_at }}'></i></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back</a></p>
|
||||
{% endblock body %}
|
15
src/backoffice/templates/autoschedule_diff.html
Normal file
15
src/backoffice/templates/autoschedule_diff.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Show schedule diff</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Showing the diff between the current schedule (calculated from currently published Events), and the new schedule based on current database object.</p>
|
||||
{% include 'includes/autoschedule_diff_table.html' %}
|
||||
<a href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
58
src/backoffice/templates/autoschedule_index.html
Normal file
58
src/backoffice/templates/autoschedule_index.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Manage AutoScheduler</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Select your desired action below</p>
|
||||
<div class="list-group">
|
||||
<a href="{% url 'backoffice:autoschedule_validate' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-fire-extinguisher fa-fw"></i> Validate Schedule
|
||||
</h4>
|
||||
<p class="list-group-item-text">
|
||||
Validate published or draft schedule. Useful to check if the published schedule is still valid after making changes to the underlying data. Can also check the draft schedule.
|
||||
</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:autoschedule_diff' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-random fa-fw"></i> Show Schedule Diff
|
||||
</h4>
|
||||
<p class="list-group-item-text">
|
||||
Show the differences between the published schedule and the similar draft schedule. Use this to predict what schedule changes would happen if the draft schedule was applied now. <i>This view takes a while to load.</i>
|
||||
</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:autoschedule_apply' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-check fa-fw"></i> Apply Schedule
|
||||
</h4>
|
||||
<p class="list-group-item-text">
|
||||
Apply the draft schedule by unscheduling any currently autoscheduled Events and scheduling new Events in EventSlots to match the Slot/Event combinations in the draft schedule. It is prudent to check the validity and diff for the draft schedule before applying!
|
||||
</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:autoschedule_debug_event_slot_unavailability' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-chess-board fa-fw"></i> Debug Event/Slot Unavailability
|
||||
</h4>
|
||||
<p class="list-group-item-text">
|
||||
This debug view shows a matrix/table with all AutoScheduler Events on the Y axis and all AutoScheduler Slots on the X axis, and red/green table cells to indicate whether an event can be scheduled in that Slot. <i>This view takes a while to load.</i>
|
||||
</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:autoschedule_debug_event_conflicts' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-chess-board fa-fw"></i> Debug Event Conflicts
|
||||
</h4>
|
||||
<p class="list-group-item-text">
|
||||
This view shows a matrix/table with all Events on both axis, and red/green table cells to indicate conflicts between Events. Event conflicts can happen either because a Speaker on an Event is also a Speaker on other Events, or because of a SpeakerEventConflict, or because the events share one or more tags. <i>This view takes a while to load.</i>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<p><i>If you are new to the AutoScheduler you might wish to consult the <a href="{% url 'backoffice:autoschedule_crash_course' camp_slug=camp.slug %}">crash course</a> for a five minute introduction to the terminology and concepts used.</i></p>
|
||||
<p><a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
26
src/backoffice/templates/autoschedule_validate.html
Normal file
26
src/backoffice/templates/autoschedule_validate.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Validate Schedule</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">What do you want to validate?</p>
|
||||
<p>Validating the <i>current</i> schedule means looking at the EventSessions, EventLocations, EventTypes, Events and their conflicts, Speakers, SpeakerAvailability, and Speaker<>EventConflicts, and comparing all that with the currently published schedule. Maybe some speakers changed availability or added some event conflicts? Use this tool to check if we still have a valid schedule!</p>
|
||||
<p>Validating the <i>similar</i> schedule means looking at the same things as above, then calculating a new schedule (as similar to the current one as possible), and checking if the new schedule is (would be) valid. Use this to find out if it is feasible to create a new similar schedule from the current data in the database.</p>
|
||||
<p>Validating the <i>new</i> schedule means looking at the same things as above, then calculating a new schedule, without considering the existing schedule. The new schedule will likely be very different from the existing one.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-check"></i> Validate Schedule
|
||||
</button>
|
||||
<a href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}" class="btn btn-default">
|
||||
<i class="fas fa-undo"></i> Cancel
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Hand Out Badges</h2>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket UUID</th>
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block title %}
|
||||
Select Chain | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
@ -16,7 +11,7 @@ Select Chain | {{ block.super }}
|
|||
<p><a href="{% url "backoffice:index" camp_slug=camp.slug %}">Back to Backoffice Index</a></p>
|
||||
|
||||
{% if chain_list %}
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Chain Name</th>
|
||||
|
|
20
src/backoffice/templates/event_delete.html
Normal file
20
src/backoffice/templates/event_delete.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">Really Delete Event?</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Really delete event <i>{{ object.title }}</i> ?</p>
|
||||
<p class="lead">You can recreate the Event (but not the Events scheduling!) later by re-approving the EventProposal this Event came from.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger"><i class='fas fa-times'></i> Yes, Delete it</button>
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=object.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
77
src/backoffice/templates/event_detail_backoffice.html
Normal file
77
src/backoffice/templates/event_detail_backoffice.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load bornhack %}
|
||||
{% load commonmark %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Event: {{ event.title }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ event.abstract|untrustedcommonmark }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Details for <i>{{ event.title }}</i></h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>EventType</th>
|
||||
<td>{{ event.event_type.icon_html }} <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=event.event_type.slug %}">{{ event.event_type }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event Track</th>
|
||||
<td>{{ event.track.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tags</th>
|
||||
<td>
|
||||
{% for tag in event.tags.all %}
|
||||
<span class="badge">{{ tag }}</span>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Video Recording</th>
|
||||
<td>{{ event.video_recording|truefalseicon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Demand</th>
|
||||
<td>{% if event.demand %}{{ event.demand }} participants expected{% else %}0 (The autoscheduler will not consider capacity/demand when scheduling this event){% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Proposal</th>
|
||||
<td><a href="{% url 'backoffice:event_proposal_detail' camp_slug=camp.slug pk=event.proposal.pk %}" class="btn btn-default btn-xs"><i class="fas fa-search"></i> Show</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Submitted by</th>
|
||||
<td><i class="fas fa-user"></i> {{ event.proposal.user.username }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Speakers for <i>{{ event.title }}</i></h4>
|
||||
{% include "includes/speaker_list_table_backoffice.html" with speaker_list=event.speakers.all noactions=True nodatatable=True %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Schedule for <i>{{ event.title }}</i></h4>
|
||||
{% if event.event_slots.exists %}
|
||||
{% include "includes/event_slot_list_backoffice.html" with event_slot_list=event.event_slots.all notitle=True %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
<a href="{% url 'backoffice:event_schedule' camp_slug=camp.slug slug=event.slug %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i> Schedule Event</a>
|
||||
<br>
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="{% url 'backoffice:event_update' camp_slug=camp.slug slug=event.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update Event</a>
|
||||
<a href="{% url 'backoffice:event_delete' camp_slug=camp.slug slug=event.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete Event</a>
|
||||
<a href="{% url 'backoffice:event_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Event List</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
28
src/backoffice/templates/event_list_backoffice.html
Normal file
28
src/backoffice/templates/event_list_backoffice.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block title %}
|
||||
Event List | Backoffice | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title">Event List - BackOffice</h3></div>
|
||||
<div class="panel-body">
|
||||
<p><i>Events</i> are the result of approving an <i>EventProposal</i>. They contain data for the event like title, abstract, duration and more. Search for an event title, speaker names, tags or event type to filter the list. <b>Any changes made here will be reflected immediately on the live site!</b></p>
|
||||
{% if not event_list %}
|
||||
<p class="lead">No Events found.</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
{% include "includes/event_list_table_backoffice.html" %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
18
src/backoffice/templates/event_location_delete.html
Normal file
18
src/backoffice/templates/event_location_delete.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">Delete EventLocation {{ event_location.name }}?</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">This EventLocation has <b>{{ event_location.event_sessions.count }}</b> EventSessions with <b>{{ event_location.event_slots.count }}</b> EventSlots which have a total of <b>{{ event_location.scheduled_event_slots.count }}</b> Events scheduled. Deleting the EventLocation will also delete the EventSessions and EventSlots, removing the Events from the schedule!</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Yes, Delete it" button_type="submit" button_class="btn-danger" name="Delete" %}
|
||||
<a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=event_location.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
89
src/backoffice/templates/event_location_detail.html
Normal file
89
src/backoffice/templates/event_location_detail.html
Normal file
|
@ -0,0 +1,89 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">EventLocation Details</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td><i class="fas fa-{{ event_location.icon }}"></i> {{ event_location.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Icon</th>
|
||||
<td>fas fa-{{ event_location.icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Capacity</th>
|
||||
<td><i class="fas fa-users"></i> {{ event_location.capacity }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Conflicts</th>
|
||||
<td>
|
||||
{% if event_location.conflicts.all %}
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Capacity</th>
|
||||
<th>Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for conflict in event_location.conflicts.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=conflict.slug %}">{{ conflict.name }}</a></td>
|
||||
<td><span class="badge">{{ conflict.capacity }}</span></td>
|
||||
<td><span class="badge">{{ conflict.event_sessions.count }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Sessions</th>
|
||||
<td>
|
||||
{% if event_location.event_sessions.all %}
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session</th>
|
||||
<th>EventType</th>
|
||||
<th>When</th>
|
||||
<th>Description</th>
|
||||
<th class="text-center">Slots</th>
|
||||
<th class="text-center">Events</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in event_location.event_sessions.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=session.pk %}">{{ session.pk }}</a></td>
|
||||
<td><a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=session.event_type.slug %}">{{ session.event_type.icon_html }} {{ session.event_type.name }}</a></td>
|
||||
<td>{{ session.when.lower }} to<br> {{ session.when.upper }}</td>
|
||||
<td>{{ session.description|default:"N/A" }}</td>
|
||||
<td class="text-center"><span class="badge">{{ session.event_slots.count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ session.scheduled_event_slots.count }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a href="{% url 'backoffice:event_location_update' camp_slug=camp.slug slug=event_location.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update Location</a>
|
||||
<a href="{% url 'backoffice:event_location_delete' camp_slug=camp.slug slug=event_location.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete Location</a>
|
||||
<a href="{% url 'backoffice:event_location_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> EventLocation List</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
19
src/backoffice/templates/event_location_form.html
Normal file
19
src/backoffice/templates/event_location_form.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if form.instance.pk %}Update{% else %}Create new{% endif %} EventLocation</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">{% if form.instance.pk %}Update{% else %}Create{% endif %} EventLocation</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Save</button>
|
||||
<a href="{% url 'backoffice:event_session_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
69
src/backoffice/templates/event_location_list.html
Normal file
69
src/backoffice/templates/event_location_list.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="h3">BackOffice - EventLocations</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p><i>EventLocations</i> - the places where stuff happens!</p>
|
||||
{% if not event_location_list %}
|
||||
<p class="lead">
|
||||
No EventLocations found. Create some!
|
||||
<a href="{% url 'backoffice:event_location_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create EventLocation</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url 'backoffice:event_location_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create EventLocation</a>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-center">Capacity</th>
|
||||
<th>Location Conflicts</th>
|
||||
<th class="text-center">Sessions</th>
|
||||
<th class="text-center">Slots</th>
|
||||
<th class="text-center">Events</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for location in event_location_list %}
|
||||
<tr>
|
||||
<td>{{ location.icon_html }} <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=location.slug %}">{{ location.name }}</a></td>
|
||||
<td class="text-center"><span class="badge">{{ location.capacity }}</span></td>
|
||||
<td>
|
||||
{% if location.conflicts.exists %}
|
||||
<ul class="list-group">
|
||||
{% for conflict in location.conflicts.all %}
|
||||
<a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=conflict.slug %}"class="list-group-item"><i class="fas fa-{{ conflict.icon }}"></i> {{ conflict.name }}</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</ul>
|
||||
</td>
|
||||
<td class="text-center"><span class="badge">{{ location.event_sessions.count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ location.event_slots.count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ location.scheduled_event_slots.count }}</span></td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=location.slug %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
<a href="{% url 'backoffice:event_location_update' camp_slug=camp.slug slug=location.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:event_location_delete' camp_slug=camp.slug slug=location.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url 'backoffice:event_location_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create EventLocation</a>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
28
src/backoffice/templates/event_proposal_approve_reject.html
Normal file
28
src/backoffice/templates/event_proposal_approve_reject.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
{% if not form.instance.can_be_approved %}
|
||||
<br>
|
||||
<p><span class="alert alert-warning">NOTE: Not all SpeakerProposals associated with this EventProposal have been approved. EventProposal can not be approved! It can still be rejected though.</span></p>
|
||||
<br>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Approve or Reject {{ form.instance.event_type.name }} Proposal</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>Submitter {{ event_proposal.user }} will receive an email when the EventProposal is approved or rejected. It is possible to include an extra message in the form below explaining why the proposal was accepted or rejected. If the field is left blank a standard email is sent.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% if form.instance.can_be_approved %}
|
||||
<button type="submit" class="btn btn-success" name="approve"><i class='fas fa-check'></i> Approve Proposal</button>
|
||||
{% endif %}
|
||||
<button type="reject" class="btn btn-danger" name="reject"><i class='fas fa-times'></i> Reject Proposal</button>
|
||||
<a href="{% url 'backoffice:pending_proposals' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<p><a href="{% url 'backoffice:event_proposal_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to EventProposal List</a></p>
|
||||
{% if not event_proposal.can_be_approved %}
|
||||
<br>
|
||||
<p><span class="alert alert-warning">NOTE: Not all SpeakerProposals associated with this EventProposal have been approved. EventProposal can not be approved! It can still be rejected though.</span></p>
|
||||
<br>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fas fa-{{ event_proposal.event_type.icon }} fa-lg" style="color: {{ event_proposal.event_type.color }}"></i> <span class="h3">{{ event_proposal.event_type.name }} Proposal: <i>{{ event_proposal.title }}</i></span></div>
|
||||
<div class="panel-body">
|
||||
{{ event_proposal.abstract|untrustedcommonmark }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Details for <i>{{ event_proposal.title }}</i></h4>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<td>{{ event_proposal.uuid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>{{ event_proposal.proposal_status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventType</th>
|
||||
<td>{{ event_proposal.event_type.icon_html }} <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=event_proposal.event_type.slug %}">{{ event_proposal.event_type }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Duration</th>
|
||||
<td>{{ event_proposal.duration }} minutes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tags</th>
|
||||
<td>
|
||||
{% for tag in event_proposal.tags.all %}
|
||||
<span class="badge">{{ tag }}</span>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Use provided laptop?</th>
|
||||
<td>{{ event_proposal.use_provided_speaker_laptop|truefalseicon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Submission Notes</th>
|
||||
<td>{{ event_proposal.submission_notes|untrustedcommonmark|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>URLs for <i>{{ event_proposal.title }}</i></h4>
|
||||
{% if event_proposal.urls.exists %}
|
||||
{% include 'includes/event_proposal_url_table.html' %}
|
||||
{% else %}
|
||||
<i>Nothing found.</i>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>{{ event_proposal.event_type.host_title }} Proposals for <i>{{ event_proposal.title }}</i></h4>
|
||||
{% if event_proposal.speakers.exists %}
|
||||
{% include 'includes/speaker_proposal_list_table_backoffice.html' with speaker_proposal_list=event_proposal.speakers.all nodatatable=True %}
|
||||
{% else %}
|
||||
<i>Nothing found.</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-footer">Status: <span class="badge">{{ event_proposal.proposal_status }}</span> | ID: <span class="badge">{{ event_proposal.uuid }}</span></div>
|
||||
</div>
|
||||
|
||||
{% if not event_proposal.can_be_approved %}
|
||||
<p><span class="alert alert-warning">NOTE: Not all SpeakerProposals associated with this EventProposal have been approved. EventProposal can not be approved! It can still be rejected though.</span></p>
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{% if event_proposal.proposal_status == "pending" %}
|
||||
{% if form.instance.can_be_approved %}
|
||||
<a href="{% url 'backoffice:event_proposal_approve_reject' camp_slug=camp.slug pk=event_proposal.uuid %}" class="btn btn-success"><i class="fas fa-check"></i> Approve EventProposal</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'backoffice:event_proposal_approve_reject' camp_slug=camp.slug pk=event_proposal.uuid %}" class="btn btn-danger"><i class="fas fa-times"></i> Reject EventProposal</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'backoffice:event_proposal_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to EventProposal List</a>
|
||||
</p>
|
||||
|
||||
{% endblock content %}
|
22
src/backoffice/templates/event_proposal_list.html
Normal file
22
src/backoffice/templates/event_proposal_list.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
EventProposal List | Backoffice | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if event_proposal_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title">EventProposal List - BackOffice</h3></div>
|
||||
<div class="panel-body">
|
||||
<p>This is a list of all EventProposal objects in the system. Search for title, type, username, speakers or status to filter the table.</p>
|
||||
<p><a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a></p>
|
||||
{% if event_proposal_list %}
|
||||
{% include 'includes/event_proposal_list_table_backoffice.html' %}
|
||||
{% else %}
|
||||
No EventProposals found.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
48
src/backoffice/templates/event_schedule.html
Normal file
48
src/backoffice/templates/event_schedule.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Select EventSlot for <i>{{ event.title }}</i></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>The following EventSlots are available and suitable for scheduling <i>{{ event.title }}</i></p>
|
||||
<p>This event has a demand of {{ event.demand }} people.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% if event_slots %}
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pick</th>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
<th>Location</th>
|
||||
<th class="text-center">Capacity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in event_slots %}
|
||||
<tr>
|
||||
<td><input id="id_slot_{{ slot.index }}" name="slot" value="{{ slot.index }}" type="radio" required></td>
|
||||
<td>{{ slot.slot.when.lower }}</td>
|
||||
<td>{{ slot.slot.when.upper }}</td>
|
||||
<td><i class="fas fa-{{ slot.slot.event_session.event_location.icon }}"></i> <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=slot.slot.event_location.slug %}">{{ slot.slot.event_session.event_location.name }}</a></td>
|
||||
<td class="text-center{% if event.demand > slot.slot.event_session.event_location.capacity %} danger{% endif %}"><span class="badge">{{ slot.slot.event_session.event_location.capacity }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Schedule Event</button>
|
||||
{% else %}
|
||||
<p class="lead">No available slots found</p>
|
||||
{% endif %}
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Create New EventSession - Select Event Location</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Select Event Location for the new EventSession</p>
|
||||
<div class="list-group">
|
||||
{% for location in event_location_list %}
|
||||
<a href="{%url 'backoffice:event_session_create' camp_slug=camp.slug event_type_slug=event_type.slug event_location_slug=location.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-{{ location.icon }} fa-2x fa-pull-left fa-fw"></i>
|
||||
{{ location.name }}<span class="pull-right"><i class="fas fa-plus fa-2x fa-pull-right"></i></span>
|
||||
</h4>
|
||||
<p class="list-group-item-text">Create EventSession for {{ location.name }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Create New EventSession - Select Event Type</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Select Event Type for the new EventSession</p>
|
||||
<div class="list-group">
|
||||
{% for event_type in event_type_list %}
|
||||
<a href="{%url 'backoffice:event_session_create_location_select' camp_slug=camp.slug event_type_slug=event_type.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="fas fa-{{ event_type.icon }} fa-2x fa-pull-left fa-fw" style="color: {{ event_type.color }};"></i>
|
||||
{{ event_type.name }}<span class="pull-right"><i class="fas fa-plus fa-2x fa-pull-right" style="color: {{ event_type.color }};"></i></span>
|
||||
</h4>
|
||||
{% if event_type.description %}<p class="list-group-item-text">{{ event_type.description }}</p>{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
19
src/backoffice/templates/event_session_delete.html
Normal file
19
src/backoffice/templates/event_session_delete.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">Delete EventSession?</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">This EventSession runs from <b>{{ session.when.lower }}</b> to <b>{{ session.when.upper }}</b>. We currently have <b>{{ session.scheduled_event_slots.count }}</b> Events scheduled in this session, leaving <b>{{ session.free_time }}</b> time unused. Deleting an EventSession means deleting all the EventSlots, which means anything scheduled will no longer be scheduled.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Yes, Delete it" button_type="submit" button_class="btn-danger" name="Delete" %}
|
||||
<a href="{% url 'backoffice:event_session_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
82
src/backoffice/templates/event_session_detail.html
Normal file
82
src/backoffice/templates/event_session_detail.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">EventSession Details</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<td>{{ session.pk }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event Type</th>
|
||||
<td><i class="fas fa-{{ session.event_type.icon }}" style="color: {{ session.event_type.color }};"></i> <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=session.event_type.slug %}">{{ session.event_type }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event Location</th>
|
||||
<td><i class="fas fa-{{ session.event_location.icon }}"></i> <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=session.event_location.slug %}">{{ session.event_location.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Session Start</th>
|
||||
<td>{{ session.when.lower }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Session End</th>
|
||||
<td>{{ session.when.upper }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Session Duration</th>
|
||||
<td>{{ session.duration }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSlots and Events</th>
|
||||
<td>
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
<th>Event</th>
|
||||
<th>Speakers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in session.event_slots.all %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.pk %}">{{ slot.when.lower }}</a></td>
|
||||
<td>{{ slot.when.upper }}</td>
|
||||
<td>
|
||||
{% if slot.event %}
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=slot.event.slug %}">{{ slot.event.event_type.icon_html }} {{ slot.event.title }}</a>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if slot.event.speakers.exists %}
|
||||
<ul class="list-group">
|
||||
{% for speaker in slot.event.speakers.all %}
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item"><i class="fas fa-user"></i> {{ speaker.name }}</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a href="{% url 'backoffice:event_session_update' camp_slug=camp.slug pk=session.id %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update EventSession</a>
|
||||
<a href="{% url 'backoffice:event_session_delete' camp_slug=camp.slug pk=session.id %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete EventSession</a>
|
||||
<a href="{% url 'backoffice:event_session_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> List EventSessions</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
63
src/backoffice/templates/event_session_form.html
Normal file
63
src/backoffice/templates/event_session_form.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load tz %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% if form.instance.pk %}Update{% else %}Create new{% endif %} EventSession</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">{% if form.instance.pk %}Update{% else %}Create new{% endif %} EventSession for Event Type <b>{{ event_type }}</b> at location <b>{{ event_location.name }}</b>.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% get_current_timezone as TIME_ZONE %}
|
||||
<div class="alert alert-info">Note: Input will be interpreted as being in time zone {{ TIME_ZONE }}.</div>
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Save</button>
|
||||
<a href="{% url 'backoffice:event_session_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% if sessions %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">We have {{ sessions.count }} existing EventSessions for EventType <b>{{ event_type }}</b> at location <b>{{ event_location.name }}</b>:</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>Session Start</th>
|
||||
<th>Session End</th>
|
||||
<th>Session Length</th>
|
||||
<th>Events</th>
|
||||
<th>Free Time</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in sessions %}
|
||||
<tr>
|
||||
<td>{{ session.id }}</td>
|
||||
<td>{{ session.when.lower }}</td>
|
||||
<td>{{ session.when.upper }}</td>
|
||||
<td>{{ session.duration }}</td>
|
||||
<td>{{ session.event_count }}</td>
|
||||
<td>{{ session.free_time }}</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=session.id %}" class="btn btn-primary btn-xs"><i class="fas fa-search"></i> Details</a>
|
||||
<a href="{% url 'backoffice:event_session_update' camp_slug=camp.slug pk=session.id %}" class="btn btn-primary btn-xs"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:event_session_delete' camp_slug=camp.slug pk=session.id %}" class="btn btn-danger btn-xs"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
70
src/backoffice/templates/event_session_list.html
Normal file
70
src/backoffice/templates/event_session_list.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="h3">BackOffice - EventSessions</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p><i>EventSessions</i> define the <i>EventSlots</i> where <i>Events</i> of a certain <i>EventType</i> at a certain <i>EventLocation</i> can be scheduled. <i>EventSessions</i> and <i>EventSlots</i> are used in the following ways in the system:</p>
|
||||
<ul>
|
||||
<li>To customise the speaker availability form shown when users submit speakers.</li>
|
||||
<li>They are also used to assist and restrict manual scheduling in backoffice.</li>
|
||||
<li>They are also used by the AutoScheduler.</li>
|
||||
</ul>
|
||||
<p>Search for an EventType or EventLocation to filter the list.</p>
|
||||
{% if not event_session_list %}
|
||||
<p class="lead">
|
||||
No EventSessions found. Go create one!
|
||||
<a href="{% url 'backoffice:event_session_create_type_select' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create New EventSession</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a class="btn btn-success" href="{% url 'backoffice:event_session_create_type_select' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create New EventSession</a>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>When</th>
|
||||
<th>Description</th>
|
||||
<th class="text-center">Duration</th>
|
||||
<th class="text-center">Scheduled Events</th>
|
||||
<th>Event Type</th>
|
||||
<th>Event Location</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in event_session_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=session.id %}"> {{ session.id }}</a></td>
|
||||
<td>{{ session.when.lower }} to<br>{{ session.when.upper }}</td>
|
||||
<td>{{ session.description|default:"N/A" }}</td>
|
||||
<td class="text-center"><span class="badge">{{ session.duration }}</span><br><span class="badge">{{ session.event_slots.count }} slots</span></td>
|
||||
<td class="text-center"><span class="badge">{{ session.scheduled_event_slots.count }}</td>
|
||||
<td><i class="fas fa-{{ session.event_type.icon }}" style="color: {{ session.event_type.color }};"></i> <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=session.event_type.slug %}">{{ session.event_type }}</a></td>
|
||||
<td><i class="fas fa-{{ session.event_location.icon }}"></i> <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=session.event_location.slug %}">{{ session.event_location.name }}</a></td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=session.id %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
<a href="{% url 'backoffice:event_session_update' camp_slug=camp.slug pk=session.id %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:event_session_delete' camp_slug=camp.slug pk=session.id %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a class="btn btn-success" href="{% url 'backoffice:event_session_create_type_select' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create New Session</a>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
54
src/backoffice/templates/event_slot_detail.html
Normal file
54
src/backoffice/templates/event_slot_detail.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">EventSlot {{ event_slot.pk }} Details</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>EventSlot ID</th>
|
||||
<td>{{ event_slot.pk }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSession</th>
|
||||
<td>This EventSlot is part of <a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=event_slot.event_session.pk %}">EventSession {{ event_slot.event_session.pk }}</a> which happens at <i class="fas fa-{{ event_slot.event_session.event_location.icon }}"></i> {{ event_slot.event_session.event_location.name }} from {{ event_slot.event_session.when.lower }} to {{ event_slot.event_session.when.upper }}.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event Type</th>
|
||||
<td><i class="fas fa-{{ event_slot.event_session.event_type.icon }}" style="color: {{ event_slot.event_session.event_type.color }};"></i> <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=event_slot.event_session.event_type.slug %}">{{ event_slot.event_session.event_type }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event Location</th>
|
||||
<td><i class="fas fa-{{ event_slot.event_session.event_location.icon }}"></i> <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=event_slot.event_session.event_location.slug %}">{{ event_slot.event_session.event_location.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSlot Start</th>
|
||||
<td>{{ event_slot.when.lower }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSlot End</th>
|
||||
<td>{{ event_slot.when.upper }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSlot Duration</th>
|
||||
<td>{{ event_slot.duration }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Scheduled Event</th>
|
||||
<td>
|
||||
{% if event_slot.event %}
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event_slot.event.slug %}">{{ event_slot.event_type.icon_html }} {{ event_slot.event.title }}</a>
|
||||
<a href="{% url 'backoffice:event_slot_unschedule' camp_slug=camp.slug pk=event_slot.pk %}" class="btn btn-danger btn-xs"><i class="fas fa-times"></i> Unschedule Event</a>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a href="{% url 'backoffice:event_slot_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> List EventSlots</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
82
src/backoffice/templates/event_slot_list.html
Normal file
82
src/backoffice/templates/event_slot_list.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="h3">BackOffice - EventSlots</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p><i>EventSlots</i> are what we schedule <i>Events</i> in. Search for an Event, Speaker, EventType, EventLocation, or day to filter the list.</p>
|
||||
{% if not event_slot_list %}
|
||||
<p class="lead">
|
||||
No EventSlots found. Go create some EventSessions!
|
||||
<a href="{% url 'backoffice:event_session_create_type_select' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create EventSession</a>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
<table class="table table-striped datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
<th>Session</th>
|
||||
<th>Type</th>
|
||||
<th>Location</th>
|
||||
<th>Event</th>
|
||||
<th>Speakers</th>
|
||||
<th>Overflow</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in event_slot_list %}
|
||||
<tr>
|
||||
<td>{{ slot.when.lower }}</td>
|
||||
<td>{{ slot.when.upper }}</td>
|
||||
<td><a href="{% url 'backoffice:event_session_detail' camp_slug=camp.slug pk=slot.event_session.id %}" class="btn btn-primary"><i class="fas fa-chess-board"></i> {{ slot.event_session.id }}</a></td>
|
||||
<td>{{ slot.event_type.icon_html }} <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=slot.event_type.slug %}">{{ slot.event_type.name }}</a></td>
|
||||
<td>{{ slot.event_location.icon_html }} <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=slot.event_location.slug %}">{{ slot.event_location.name }}</a></td>
|
||||
{% if slot.event %}
|
||||
<td><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=slot.event.slug %}">{{ slot.event.title }}</a><br>({% if slot.autoscheduled %}autoscheduled{% else %}manual{% endif %})</td>
|
||||
<td>
|
||||
{% if slot.event.speakers.exists %}
|
||||
<ul class="list-group">
|
||||
{% for speaker in slot.event.speakers.all %}
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item"><i class="fas fa-user"></i> {{ speaker.name }}</li></a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</ul>
|
||||
</td>
|
||||
<td class="text-center {% if slot.overflow %}danger{% endif %}"><span class="badge">{{ slot.overflow }}</span></td>
|
||||
{% else %}
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.id %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
{% if slot.event %}
|
||||
<a href="{% url 'backoffice:event_slot_unschedule' camp_slug=camp.slug pk=slot.id %}" class="btn btn-danger"><i class="fas fa-times"></i> Unschedule</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
24
src/backoffice/templates/event_slot_unschedule.html
Normal file
24
src/backoffice/templates/event_slot_unschedule.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Unschedule Event <i>{{ event_slot.event.title }}</i>?</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>This EventSlot runs from <b>{{ event_slot.when.lower }} to {{ event_slot.when.upper }}</b>. It is part of an EventSession for {{ event_slot.event_type.icon_html }} {{ event_slot.event_type }} at location {{ event_slot.event_location }}.</p>
|
||||
<p>The scheduled Event is titled <i>{{ event_slot.event.title }}</i></p>
|
||||
|
||||
<p>Really unschedule this Event?</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-danger"><i class='fas fa-times'></i> Yes, Unschedule It</button>
|
||||
<a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=event_slot.pk %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
91
src/backoffice/templates/event_type_detail.html
Normal file
91
src/backoffice/templates/event_type_detail.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Details for EventType: {{ event_type.name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Event Type</th>
|
||||
<td>
|
||||
<i class="fas fa-{{ event_type.icon }}" style="color: {{ event_type.color }};"></i> {{ event_type }}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EventSessions</th>
|
||||
<td>
|
||||
{% if event_sessions %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
<th class="text-center">Slots</th>
|
||||
<th class="text-center">Events</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in event_sessions %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ session.event_location.icon_html }} <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=session.event_location.slug %}">{{ session.event_location.name }}</a>
|
||||
</td>
|
||||
<td>{{ session.when.lower }}</td>
|
||||
<td>{{ session.when.upper }}</td>
|
||||
<td class="text-center"><span class="badge">{{ session.event_slots.count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ session.scheduled_event_slots.count }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Events</th>
|
||||
<td>
|
||||
{% if events %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>People</th>
|
||||
<th>Slots</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr>
|
||||
<td>{{ event.event_type.icon_html }} <a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a></td>
|
||||
<td>
|
||||
<ul class="list-group">
|
||||
{% for speaker in event.speakers.all %}
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item"><i class="fas fa-user"></i> {{ speaker.name }}</a>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="list-group">
|
||||
{% for slot in event.event_slots.all %}
|
||||
<a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.pk %}" class="list-group-item">From {{ slot.when.lower }} to {{ slot.when.upper }} at {{ slot.event_location.name }}</a>{% endfor %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a href="{% url 'backoffice:event_type_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> EventType List</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
50
src/backoffice/templates/event_type_list.html
Normal file
50
src/backoffice/templates/event_type_list.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="h3">BackOffice - EventTypes</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p><i>EventTypes</i> define categories or types of events, like "Music Act" or "Workshop" or "Talk". They are not camp specific, so they are not editable here.</p>
|
||||
{% if not event_type_list %}
|
||||
<p class="lead">
|
||||
No EventTypes found. Is this the twillight zone?
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-center">Sessions</th>
|
||||
<th class="text-center">Slots</th>
|
||||
<th class="text-center">Events</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for type in event_type_list %}
|
||||
<tr>
|
||||
<td>{{ type.icon_html }} <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=type.slug %}">{{ type.name }}</a></td>
|
||||
<td class="text-center"><span class="badge">{{ type.event_sessions_count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ type.event_slots_count }}</span></td>
|
||||
<td class="text-center"><span class="badge">{{ type.event_count }}</span></td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=type.slug %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
21
src/backoffice/templates/event_update.html
Normal file
21
src/backoffice/templates/event_update.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Update Event: {{ event.title }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Note: Any changes made here will be overwritten if the EventProposal for this event is later re-approved.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Update Event</button>
|
||||
<a href="{% url 'backoffice:event_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,11 +1,6 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Manage Expenses for {{ camp.title }}</h2>
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<table class="table">
|
||||
<caption>Event Differences - each row shows an event which changed time or location</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Old Location</th>
|
||||
<th>New Location</th>
|
||||
<th>Old Start Time</th>
|
||||
<th>New Start Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for eventdiff in diff.event_diffs %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if eventdiff.event.slug %}
|
||||
<a href="{% url "program:event_detail" camp_slug=camp.slug event_slug=eventdiff.event.slug %}">{{ eventdiff.event.title }}</a>
|
||||
{% else %}
|
||||
Event ID {{ eventdiff.event }} (deleted)
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ eventdiff.old.event_location.name|default:"N/A" }}</td>
|
||||
<td>{{ eventdiff.new.event_location.name|default:"N/A" }}</td>
|
||||
<td>{{ eventdiff.old.starttime|default:"N/A" }}</td>
|
||||
<td>{{ eventdiff.new.starttime|default:"N/A" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table">
|
||||
<caption>Slot Differences - each row represents a slot with changed content</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>EventLocation</th>
|
||||
<th>Start Time</th>
|
||||
<th>Old Event</th>
|
||||
<th>New Event</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slotdiff in diff.slot_diffs %}
|
||||
<tr>
|
||||
<td>{{ slotdiff.event_location.name }}</td>
|
||||
<td>{{ slotdiff.starttime }}</td>
|
||||
<td>
|
||||
{% if slotdiff.old.event %}
|
||||
{% if slotdiff.old.event.title %}{{ slotdiff.old.event.title }}{% else %}Event ID {{ slotdiff.old.event }} (deleted){% endif %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if slotdiff.new.event %}
|
||||
{% if slotdiff.new.event.title %}{{ slotdiff.new.event.title }}{% else %}Event ID {{ slotdiff.new.event }} (deleted){% endif %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<table class="table table-striped{% if not nodatatable %} datatable{% endif %}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Event Type</th>
|
||||
<th>Tags</th>
|
||||
{% if not nopeople %}<th>People</th>{% endif %}
|
||||
{% if not noschedule %}<th>Scheduled</th>{% endif %}
|
||||
{% if not noactions %}<th>Actions</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in event_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a></td>
|
||||
<td>{{ event.event_type.icon_html }} <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=event.event_type.slug %}">{{ event.event_type }}</a></td>
|
||||
<td>
|
||||
{% for tag in event.tags.all %}
|
||||
<span class="badge">{{ tag }}</span><br>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% if not nopeople %}
|
||||
<td>
|
||||
{% for speaker in event.speakers.all %}
|
||||
<i class="fas fa-user"></i> <a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker.name }}</a><br>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if not noschedule %}
|
||||
<td>
|
||||
{% if event.event_slots.exists %}
|
||||
{% for slot in event.event_slots.all %}
|
||||
{{ slot.event_location.icon_html }}
|
||||
<a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.pk %}">{{ slot.event_location.name }}, {{ slot.when.lower }}</a><br>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Not scheduled yet
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if not noactions %}
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
<a href="{% url 'backoffice:event_update' camp_slug=camp.slug slug=event.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:event_delete' camp_slug=camp.slug slug=event.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,58 @@
|
|||
<table class="table table-hover {% if not nodatatable %}datatable{% endif %}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Tags</th>
|
||||
<th>Speaker Proposals</th>
|
||||
<th>Event?</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in event_proposal_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:event_proposal_detail' camp_slug=camp.slug pk=proposal.pk %}">{{ proposal.title }}</a></td>
|
||||
<td><span class="badge">{{ proposal.proposal_status }}</span></td>
|
||||
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> <a href="{% url 'backoffice:event_type_detail' camp_slug=camp.slug slug=proposal.event_type.slug %}">{{ proposal.event_type }}</a></td>
|
||||
<td>
|
||||
{% for tag in proposal.tags.all %}
|
||||
<span class="badge">{{ tag }}</span><br>
|
||||
{% empty %}
|
||||
N/A
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if proposal.speakers.exists %}
|
||||
<ul class="list-group">
|
||||
{% for speaker in proposal.speakers.all %}
|
||||
<a href="{% url 'backoffice:speaker_proposal_detail' camp_slug=camp.slug pk=speaker.pk %}" class="list-group-item"><i class="fas fa-user{% if speaker.proposal_status == "approved" %} text-success{% elif speaker.proposal_status == "rejected" %} text-danger{% endif %}" data-toggle="tooltip" title="Proposal {{ speaker.proposal_status }}"></i> {{ speaker.name }}</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if proposal.event %}
|
||||
<td>
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=proposal.event.slug %}" class="btn btn-default btn-sm">{{ proposal.event_type.icon_html }} Event</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="text-center"><i class="fas fa-times fa-2x text-danger"></i></td>
|
||||
{% endif %}
|
||||
<td>{{ proposal.user }}</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:event_proposal_detail' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
{% if proposal.proposal_status == "pending" %}
|
||||
<a href="{% url 'backoffice:event_proposal_approve_reject' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-success"><i class="fas fa-check"></i> Approve</a>
|
||||
<a href="{% url 'backoffice:event_proposal_approve_reject' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-danger"><i class="fas fa-times"></i> Reject</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not notitle %}<th>Event</th>{% endif %}
|
||||
<th>Slot</th>
|
||||
<th>Location</th>
|
||||
<th>Duration</th>
|
||||
<th class="text-center">Overflow</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in event_slot_list %}
|
||||
<tr>
|
||||
{% if not notitle %}
|
||||
<td><a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=slot.event.slug %}">{{ slot.event.title }}</a></td>
|
||||
{% endif %}
|
||||
<td><a href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.pk %}">{{ slot.when.lower }}</a></td>
|
||||
<td>{{ slot.event_location.icon_html }} <a href="{% url 'backoffice:event_location_detail' camp_slug=camp.slug slug=slot.event_location.slug %}">{{ slot.event_location.name }}</a></td>
|
||||
<td>{{ slot.duration }}</td>
|
||||
<td class="text-center"><span class="badge">{{ slot.overflow }}</span></td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url 'backoffice:event_slot_detail' camp_slug=camp.slug pk=slot.pk %}"><i class="fas fa-search"></i> Show</a>
|
||||
<a class="btn btn-danger" href="{% url 'backoffice:event_slot_unschedule' camp_slug=camp.slug pk=slot.pk %}"><i class="fas fa-times"></i> Unschedule</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,46 @@
|
|||
<table class="table table-striped{% if not nodatatable %} datatable{% endif %}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Submitter</th>
|
||||
<th>Proposal</th>
|
||||
<th class="text-center">Event Conflicts</th>
|
||||
<th class="text-center">Events</th>
|
||||
{% if not noactions %}<th>Actions</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for speaker in speaker_list %}
|
||||
<tr>
|
||||
<td><i class="fas fa-user"></i> <a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker.name }}</a></td>
|
||||
<td>{{ speaker.email }}</td>
|
||||
<td><i class="fas fa-user"></i> {{ speaker.proposal.user }}</td>
|
||||
<td><a href="{% url 'backoffice:speaker_proposal_detail' camp_slug=camp.slug pk=speaker.proposal.pk %}" class="btn btn-default"><i class="fas fa-search"></i> Show</a></td>
|
||||
<td class="text-center"><span class="badge">{{ speaker.event_conflicts.count }}</span></td>
|
||||
<td>
|
||||
{% if speaker.events.all %}
|
||||
<ul class="list-group">
|
||||
{% for event in speaker.events.all %}
|
||||
<a href="{% url 'backoffice:event_detail' camp_slug=camp.slug slug=event.slug %}" class="list-group-item">
|
||||
<i class="fas fa-{{ event.event_type.icon }} fa-lg fa-fw" style="color: {{ event.event_type.color }};"></i> {{ event.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if not noactions %}
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
<a href="{% url 'backoffice:speaker_update' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:speaker_delete' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,41 @@
|
|||
{% load bornhack %}
|
||||
<table class="table table-hover {% if not nodatatable %}datatable{% endif %}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th class="text-center">Needs Ticket?</th>
|
||||
<th>Proposal Status</th>
|
||||
<th class="text-center">Has Speaker?</th>
|
||||
<th>Submitting User</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in speaker_proposal_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'backoffice:speaker_proposal_detail' camp_slug=camp.slug pk=proposal.uuid %}">{{ proposal.name }}</a></td>
|
||||
<td>{{ proposal.email }}</td>
|
||||
<td class="text-center">{{ proposal.needs_oneday_ticket|truefalseicon }}</td>
|
||||
<td class="text-center"><span class="badge">{{ proposal.proposal_status }}</span></td>
|
||||
<td class="text-center">
|
||||
{% if proposal.speaker %}
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=proposal.speaker.slug %}" class="btn btn-default btn-sm"><i class="fas fa-user"></i> Show Speaker</a>
|
||||
{% else %}
|
||||
<i class="fas fa-times"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ proposal.user }}</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'backoffice:speaker_proposal_detail' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
{% if proposal.proposal_status == "pending" %}
|
||||
<a href="{% url 'backoffice:speaker_proposal_approve_reject' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-success"><i class="fas fa-check"></i> Approve</a>
|
||||
<a href="{% url 'backoffice:speaker_proposal_approve_reject' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-danger"><i class="fas fa-times"></i> Reject</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -4,15 +4,12 @@
|
|||
{% load imageutils %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h2>{{ camp.title }} Backoffice</h2>
|
||||
<div class="lead">
|
||||
Welcome to the promised land! Please select your desired action below:
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{ camp.title }} Backoffice</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Welcome to the promised land! Please select your desired action below:</p>
|
||||
<div class="list-group">
|
||||
{% for team in facilityfeedback_teams %}
|
||||
{% if "camps."|add:team.permission_set in perms %}
|
||||
|
@ -52,13 +49,49 @@
|
|||
|
||||
{% if perms.camps.contentteam_permission %}
|
||||
<h3>Content Team</h3>
|
||||
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Manage Proposals</h4>
|
||||
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:approve_eventfeedback' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<a href="{% url 'backoffice:approve_event_feedback' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Approve Feedback</h4>
|
||||
<p class="list-group-item-text">Use this view to approve or reject EventFeedback</p>
|
||||
<p class="list-group-item-text">Use these views to approve or reject EventFeedback</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:pending_proposals' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Approve/Reject Pending Proposals</h4>
|
||||
<p class="list-group-item-text">Use these views to approve/reject pending SpeakerProposals and EventProposals</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:autoschedule_manage' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">AutoScheduler</h4>
|
||||
<p class="list-group-item-text">Use these views to manage the AutoScheduler</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Events</h4>
|
||||
<p class="list-group-item-text">Use these views to manage Events</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_location_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">EventLocations</h4>
|
||||
<p class="list-group-item-text">Use these views to manage EventLocations</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_proposal_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">EventProposals</h4>
|
||||
<p class="list-group-item-text">Use these views to see all EventProposals</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_session_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">EventSessions</h4>
|
||||
<p class="list-group-item-text">Use these views to manage <i>EventSession</i> objects. Each <i>EventSession</i> is parent to one or more related <i>EventSlot</i> objects, which is what <i>Events</i> are scheduled in.</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_slot_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">EventSlots</h4>
|
||||
<p class="list-group-item-text">Use these views to see <i>EventSlot</i> objects. <i>Events</i> are scheduled in <i>EventSlots</i>. An <i>EventSlot</i> belong to an <i>EventSession</i>, which is what defines the <i>EventType</i> and <i>EventLocation</i> of the <i>EventSlot</i>.</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:event_type_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">EventTypes</h4>
|
||||
<p class="list-group-item-text">Use these views to manage EventTypes</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:speaker_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Speakers</h4>
|
||||
<p class="list-group-item-text">Use these views to manage Speakers</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:speaker_proposal_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">SpeakerProposals</h4>
|
||||
<p class="list-group-item-text">Use these views to see all SpeakerProposals</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
@ -116,6 +149,7 @@
|
|||
<p class="list-group-item-text">Use this view to see proxied content</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Manage {{ form.instance.event_type.name }} Proposal</h3>
|
||||
{% include 'includes/eventproposal_detail.html' with camp=camp %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>BackOffice - Manage Speaker+EventProposals</h2>
|
||||
<div class="lead">
|
||||
The Content team can approve or reject pending SpeakerProposals and EventProposals from this page.
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<h3>SpeakerProposals</h3>
|
||||
{% if not speakerproposals %}
|
||||
<p class="lead">No pending SpeakerProposals found</p>
|
||||
{% else %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th class="text-center">Ticket?</th>
|
||||
<th class="text-center">Speaker?</th>
|
||||
<th>Submitting User</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in speakerproposals %}
|
||||
<tr>
|
||||
<td>{{ proposal.name }}</td>
|
||||
<td>{{ proposal.email }}</td>
|
||||
<td class="text-center">{{ proposal.needs_oneday_ticket|truefalseicon }}</td>
|
||||
<td class="text-center">{{ proposal.event|truefalseicon }}</td>
|
||||
<td>{{ proposal.user }}</td>
|
||||
<td><a href="{% url 'backoffice:speakerproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<h3>EventProposals</h3>
|
||||
{% if not eventproposals %}
|
||||
<p class="lead">No pending SpeakerProposals found</p>
|
||||
{% else %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Track</th>
|
||||
<th>Type</th>
|
||||
<th>Speakers</th>
|
||||
<th class="text-center">Event?</th>
|
||||
<th>Submitting User</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in eventproposals %}
|
||||
<tr>
|
||||
<td>{{ proposal.title }}</td>
|
||||
<td>{{ proposal.track }}</td>
|
||||
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> {{ proposal.event_type }}</td>
|
||||
<td>{% for speaker in proposal.speakers.all %}<i class="fas fa-user{% if speaker.proposal_status == "approved" %} text-success{% elif speaker.proposal_status == "rejected" %} text-danger{% endif %}" data-toggle="tooltip" title="{{ speaker.name }} ({{ speaker.proposal_status }})"></i> {% endfor %}</td>
|
||||
<td class="text-center">{{ proposal.speaker|truefalseicon }}</td>
|
||||
<td>{{ proposal.user }}</td>
|
||||
<td><a href="{% url 'backoffice:eventproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Manage Speaker Proposal</h3>
|
||||
{% include 'includes/speakerproposal_detail.html' with camp=camp %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Merchandise To Order</h2>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Merchandise Type</th>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Merchandise Orders</h2>
|
||||
|
@ -17,7 +13,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Village Orders</h2>
|
||||
|
@ -17,7 +13,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
|
|
32
src/backoffice/templates/pending_proposals.html
Normal file
32
src/backoffice/templates/pending_proposals.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% load bornhack %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">BackOffice - Pending Proposals</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<a href="{% url 'backoffice:index' camp_slug=camp.slug%}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a>
|
||||
<p>
|
||||
<h3>Pending SpeakerProposals</h3>
|
||||
{% if not speaker_proposal_list %}
|
||||
<p class="lead">No pending SpeakerProposals found</p>
|
||||
{% else %}
|
||||
{% include 'includes/speaker_proposal_list_table_backoffice.html' %}
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Pending EventProposals</h3>
|
||||
{% if not event_proposal_list %}
|
||||
<p class="lead">No pending EventProposals found</p>
|
||||
{% else %}
|
||||
{% include 'includes/event_proposal_list_table_backoffice.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Hand Out Products</h2>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Reimbursements for {{ camp.title }}</h2>
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Manage Revenues for {{ camp.title }}</h2>
|
||||
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Shop Tickets</h2>
|
||||
|
@ -15,7 +11,7 @@
|
|||
<span class="clearfix"></span>
|
||||
<hr class="clearfix"/>
|
||||
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket Type</th>
|
||||
|
|
19
src/backoffice/templates/speaker_delete.html
Normal file
19
src/backoffice/templates/speaker_delete.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">Delete Speaker {{ object.name }}?</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Deleting a Speaker object will remove the person from all Events, scheduled or not. The change will immediately take effect on the live site. The speaker can later be recreated by re-approving the SpeakerProposal if needed.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger"><i class='fas fa-times'></i> Yes, Delete Speaker</button>
|
||||
<a href="{% url 'backoffice:speaker_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
68
src/backoffice/templates/speaker_detail_backoffice.html
Normal file
68
src/backoffice/templates/speaker_detail_backoffice.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load bornhack %}
|
||||
{% load program %}
|
||||
{% load commonmark %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Speaker: {{ speaker.name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ speaker.biography|untrustedcommonmark }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Details for <i>{{ speaker.name }}</i></h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>{{ speaker.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Proposal</th>
|
||||
<td><a href="{% url 'backoffice:speaker_proposal_detail' camp_slug=camp.slug pk=speaker.proposal.pk %}">{{ speaker.proposal.pk }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Availability for <i>{{ speaker.name }}</i></h4>
|
||||
{% availabilitytable matrix=matrix %}
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Events for <i>{{ speaker.name }}</i></h4>
|
||||
{% if speaker.events.exists %}
|
||||
{% include "includes/event_list_table_backoffice.html" with event_list=speaker.events.all noactions=True noschedule=True nodatatable=True %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Schedule for <i>{{ speaker.name }}</i></h4>
|
||||
{% if speaker.scheduled_event_slots.exists %}
|
||||
{% include "includes/event_slot_list_backoffice.html" with event_slot_list=speaker.scheduled_event_slots.all %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Event Conflicts for <i>{{ speaker.name }}</i></h4>
|
||||
{% if speaker.event_conflicts.exists %}
|
||||
{% include "includes/event_list_table_backoffice.html" with event_list=speaker.event_conflicts.all nodatatable=True nopeople=True noactions=True noschedule=True %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<a href="{% url 'backoffice:speaker_update' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
|
||||
<a href="{% url 'backoffice:speaker_delete' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
|
||||
<a href="{% url 'backoffice:speaker_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Speaker List</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
20
src/backoffice/templates/speaker_list_backoffice.html
Normal file
20
src/backoffice/templates/speaker_list_backoffice.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends 'program_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Speaker List | Backoffice | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if speaker_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title">Speaker List - Backoffice</h3></div>
|
||||
<div class="panel-body">
|
||||
<p><i>Speakers</i> are the result of approving a <i>SpeakerProposal</i>. This is an alphabetical list of all speakers, workshop hosts, artists and other event anchors at {{ camp.title }}. <b>Any changes made here will be reflected immediately on the live site!</b></p>
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a></p>
|
||||
{% include "includes/speaker_list_table_backoffice.html" %}
|
||||
<p><a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a></p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="lead">No speakers found for {{ camp.title }}</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
|
@ -0,0 +1,21 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Approve or Reject SpeakerProposal: {{ speaker_proposal.name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{{ speaker_proposal.name }} will receive an email when the proposal is approved or rejected. It is possible to include an extra message in the form below explaining why the proposal was accepted or rejected. If the field is left blank a standard email is sent.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-success" name="approve"><i class='fas fa-check'></i> Approve Proposal</button>
|
||||
<button type="reject" class="btn btn-danger" name="reject"><i class='fas fa-times'></i> Reject Proposal</button>
|
||||
<a href="{% url 'backoffice:pending_proposals' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load program %}
|
||||
|
||||
{% block content %}
|
||||
<p><a href="{% url 'backoffice:speaker_proposal_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to SpeakerProposal List</a></p>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><span class="h3">{{ speaker_proposal.title }} Proposal: {{ speaker_proposal.name }}</span></div>
|
||||
<div class="panel-body">
|
||||
{{ speaker_proposal.biography|untrustedcommonmark }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Details for <i>{{ speaker_proposal.name }}</i></h4>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<td>{{ speaker_proposal.uuid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>{{ speaker_proposal.proposal_status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Speaker Object</th>
|
||||
<td>
|
||||
{% if speaker_proposal.speaker %}
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker_proposal.speaker.slug %}" class="btn btn-default"><i class="fas fa-user"></i> Show</a>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{ speaker_proposal.user }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Submission Notes</th>
|
||||
<td>{{ speaker_proposal.submission_notes|untrustedcommonmark|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Availability for <i>{{ speaker_proposal.name }}</i></h4>
|
||||
{% availabilitytable matrix=matrix %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>URLs for <i>{{ speaker_proposal.name }}</i></h4>
|
||||
{% if speaker_proposal.urls.exists %}
|
||||
{% include 'includes/speaker_proposal_url_table.html' %}
|
||||
{% else %}
|
||||
<i>Nothing found.</i>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>EventProposals involving <i>{{ speaker_proposal.name }}</i></h4>
|
||||
{% if speaker_proposal.event_proposals.exists %}
|
||||
{% include 'includes/event_proposal_list_table_backoffice.html' with event_proposal_list=speaker_proposal.event_proposals.all nodatatable=True %}
|
||||
{% else %}
|
||||
<i>Nothing found.</i>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Event Conflicts for <i>{{ speaker_proposal.name }}</i></h4>
|
||||
{% if speaker_proposal.event_conflicts.exists %}
|
||||
{% include 'includes/event_list_table_backoffice.html' with event_list=speaker_proposal.event_conflicts.all nodatatable=True nopeople=True noactions=True noschedule=True %}
|
||||
{% else %}
|
||||
<i>Nothing found.</i>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="panel-footer">Status: <span class="badge">{{ speaker_proposal.proposal_status }}</span> | ID: <span class="badge">{{ speaker_proposal.uuid }}</span></div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{% if speaker_proposal.proposal_status == "pending" %}
|
||||
<a href="{% url 'backoffice:speaker_proposal_approve_reject' camp_slug=camp.slug pk=speaker_proposal.uuid %}" class="btn btn-success"><i class="fas fa-check"></i> Approve SpeakerProposal</a>
|
||||
<a href="{% url 'backoffice:speaker_proposal_approve_reject' camp_slug=camp.slug pk=speaker_proposal.uuid %}" class="btn btn-danger"><i class="fas fa-times"></i> Reject SpeakerProposal</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'backoffice:speaker_proposal_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to SpeakerProposal List</a>
|
||||
</p>
|
||||
|
||||
{% endblock content %}
|
24
src/backoffice/templates/speaker_proposal_list.html
Normal file
24
src/backoffice/templates/speaker_proposal_list.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
SpeakerProposal List | Backoffice | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if speaker_proposal_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title">SpeakerProposal List - Backoffice</h3></div>
|
||||
<div class="panel-body">
|
||||
<p>This is a list of all <i>SpeakerProposal</i> objects in the system. Search for name, email, username, or status to filter the table.</p>
|
||||
<p><a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a></p>
|
||||
{% if speaker_proposal_list %}
|
||||
{% include 'includes/speaker_proposal_list_table_backoffice.html' %}
|
||||
{% else %}
|
||||
No SpeakerProposals found.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
32
src/backoffice/templates/speaker_update.html
Normal file
32
src/backoffice/templates/speaker_update.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load bornhack %}
|
||||
{% load program %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Update Speaker: {{ speaker.name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">Note: All changes made here take effect immediately on the live site. Any changes will be overwritten if the SpeakerProposal is later re-approved.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if field.id_for_label == "id_email" %}
|
||||
{% availabilitytable form=form matrix=matrix %}
|
||||
{% bootstrap_field field %}
|
||||
{% else %}
|
||||
{% if field.name|slice:":12" != "availability" %}
|
||||
{% bootstrap_field field %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Update Speaker</button>
|
||||
<a href="{% url 'backoffice:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Ticket Check-In</h2>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket UUID</th>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{% load commonmark %}
|
||||
{% load static %}
|
||||
{% load imageutils %}
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Village Gear To Order</h2>
|
||||
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
|
|
|
@ -3,19 +3,50 @@ from django.urls import include, path
|
|||
from .views import (
|
||||
ApproveFeedbackView,
|
||||
ApproveNamesView,
|
||||
AutoScheduleApplyView,
|
||||
AutoScheduleCrashCourseView,
|
||||
AutoScheduleDebugEventConflictsView,
|
||||
AutoScheduleDebugEventSlotUnavailabilityView,
|
||||
AutoScheduleDiffView,
|
||||
AutoScheduleManageView,
|
||||
AutoScheduleValidateView,
|
||||
BackofficeIndexView,
|
||||
BackofficeProxyView,
|
||||
BadgeHandoutView,
|
||||
ChainDetailView,
|
||||
ChainListView,
|
||||
CredebtorDetailView,
|
||||
EventProposalManageView,
|
||||
EventDeleteView,
|
||||
EventDetailView,
|
||||
EventListView,
|
||||
EventLocationCreateView,
|
||||
EventLocationDeleteView,
|
||||
EventLocationDetailView,
|
||||
EventLocationListView,
|
||||
EventLocationUpdateView,
|
||||
EventProposalApproveRejectView,
|
||||
EventProposalDetailView,
|
||||
EventProposalListView,
|
||||
EventScheduleView,
|
||||
EventSessionCreateLocationSelectView,
|
||||
EventSessionCreateTypeSelectView,
|
||||
EventSessionCreateView,
|
||||
EventSessionDeleteView,
|
||||
EventSessionDetailView,
|
||||
EventSessionListView,
|
||||
EventSessionUpdateView,
|
||||
EventSlotDetailView,
|
||||
EventSlotListView,
|
||||
EventSlotUnscheduleView,
|
||||
EventTypeDetailView,
|
||||
EventTypeListView,
|
||||
EventUpdateView,
|
||||
ExpenseDetailView,
|
||||
ExpenseListView,
|
||||
FacilityFeedbackView,
|
||||
ManageProposalsView,
|
||||
MerchandiseOrdersView,
|
||||
MerchandiseToOrderView,
|
||||
PendingProposalsView,
|
||||
ProductHandoutView,
|
||||
ReimbursementCreateUserSelectView,
|
||||
ReimbursementCreateView,
|
||||
|
@ -27,7 +58,13 @@ from .views import (
|
|||
RevenueListView,
|
||||
ScanTicketsView,
|
||||
ShopTicketOverview,
|
||||
SpeakerProposalManageView,
|
||||
SpeakerDeleteView,
|
||||
SpeakerDetailView,
|
||||
SpeakerListView,
|
||||
SpeakerProposalApproveRejectView,
|
||||
SpeakerProposalDetailView,
|
||||
SpeakerProposalListView,
|
||||
SpeakerUpdateView,
|
||||
TicketCheckinView,
|
||||
VillageOrdersView,
|
||||
VillageToOrderView,
|
||||
|
@ -72,28 +109,314 @@ urlpatterns = [
|
|||
# village orders
|
||||
path("village_orders/", VillageOrdersView.as_view(), name="village_orders"),
|
||||
path("village_to_order/", VillageToOrderView.as_view(), name="village_to_order"),
|
||||
# manage proposals
|
||||
# manage SpeakerProposals and EventProposals
|
||||
path(
|
||||
"manage_proposals/",
|
||||
"proposals/",
|
||||
include(
|
||||
[
|
||||
path("", ManageProposalsView.as_view(), name="manage_proposals"),
|
||||
path(
|
||||
"speakers/<uuid:pk>/",
|
||||
SpeakerProposalManageView.as_view(),
|
||||
name="speakerproposal_manage",
|
||||
"pending/", PendingProposalsView.as_view(), name="pending_proposals"
|
||||
),
|
||||
path(
|
||||
"events/<uuid:pk>/",
|
||||
EventProposalManageView.as_view(),
|
||||
name="eventproposal_manage",
|
||||
"speakers/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
SpeakerProposalListView.as_view(),
|
||||
name="speaker_proposal_list",
|
||||
),
|
||||
path(
|
||||
"<uuid:pk>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
SpeakerProposalDetailView.as_view(),
|
||||
name="speaker_proposal_detail",
|
||||
),
|
||||
path(
|
||||
"approve_reject/",
|
||||
SpeakerProposalApproveRejectView.as_view(),
|
||||
name="speaker_proposal_approve_reject",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# approve eventfeedback objects
|
||||
path(
|
||||
"approve_feedback", ApproveFeedbackView.as_view(), name="approve_eventfeedback",
|
||||
"events/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventProposalListView.as_view(),
|
||||
name="event_proposal_list",
|
||||
),
|
||||
path(
|
||||
"<uuid:pk>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventProposalDetailView.as_view(),
|
||||
name="event_proposal_detail",
|
||||
),
|
||||
path(
|
||||
"approve_reject/",
|
||||
EventProposalApproveRejectView.as_view(),
|
||||
name="event_proposal_approve_reject",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage EventSession objects
|
||||
path(
|
||||
"event_sessions/",
|
||||
include(
|
||||
[
|
||||
path("", EventSessionListView.as_view(), name="event_session_list"),
|
||||
path(
|
||||
"create/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventSessionCreateTypeSelectView.as_view(),
|
||||
name="event_session_create_type_select",
|
||||
),
|
||||
path(
|
||||
"<slug:event_type_slug>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventSessionCreateLocationSelectView.as_view(),
|
||||
name="event_session_create_location_select",
|
||||
),
|
||||
path(
|
||||
"<slug:event_location_slug>/",
|
||||
EventSessionCreateView.as_view(),
|
||||
name="event_session_create",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
path(
|
||||
"<int:pk>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventSessionDetailView.as_view(),
|
||||
name="event_session_detail",
|
||||
),
|
||||
path(
|
||||
"update/",
|
||||
EventSessionUpdateView.as_view(),
|
||||
name="event_session_update",
|
||||
),
|
||||
path(
|
||||
"delete/",
|
||||
EventSessionDeleteView.as_view(),
|
||||
name="event_session_delete",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage EventSlot objects
|
||||
path(
|
||||
"event_slots/",
|
||||
include(
|
||||
[
|
||||
path("", EventSlotListView.as_view(), name="event_slot_list"),
|
||||
path(
|
||||
"<int:pk>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventSlotDetailView.as_view(),
|
||||
name="event_slot_detail",
|
||||
),
|
||||
path(
|
||||
"unschedule/",
|
||||
EventSlotUnscheduleView.as_view(),
|
||||
name="event_slot_unschedule",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage Speaker objects
|
||||
path(
|
||||
"speakers/",
|
||||
include(
|
||||
[
|
||||
path("", SpeakerListView.as_view(), name="speaker_list"),
|
||||
path(
|
||||
"<slug:slug>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"", SpeakerDetailView.as_view(), name="speaker_detail",
|
||||
),
|
||||
path(
|
||||
"update/",
|
||||
SpeakerUpdateView.as_view(),
|
||||
name="speaker_update",
|
||||
),
|
||||
path(
|
||||
"delete/",
|
||||
SpeakerDeleteView.as_view(),
|
||||
name="speaker_delete",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage EventType objects
|
||||
path(
|
||||
"event_types/",
|
||||
include(
|
||||
[
|
||||
path("", EventTypeListView.as_view(), name="event_type_list"),
|
||||
path(
|
||||
"<slug:slug>/",
|
||||
EventTypeDetailView.as_view(),
|
||||
name="event_type_detail",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage EventLocation objects
|
||||
path(
|
||||
"event_locations/",
|
||||
include(
|
||||
[
|
||||
path("", EventLocationListView.as_view(), name="event_location_list"),
|
||||
path(
|
||||
"create/",
|
||||
EventLocationCreateView.as_view(),
|
||||
name="event_location_create",
|
||||
),
|
||||
path(
|
||||
"<slug:slug>/",
|
||||
include(
|
||||
[
|
||||
path(
|
||||
"",
|
||||
EventLocationDetailView.as_view(),
|
||||
name="event_location_detail",
|
||||
),
|
||||
path(
|
||||
"update/",
|
||||
EventLocationUpdateView.as_view(),
|
||||
name="event_location_update",
|
||||
),
|
||||
path(
|
||||
"delete/",
|
||||
EventLocationDeleteView.as_view(),
|
||||
name="event_location_delete",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage Event objects
|
||||
path(
|
||||
"events/",
|
||||
include(
|
||||
[
|
||||
path("", EventListView.as_view(), name="event_list"),
|
||||
path(
|
||||
"<slug:slug>/",
|
||||
include(
|
||||
[
|
||||
path("", EventDetailView.as_view(), name="event_detail",),
|
||||
path(
|
||||
"update/",
|
||||
EventUpdateView.as_view(),
|
||||
name="event_update",
|
||||
),
|
||||
path(
|
||||
"schedule/",
|
||||
EventScheduleView.as_view(),
|
||||
name="event_schedule",
|
||||
),
|
||||
path(
|
||||
"delete/",
|
||||
EventDeleteView.as_view(),
|
||||
name="event_delete",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# manage AutoScheduler
|
||||
path(
|
||||
"autoscheduler/",
|
||||
include(
|
||||
[
|
||||
path("", AutoScheduleManageView.as_view(), name="autoschedule_manage",),
|
||||
path(
|
||||
"crashcourse/",
|
||||
AutoScheduleCrashCourseView.as_view(),
|
||||
name="autoschedule_crash_course",
|
||||
),
|
||||
path(
|
||||
"validate/",
|
||||
AutoScheduleValidateView.as_view(),
|
||||
name="autoschedule_validate",
|
||||
),
|
||||
path(
|
||||
"diff/", AutoScheduleDiffView.as_view(), name="autoschedule_diff",
|
||||
),
|
||||
path(
|
||||
"apply/",
|
||||
AutoScheduleApplyView.as_view(),
|
||||
name="autoschedule_apply",
|
||||
),
|
||||
path(
|
||||
"debug-event-slot-unavailability/",
|
||||
AutoScheduleDebugEventSlotUnavailabilityView.as_view(),
|
||||
name="autoschedule_debug_event_slot_unavailability",
|
||||
),
|
||||
path(
|
||||
"debug-event-conflicts/",
|
||||
AutoScheduleDebugEventConflictsView.as_view(),
|
||||
name="autoschedule_debug_event_conflicts",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
# approve EventFeedback objects
|
||||
path(
|
||||
"approve_feedback",
|
||||
ApproveFeedbackView.as_view(),
|
||||
name="approve_event_feedback",
|
||||
),
|
||||
# economy
|
||||
path(
|
||||
|
|
|
@ -4,28 +4,49 @@ from itertools import chain
|
|||
|
||||
import requests
|
||||
from camps.mixins import CampViewMixin
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.db.models import Sum
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.forms import modelformset_factory
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||
from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
|
||||
from facilities.models import FacilityFeedback
|
||||
from profiles.models import Profile
|
||||
from program.models import EventFeedback, EventProposal, SpeakerProposal
|
||||
from program.autoscheduler import AutoScheduler
|
||||
from program.mixins import AvailabilityMatrixViewMixin
|
||||
from program.models import (
|
||||
Event,
|
||||
EventFeedback,
|
||||
EventLocation,
|
||||
EventProposal,
|
||||
EventSession,
|
||||
EventSlot,
|
||||
EventType,
|
||||
Speaker,
|
||||
SpeakerProposal,
|
||||
)
|
||||
from program.utils import save_speaker_availability
|
||||
from shop.models import Order, OrderProductRelation
|
||||
from teams.models import Team
|
||||
from tickets.models import DiscountTicket, ShopTicket, SponsorTicket, TicketType
|
||||
|
||||
from .forms import (
|
||||
AutoScheduleApplyForm,
|
||||
AutoScheduleValidateForm,
|
||||
EventScheduleForm,
|
||||
SpeakerForm,
|
||||
)
|
||||
from .mixins import (
|
||||
ContentTeamPermissionMixin,
|
||||
EconomyTeamPermissionMixin,
|
||||
|
@ -188,7 +209,7 @@ class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView):
|
|||
Why the hell do the forms in the formset not include the object?
|
||||
"""
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["eventfeedback_list"] = self.queryset
|
||||
context["event_feedback_list"] = self.queryset
|
||||
context["formset"] = self.form_class(queryset=self.queryset)
|
||||
return context
|
||||
|
||||
|
@ -202,34 +223,37 @@ class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView):
|
|||
|
||||
def get_success_url(self, *args, **kwargs):
|
||||
return reverse(
|
||||
"backoffice:approve_eventfeedback", kwargs={"camp_slug": self.camp.slug}
|
||||
"backoffice:approve_event_feedback", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
|
||||
|
||||
class ManageProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
"""
|
||||
This view shows a list of pending SpeakerProposal and EventProposals.
|
||||
"""
|
||||
#######################################
|
||||
# MANAGE SPEAKER/EVENT PROPOSAL VIEWS
|
||||
|
||||
template_name = "manage_proposals.html"
|
||||
context_object_name = "speakerproposals"
|
||||
|
||||
class PendingProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This convenience view shows a list of pending proposals """
|
||||
|
||||
model = SpeakerProposal
|
||||
template_name = "pending_proposals.html"
|
||||
context_object_name = "speaker_proposal_list"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return SpeakerProposal.objects.filter(
|
||||
camp=self.camp, proposal_status=SpeakerProposal.PROPOSAL_PENDING
|
||||
)
|
||||
qs = super().get_queryset(**kwargs).filter(proposal_status="pending")
|
||||
qs = qs.prefetch_related("user", "urls", "speaker")
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["eventproposals"] = EventProposal.objects.filter(
|
||||
track__camp=self.camp, proposal_status=EventProposal.PROPOSAL_PENDING
|
||||
)
|
||||
context["event_proposal_list"] = self.camp.event_proposals.filter(
|
||||
proposal_status=EventProposal.PROPOSAL_PENDING
|
||||
).prefetch_related("event_type", "track", "speakers", "tags", "user", "event")
|
||||
return context
|
||||
|
||||
|
||||
class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
class ProposalApproveBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
"""
|
||||
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
|
||||
Shared logic between SpeakerProposalApproveView and EventProposalApproveView
|
||||
"""
|
||||
|
||||
fields = ["reason"]
|
||||
|
@ -247,26 +271,855 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi
|
|||
else:
|
||||
messages.error(self.request, "Unknown submit action")
|
||||
return redirect(
|
||||
reverse("backoffice:manage_proposals", kwargs={"camp_slug": self.camp.slug})
|
||||
reverse(
|
||||
"backoffice:pending_proposals", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SpeakerProposalManageView(ProposalManageBaseView):
|
||||
"""
|
||||
This view allows an admin to approve/reject SpeakerProposals
|
||||
"""
|
||||
class SpeakerProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view permits Content Team members to list SpeakerProposals """
|
||||
|
||||
model = SpeakerProposal
|
||||
template_name = "manage_speakerproposal.html"
|
||||
template_name = "speaker_proposal_list.html"
|
||||
context_object_name = "speaker_proposal_list"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
qs = qs.prefetch_related("user", "urls", "speaker")
|
||||
return qs
|
||||
|
||||
|
||||
class EventProposalManageView(ProposalManageBaseView):
|
||||
"""
|
||||
This view allows an admin to approve/reject EventProposals
|
||||
"""
|
||||
class SpeakerProposalDetailView(
|
||||
AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView,
|
||||
):
|
||||
""" This view permits Content Team members to see SpeakerProposal details """
|
||||
|
||||
model = SpeakerProposal
|
||||
template_name = "speaker_proposal_detail_backoffice.html"
|
||||
context_object_name = "speaker_proposal"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related("user", "urls")
|
||||
return qs
|
||||
|
||||
|
||||
class SpeakerProposalApproveRejectView(ProposalApproveBaseView):
|
||||
""" This view allows ContentTeam members to approve/reject SpeakerProposals """
|
||||
|
||||
model = SpeakerProposal
|
||||
template_name = "speaker_proposal_approve_reject.html"
|
||||
context_object_name = "speaker_proposal"
|
||||
|
||||
|
||||
class EventProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view permits Content Team members to list EventProposals """
|
||||
|
||||
model = EventProposal
|
||||
template_name = "manage_eventproposal.html"
|
||||
template_name = "event_proposal_list.html"
|
||||
context_object_name = "event_proposal_list"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"user",
|
||||
"urls",
|
||||
"event",
|
||||
"event_type",
|
||||
"speakers__event_proposals",
|
||||
"track",
|
||||
"tags",
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class EventProposalDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
""" This view permits Content Team members to see EventProposal details """
|
||||
|
||||
model = EventProposal
|
||||
template_name = "event_proposal_detail_backoffice.html"
|
||||
context_object_name = "event_proposal"
|
||||
|
||||
|
||||
class EventProposalApproveRejectView(ProposalApproveBaseView):
|
||||
""" This view allows ContentTeam members to approve/reject EventProposals """
|
||||
|
||||
model = EventProposal
|
||||
template_name = "event_proposal_approve_reject.html"
|
||||
context_object_name = "event_proposal"
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE SPEAKER VIEWS
|
||||
|
||||
|
||||
class SpeakerListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view is used by the Content Team to see Speaker objects. """
|
||||
|
||||
model = Speaker
|
||||
template_name = "speaker_list_backoffice.html"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"proposal__user",
|
||||
"events__event_slots",
|
||||
"events__event_type",
|
||||
"event_conflicts",
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class SpeakerDetailView(
|
||||
AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView
|
||||
):
|
||||
""" This view is used by the Content Team to see details for Speaker objects """
|
||||
|
||||
model = Speaker
|
||||
template_name = "speaker_detail_backoffice.html"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"event_conflicts", "events__event_slots", "events__event_type"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class SpeakerUpdateView(
|
||||
AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, UpdateView
|
||||
):
|
||||
""" This view is used by the Content Team to update Speaker objects """
|
||||
|
||||
model = Speaker
|
||||
template_name = "speaker_update.html"
|
||||
form_class = SpeakerForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
""" Set camp for the form """
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs.update({"camp": self.camp})
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
""" Save object and availability """
|
||||
speaker = form.save()
|
||||
save_speaker_availability(form, obj=speaker)
|
||||
messages.success(self.request, "Speaker has been updated")
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:speaker_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SpeakerDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
|
||||
""" This view is used by the Content Team to delete Speaker objects """
|
||||
|
||||
model = Speaker
|
||||
template_name = "speaker_delete.html"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
speaker = self.get_object()
|
||||
# delete related objects first
|
||||
speaker.availabilities.all().delete()
|
||||
speaker.urls.all().delete()
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request, f"Speaker '{self.get_object().name}' has been deleted"
|
||||
)
|
||||
return reverse("backoffice:speaker_list", kwargs={"camp_slug": self.camp.slug})
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE EVENTTYPE VIEWS
|
||||
|
||||
|
||||
class EventTypeListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view is used by the Content Team to list EventTypes """
|
||||
|
||||
model = EventType
|
||||
template_name = "event_type_list.html"
|
||||
context_object_name = "event_type_list"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.annotate(
|
||||
# only count events for the current camp
|
||||
event_count=Count(
|
||||
"events", distinct=True, filter=Q(events__track__camp=self.camp)
|
||||
),
|
||||
# only count EventSessions for the current camp
|
||||
event_sessions_count=Count(
|
||||
"event_sessions",
|
||||
distinct=True,
|
||||
filter=Q(event_sessions__camp=self.camp),
|
||||
),
|
||||
# only count EventSlots for the current camp
|
||||
event_slots_count=Count(
|
||||
"event_sessions__event_slots",
|
||||
distinct=True,
|
||||
filter=Q(event_sessions__camp=self.camp),
|
||||
),
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class EventTypeDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
""" This view is used by the Content Team to see details for EventTypes """
|
||||
|
||||
model = EventType
|
||||
template_name = "event_type_detail.html"
|
||||
context_object_name = "event_type"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["event_sessions"] = self.camp.event_sessions.filter(
|
||||
event_type=self.get_object()
|
||||
).prefetch_related("event_location", "event_slots")
|
||||
context["events"] = self.camp.events.filter(
|
||||
event_type=self.get_object()
|
||||
).prefetch_related(
|
||||
"speakers", "event_slots__event_session__event_location", "event_type"
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE EVENTLOCATION VIEWS
|
||||
|
||||
|
||||
class EventLocationListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view is used by the Content Team to list EventLocation objects. """
|
||||
|
||||
model = EventLocation
|
||||
template_name = "event_location_list.html"
|
||||
context_object_name = "event_location_list"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related("event_sessions__event_slots", "conflicts")
|
||||
return qs
|
||||
|
||||
|
||||
class EventLocationDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
""" This view is used by the Content Team to see details for EventLocation objects """
|
||||
|
||||
model = EventLocation
|
||||
template_name = "event_location_detail.html"
|
||||
context_object_name = "event_location"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"conflicts", "event_sessions__event_slots", "event_sessions__event_type"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class EventLocationCreateView(CampViewMixin, ContentTeamPermissionMixin, CreateView):
|
||||
""" This view is used by the Content Team to create EventLocation objects """
|
||||
|
||||
model = EventLocation
|
||||
fields = ["name", "icon", "capacity", "conflicts"]
|
||||
template_name = "event_location_form.html"
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super().get_form(*args, **kwargs)
|
||||
form.fields["conflicts"].queryset = self.camp.event_locations.all()
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
location = form.save(commit=False)
|
||||
location.camp = self.camp
|
||||
location.save()
|
||||
form.save_m2m()
|
||||
messages.success(
|
||||
self.request, f"EventLocation {location.name} has been created"
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:event_location_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": location.slug},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EventLocationUpdateView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
""" This view is used by the Content Team to update EventLocation objects """
|
||||
|
||||
model = EventLocation
|
||||
fields = ["name", "icon", "capacity", "conflicts"]
|
||||
template_name = "event_location_form.html"
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super().get_form(*args, **kwargs)
|
||||
form.fields["conflicts"].queryset = self.camp.event_locations.exclude(
|
||||
pk=self.get_object().pk
|
||||
)
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request, f"EventLocation {self.get_object().name} has been updated"
|
||||
)
|
||||
return reverse(
|
||||
"backoffice:event_location_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug},
|
||||
)
|
||||
|
||||
|
||||
class EventLocationDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
|
||||
""" This view is used by the Content Team to delete EventLocation objects """
|
||||
|
||||
model = EventLocation
|
||||
template_name = "event_location_delete.html"
|
||||
context_object_name = "event_location"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
slotsdeleted, slotdetails = self.get_object().event_slots.all().delete()
|
||||
sessionsdeleted, sessiondetails = (
|
||||
self.get_object().event_sessions.all().delete()
|
||||
)
|
||||
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request, f"EventLocation '{self.get_object().name}' has been deleted."
|
||||
)
|
||||
return reverse(
|
||||
"backoffice:event_location_list", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE EVENT VIEWS
|
||||
|
||||
|
||||
class EventListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view is used by the Content Team to see Event objects. """
|
||||
|
||||
model = Event
|
||||
template_name = "event_list_backoffice.html"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"speakers__events",
|
||||
"event_type",
|
||||
"event_slots__event_session__event_location",
|
||||
"tags",
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class EventDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
""" This view is used by the Content Team to see details for Event objects """
|
||||
|
||||
model = Event
|
||||
template_name = "event_detail_backoffice.html"
|
||||
|
||||
|
||||
class EventUpdateView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
""" This view is used by the Content Team to update Event objects """
|
||||
|
||||
model = Event
|
||||
fields = [
|
||||
"title",
|
||||
"abstract",
|
||||
"video_recording",
|
||||
"duration_minutes",
|
||||
"demand",
|
||||
"tags",
|
||||
]
|
||||
template_name = "event_update.html"
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Event has been updated")
|
||||
return reverse(
|
||||
"backoffice:event_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug},
|
||||
)
|
||||
|
||||
|
||||
class EventDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
|
||||
""" This view is used by the Content Team to delete Event objects """
|
||||
|
||||
model = Event
|
||||
template_name = "event_delete.html"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.get_object().urls.all().delete()
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request, f"Event '{self.get_object().title}' has been deleted!",
|
||||
)
|
||||
return reverse("backoffice:event_list", kwargs={"camp_slug": self.camp.slug})
|
||||
|
||||
|
||||
class EventScheduleView(CampViewMixin, ContentTeamPermissionMixin, FormView):
|
||||
""" This view is used by the Content Team to manually schedule Events.
|
||||
It shows a table with radioselect buttons for the available slots for the
|
||||
EventType of the Event """
|
||||
|
||||
form_class = EventScheduleForm
|
||||
template_name = "event_schedule.html"
|
||||
|
||||
def setup(self, *args, **kwargs):
|
||||
super().setup(*args, **kwargs)
|
||||
self.event = get_object_or_404(Event, slug=kwargs["slug"])
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super().get_form(*args, **kwargs)
|
||||
self.slots = []
|
||||
slotindex = 0
|
||||
# loop over sessions, get free slots
|
||||
for session in self.camp.event_sessions.filter(
|
||||
event_type=self.event.event_type
|
||||
):
|
||||
for slot in session.get_available_slots():
|
||||
# loop over speakers to see if they are all available
|
||||
for speaker in self.event.speakers.all():
|
||||
if not speaker.is_available(slot.when):
|
||||
# this speaker is not available, skip this slot
|
||||
break
|
||||
else:
|
||||
# all speakers are available for this slot
|
||||
self.slots.append({"index": slotindex, "slot": slot})
|
||||
slotindex += 1
|
||||
# add the slot choicefield
|
||||
form.fields["slot"] = forms.ChoiceField(
|
||||
widget=forms.RadioSelect,
|
||||
choices=[(s["index"], s["index"]) for s in self.slots],
|
||||
)
|
||||
return form
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
"""
|
||||
Add event to context
|
||||
"""
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["event"] = self.event
|
||||
context["event_slots"] = self.slots
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Set needed values, save slot and return
|
||||
"""
|
||||
slot = self.slots[int(form.cleaned_data["slot"])]["slot"]
|
||||
slot.event = self.event
|
||||
slot.autoscheduled = False
|
||||
slot.save()
|
||||
messages.success(
|
||||
self.request,
|
||||
f"{self.event.title} has been scheduled to begin at {slot.when.lower} at location {slot.event_location.name} successfully!",
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:event_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": self.event.slug},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE EVENTSESSION VIEWS
|
||||
|
||||
|
||||
class EventSessionCreateTypeSelectView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, ListView
|
||||
):
|
||||
"""
|
||||
This view is shown first when creating a new EventSession
|
||||
"""
|
||||
|
||||
model = EventType
|
||||
template_name = "event_session_create_type_select.html"
|
||||
context_object_name = "event_type_list"
|
||||
|
||||
|
||||
class EventSessionCreateLocationSelectView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, ListView
|
||||
):
|
||||
"""
|
||||
This view is shown second when creating a new EventSession
|
||||
"""
|
||||
|
||||
model = EventLocation
|
||||
template_name = "event_session_create_location_select.html"
|
||||
context_object_name = "event_location_list"
|
||||
|
||||
def setup(self, *args, **kwargs):
|
||||
super().setup(*args, **kwargs)
|
||||
self.event_type = get_object_or_404(EventType, slug=kwargs["event_type_slug"])
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
"""
|
||||
Add event_type to context
|
||||
"""
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["event_type"] = self.event_type
|
||||
return context
|
||||
|
||||
|
||||
class EventSessionFormViewMixin:
|
||||
"""
|
||||
A mixin with the stuff shared between EventSession{Create|Update}View
|
||||
"""
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
"""
|
||||
The default range widgets are a bit shit because they eat the help_text and
|
||||
have no indication of which field is for what. So we add a nice placeholder.
|
||||
We also limit the event_location dropdown to only the current camps locations.
|
||||
"""
|
||||
form = super().get_form(*args, **kwargs)
|
||||
form.fields["when"].widget.widgets[0].attrs = {
|
||||
"placeholder": f"Start Date and Time (YYYY-MM-DD HH:MM). Time zone is {settings.TIME_ZONE}.",
|
||||
}
|
||||
form.fields["when"].widget.widgets[1].attrs = {
|
||||
"placeholder": f"End Date and Time (YYYY-MM-DD HH:MM). Time zone is {settings.TIME_ZONE}.",
|
||||
}
|
||||
if hasattr(form.fields, "event_location"):
|
||||
form.fields["event_location"].queryset = EventLocation.objects.filter(
|
||||
camp=self.camp
|
||||
)
|
||||
return form
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
"""
|
||||
Add event_type and location and existing sessions to context
|
||||
"""
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
if not hasattr(self, "event_type"):
|
||||
self.event_type = self.get_object().event_type
|
||||
context["event_type"] = self.event_type
|
||||
|
||||
if not hasattr(self, "event_location"):
|
||||
self.event_location = self.get_object().event_location
|
||||
context["event_location"] = self.event_location
|
||||
|
||||
context["sessions"] = self.event_type.event_sessions.filter(camp=self.camp)
|
||||
return context
|
||||
|
||||
|
||||
class EventSessionCreateView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, EventSessionFormViewMixin, CreateView
|
||||
):
|
||||
"""
|
||||
This view is used by the Content Team to create EventSession objects
|
||||
"""
|
||||
|
||||
model = EventSession
|
||||
fields = ["description", "when", "event_duration_minutes"]
|
||||
template_name = "event_session_form.html"
|
||||
|
||||
def setup(self, *args, **kwargs):
|
||||
super().setup(*args, **kwargs)
|
||||
self.event_type = get_object_or_404(EventType, slug=kwargs["event_type_slug"])
|
||||
self.event_location = get_object_or_404(
|
||||
EventLocation, camp=self.camp, slug=kwargs["event_location_slug"]
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Set camp and event_type, check for overlaps and save
|
||||
"""
|
||||
session = form.save(commit=False)
|
||||
session.event_type = self.event_type
|
||||
session.event_location = self.event_location
|
||||
session.camp = self.camp
|
||||
session.save()
|
||||
messages.success(self.request, f"{session} has been created successfully!")
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EventSessionListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
"""
|
||||
This view is used by the Content Team to see EventSession objects.
|
||||
"""
|
||||
|
||||
model = EventSession
|
||||
template_name = "event_session_list.html"
|
||||
context_object_name = "event_session_list"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related("event_type", "event_location", "event_slots")
|
||||
return qs
|
||||
|
||||
|
||||
class EventSessionDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
"""
|
||||
This view is used by the Content Team to see details for EventSession objects
|
||||
"""
|
||||
|
||||
model = EventSession
|
||||
template_name = "event_session_detail.html"
|
||||
context_object_name = "session"
|
||||
|
||||
|
||||
class EventSessionUpdateView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, EventSessionFormViewMixin, UpdateView
|
||||
):
|
||||
"""
|
||||
This view is used by the Content Team to update EventSession objects
|
||||
"""
|
||||
|
||||
model = EventSession
|
||||
fields = ["when", "description", "event_duration_minutes"]
|
||||
template_name = "event_session_form.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Just save, we have a post_save signal which takes care of fixing EventSlots
|
||||
"""
|
||||
session = form.save()
|
||||
messages.success(self.request, f"{session} has been updated successfully!")
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EventSessionDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
|
||||
"""
|
||||
This view is used by the Content Team to delete EventSession objects
|
||||
"""
|
||||
|
||||
model = EventSession
|
||||
template_name = "event_session_delete.html"
|
||||
context_object_name = "session"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
""" Show a warning if we have something scheduled in this EventSession """
|
||||
if self.get_object().event_slots.filter(event__isnull=False).exists():
|
||||
messages.warning(
|
||||
self.request,
|
||||
"NOTE: One or more EventSlots in this EventSession has an Event scheduled. Make sure you are deleting the correct session!",
|
||||
)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
session = self.get_object()
|
||||
session.event_slots.all().delete()
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request,
|
||||
"EventSession and related EventSlots was deleted successfully!",
|
||||
)
|
||||
return reverse(
|
||||
"backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
|
||||
|
||||
################################
|
||||
# MANAGE EVENTSLOT VIEWS
|
||||
|
||||
|
||||
class EventSlotListView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
||||
""" This view is used by the Content Team to see EventSlot objects. """
|
||||
|
||||
model = EventSlot
|
||||
template_name = "event_slot_list.html"
|
||||
context_object_name = "event_slot_list"
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super().get_queryset(*args, **kwargs)
|
||||
qs = qs.prefetch_related(
|
||||
"event__speakers",
|
||||
"event_session__event_location",
|
||||
"event_session__event_type",
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class EventSlotDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView):
|
||||
""" This view is used by the Content Team to see details for EventSlot objects """
|
||||
|
||||
model = EventSlot
|
||||
template_name = "event_slot_detail.html"
|
||||
context_object_name = "event_slot"
|
||||
|
||||
|
||||
class EventSlotUnscheduleView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
""" This view is used by the Content Team to remove an Event from the schedule/EventSlot """
|
||||
|
||||
model = EventSlot
|
||||
template_name = "event_slot_unschedule.html"
|
||||
fields = []
|
||||
context_object_name = "event_slot"
|
||||
|
||||
def form_valid(self, form):
|
||||
event_slot = self.get_object()
|
||||
event = event_slot.event
|
||||
event_slot.unschedule()
|
||||
messages.success(
|
||||
self.request,
|
||||
f"The Event '{event.title}' has been removed from the slot {event_slot}",
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:event_detail",
|
||||
kwargs={"camp_slug": self.camp.slug, "slug": event.slug},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
################################
|
||||
# AUTOSCHEDULER VIEWS
|
||||
|
||||
|
||||
class AutoScheduleManageView(CampViewMixin, ContentTeamPermissionMixin, TemplateView):
|
||||
""" Just an index type view with links to the various actions """
|
||||
|
||||
template_name = "autoschedule_index.html"
|
||||
|
||||
|
||||
class AutoScheduleCrashCourseView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, TemplateView
|
||||
):
|
||||
""" A short crash course on the autoscheduler """
|
||||
|
||||
template_name = "autoschedule_crash_course.html"
|
||||
|
||||
|
||||
class AutoScheduleValidateView(CampViewMixin, ContentTeamPermissionMixin, FormView):
|
||||
""" This view is used to validate schedules. It uses the AutoScheduler and can
|
||||
either validate the currently applied schedule or a new similar schedule, or a
|
||||
brand new schedule """
|
||||
|
||||
template_name = "autoschedule_validate.html"
|
||||
form_class = AutoScheduleValidateForm
|
||||
|
||||
def form_valid(self, form):
|
||||
# initialise AutoScheduler
|
||||
scheduler = AutoScheduler(camp=self.camp)
|
||||
|
||||
# get autoschedule
|
||||
if form.cleaned_data["schedule"] == "current":
|
||||
autoschedule = scheduler.build_current_autoschedule()
|
||||
message = f"The currently scheduled Events form a valid schedule! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. {scheduler.events.count()} Events in the schedule."
|
||||
elif form.cleaned_data["schedule"] == "similar":
|
||||
original_autoschedule = scheduler.build_current_autoschedule()
|
||||
autoschedule, diff = scheduler.calculate_similar_autoschedule(
|
||||
original_autoschedule
|
||||
)
|
||||
message = f"The new similar schedule is valid! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. Differences to the current schedule: {len(diff['event_diffs'])} Event diffs and {len(diff['slot_diffs'])} Slot diffs."
|
||||
elif form.cleaned_data["schedule"] == "new":
|
||||
autoschedule = scheduler.calculate_autoschedule()
|
||||
message = f"The new schedule is valid! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. {scheduler.events.count()} Events in the schedule."
|
||||
|
||||
# check validity
|
||||
valid, violations = scheduler.is_valid(autoschedule, return_violations=True)
|
||||
if valid:
|
||||
messages.success(self.request, message)
|
||||
else:
|
||||
messages.error(self.request, "Schedule is NOT valid!")
|
||||
message = "Schedule violations:<br>"
|
||||
for v in violations:
|
||||
message += v + "<br>"
|
||||
messages.error(self.request, mark_safe(message))
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:autoschedule_validate", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AutoScheduleDiffView(CampViewMixin, ContentTeamPermissionMixin, TemplateView):
|
||||
template_name = "autoschedule_diff.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
scheduler = AutoScheduler(camp=self.camp)
|
||||
autoschedule, diff = scheduler.calculate_similar_autoschedule()
|
||||
context["diff"] = diff
|
||||
context["scheduler"] = scheduler
|
||||
return context
|
||||
|
||||
|
||||
class AutoScheduleApplyView(CampViewMixin, ContentTeamPermissionMixin, FormView):
|
||||
""" This view is used by the Content Team to apply a new schedules by unscheduling
|
||||
all autoscheduled Events, and scheduling all Event/Slot combinations in the schedule.
|
||||
|
||||
TODO: see comment in program.autoscheduler.AutoScheduler.apply() method.
|
||||
"""
|
||||
|
||||
template_name = "autoschedule_apply.html"
|
||||
form_class = AutoScheduleApplyForm
|
||||
|
||||
def form_valid(self, form):
|
||||
# initialise AutoScheduler
|
||||
scheduler = AutoScheduler(camp=self.camp)
|
||||
|
||||
# get autoschedule
|
||||
if form.cleaned_data["schedule"] == "similar":
|
||||
autoschedule, diff = scheduler.calculate_similar_autoschedule()
|
||||
elif form.cleaned_data["schedule"] == "new":
|
||||
autoschedule = scheduler.calculate_autoschedule()
|
||||
|
||||
# check validity
|
||||
valid, violations = scheduler.is_valid(autoschedule, return_violations=True)
|
||||
if valid:
|
||||
# schedule is valid, apply it
|
||||
deleted, created = scheduler.apply(autoschedule)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Schedule has been applied! {deleted} Events removed from schedule, {created} new Events scheduled. Differences to the previous schedule: {len(diff['event_diffs'])} Event diffs and {len(diff['slot_diffs'])} Slot diffs.",
|
||||
)
|
||||
else:
|
||||
messages.error(self.request, "Schedule is NOT valid, cannot apply!")
|
||||
return redirect(
|
||||
reverse(
|
||||
"backoffice:autoschedule_apply", kwargs={"camp_slug": self.camp.slug}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AutoScheduleDebugEventSlotUnavailabilityView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, TemplateView
|
||||
):
|
||||
template_name = "autoschedule_debug_slots.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
scheduler = AutoScheduler(camp=self.camp)
|
||||
context = {
|
||||
"scheduler": scheduler,
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class AutoScheduleDebugEventConflictsView(
|
||||
CampViewMixin, ContentTeamPermissionMixin, TemplateView
|
||||
):
|
||||
template_name = "autoschedule_debug_events.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
scheduler = AutoScheduler(camp=self.camp)
|
||||
context = {
|
||||
"scheduler": scheduler,
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
################################
|
||||
# MERCHANDISE VIEWS
|
||||
|
||||
|
||||
class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||
|
@ -315,6 +1168,10 @@ class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateVie
|
|||
return context
|
||||
|
||||
|
||||
################################
|
||||
# VILLAGE VIEWS
|
||||
|
||||
|
||||
class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
|
||||
template_name = "orders_village.html"
|
||||
|
||||
|
@ -763,11 +1620,8 @@ class ScanTicketsView(
|
|||
|
||||
|
||||
class ShopTicketOverview(LoginRequiredMixin, CampViewMixin, ListView):
|
||||
|
||||
model = ShopTicket
|
||||
|
||||
template_name = "shop_ticket_overview.html"
|
||||
|
||||
context_object_name = "shop_tickets"
|
||||
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
|
|
|
@ -67,6 +67,7 @@ TICKET_CATEGORY_NAME='Tickets'
|
|||
SCHEDULE_MIDNIGHT_OFFSET_HOURS=9
|
||||
SCHEDULE_TIMESLOT_LENGTH_MINUTES=30
|
||||
SCHEDULE_EVENT_NOTIFICATION_MINUTES=10
|
||||
SPEAKER_AVAILABILITY_DAYCHUNK_HOURS=3 # how many hours per speaker_availability form checkbox
|
||||
|
||||
# irc bot settings
|
||||
IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10
|
||||
|
|
|
@ -17,9 +17,9 @@ DATABASES = {
|
|||
"ENGINE": 'django.contrib.gis.db.backends.postgis',
|
||||
"NAME": "bornhack",
|
||||
"USER": "bornhack",
|
||||
# Comment back in if you are connecting via TCP
|
||||
# "PASSWORD": "bornhack",
|
||||
# "HOST": "localhost",
|
||||
#"PASSWORD": "bornhack",
|
||||
#"HOST": "localhost",
|
||||
#"PORT": 5433,
|
||||
}
|
||||
}
|
||||
DEBUG = True
|
||||
|
@ -37,6 +37,7 @@ MEDIA_ROOT = os.path.join(
|
|||
SCHEDULE_MIDNIGHT_OFFSET_HOURS = 9
|
||||
SCHEDULE_TIMESLOT_LENGTH_MINUTES = 30
|
||||
SCHEDULE_EVENT_NOTIFICATION_MINUTES = 10
|
||||
SPEAKER_AVAILABILITY_DAYCHUNK_HOURS=3
|
||||
|
||||
PDF_LETTERHEAD_FILENAME = "bornhack-2017_test_letterhead.pdf"
|
||||
PDF_ARCHIVE_PATH = os.path.join(MEDIA_ROOT, "pdf_archive")
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import os
|
||||
|
||||
# monkeypatch postgres Range object to support lookups
|
||||
from utils import range_fields # noqa: F401
|
||||
|
||||
from .environment_settings import * # noqa: F403
|
||||
|
||||
|
||||
|
@ -66,6 +69,7 @@ INSTALLED_APPS = [
|
|||
"reversion",
|
||||
"leaflet",
|
||||
"oauth2_provider",
|
||||
"taggit",
|
||||
]
|
||||
|
||||
# MEDIA_URL = '/media/'
|
||||
|
@ -79,7 +83,7 @@ USE_TZ = True
|
|||
SHORT_DATE_FORMAT = "Ymd"
|
||||
DATE_FORMAT = "l, M jS, Y"
|
||||
DATETIME_FORMAT = "l, M jS, Y, H:i (e)"
|
||||
TIME_FORMAT = "H:i (e)"
|
||||
TIME_FORMAT = "H:i"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
@ -117,7 +121,7 @@ LOGIN_URL = "/login/"
|
|||
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
|
||||
|
||||
BOOTSTRAP3 = {
|
||||
"jquery_url": "/static/js/jquery.min.js",
|
||||
"jquery_url": "/static/js/jquery-3.3.1.min.js",
|
||||
"javascript_url": "/static/js/bootstrap.min.js",
|
||||
}
|
||||
MIDDLEWARE = [
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.postgres.fields import DateTimeRangeField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from program.models import EventLocation, EventType
|
||||
from django.utils import timezone
|
||||
from psycopg2.extras import DateTimeTZRange
|
||||
from utils.models import CreatedUpdatedModel, UUIDModel
|
||||
|
||||
|
@ -153,20 +154,6 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
|||
def __str__(self):
|
||||
return "%s - %s" % (self.title, self.tagline)
|
||||
|
||||
@property
|
||||
def event_types(self):
|
||||
""" Return all event types with at least one event in this camp """
|
||||
return EventType.objects.filter(
|
||||
event__instances__isnull=False, event__camp=self
|
||||
).distinct()
|
||||
|
||||
@property
|
||||
def event_locations(self):
|
||||
""" Return all event locations with at least one event in this camp"""
|
||||
return EventLocation.objects.filter(
|
||||
eventinstances__isnull=False, camp=self
|
||||
).distinct()
|
||||
|
||||
@property
|
||||
def logo_small(self):
|
||||
return "img/%(slug)s/logo/%(slug)s-logo-s.png" % {"slug": self.slug}
|
||||
|
@ -201,31 +188,46 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
|||
logger.error("this attribute is not a datetimetzrange field: %s" % field)
|
||||
return False
|
||||
|
||||
daycount = (field.upper - field.lower).days
|
||||
# count how many unique dates we have in this range
|
||||
daycount = 1
|
||||
while True:
|
||||
if field.lower.date() + timedelta(days=daycount) > field.upper.date():
|
||||
break
|
||||
daycount += 1
|
||||
|
||||
# loop through the required number of days, append to list as we go
|
||||
days = []
|
||||
for i in range(0, daycount):
|
||||
if i == 0:
|
||||
# on the first day use actual start time instead of midnight
|
||||
# on the first day use actual start time instead of midnight (local time)
|
||||
days.append(
|
||||
DateTimeTZRange(
|
||||
field.lower,
|
||||
(field.lower + timedelta(days=i + 1)).replace(hour=0),
|
||||
timezone.localtime(field.lower),
|
||||
timezone.localtime(
|
||||
(field.lower + timedelta(days=i + 1))
|
||||
).replace(hour=0),
|
||||
)
|
||||
)
|
||||
elif i == daycount - 1:
|
||||
# on the last day use actual end time instead of midnight
|
||||
# on the last day use actual end time instead of midnight (local time)
|
||||
days.append(
|
||||
DateTimeTZRange(
|
||||
(field.lower + timedelta(days=i)).replace(hour=0),
|
||||
field.lower + timedelta(days=i + 1),
|
||||
timezone.localtime((field.lower + timedelta(days=i))).replace(
|
||||
hour=0
|
||||
),
|
||||
timezone.localtime(field.lower + timedelta(days=i)),
|
||||
)
|
||||
)
|
||||
else:
|
||||
# neither first nor last day, goes from midnight to midnight
|
||||
# neither first nor last day, goes from midnight to midnight (local time)
|
||||
days.append(
|
||||
DateTimeTZRange(
|
||||
(field.lower + timedelta(days=i)).replace(hour=0),
|
||||
(field.lower + timedelta(days=i + 1)).replace(hour=0),
|
||||
timezone.localtime((field.lower + timedelta(days=i))).replace(
|
||||
hour=0
|
||||
),
|
||||
timezone.localtime(
|
||||
(field.lower + timedelta(days=i + 1))
|
||||
).replace(hour=0),
|
||||
)
|
||||
)
|
||||
return days
|
||||
|
@ -250,3 +252,33 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
|||
Returns a list of DateTimeTZRanges representing the days during the buildup.
|
||||
"""
|
||||
return self.get_days("teardown")
|
||||
|
||||
# convenience properties to access Camp-related stuff easily from the Camp object
|
||||
|
||||
@property
|
||||
def event_types(self):
|
||||
""" Return all event types with at least one event in this camp """
|
||||
EventType = apps.get_model("program", "EventType")
|
||||
return EventType.objects.filter(
|
||||
events__isnull=False, event__track__camp=self
|
||||
).distinct()
|
||||
|
||||
@property
|
||||
def event_proposals(self):
|
||||
EventProposal = apps.get_model("program", "EventProposal")
|
||||
return EventProposal.objects.filter(track__camp=self)
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
Event = apps.get_model("program", "Event")
|
||||
return Event.objects.filter(track__camp=self)
|
||||
|
||||
@property
|
||||
def event_sessions(self):
|
||||
EventSession = apps.get_model("program", "EventSession")
|
||||
return EventSession.objects.filter(camp=self)
|
||||
|
||||
@property
|
||||
def event_slots(self):
|
||||
EventSlot = apps.get_model("program", "EventSlot")
|
||||
return EventSlot.objects.filter(event_session__in=self.event_sessions.all())
|
||||
|
|
|
@ -3,9 +3,8 @@ import os
|
|||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
|
||||
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
|
||||
from utils.slugs import unique_slugify
|
||||
|
||||
from .email import (
|
||||
send_accountingsystem_expense_email,
|
||||
|
@ -62,8 +61,13 @@ class Chain(CreatedUpdatedModel, UUIDModel):
|
|||
|
||||
def save(self, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Chain, self).save(**kwargs)
|
||||
self.slug = unique_slugify(
|
||||
self.name,
|
||||
slugs_in_use=self.__class__.objects.all().values_list(
|
||||
"slug", flat=True
|
||||
),
|
||||
)
|
||||
super().save(**kwargs)
|
||||
|
||||
@property
|
||||
def expenses(self):
|
||||
|
@ -133,8 +137,13 @@ class Credebtor(CreatedUpdatedModel, UUIDModel):
|
|||
Generate slug as needed
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Credebtor, self).save(**kwargs)
|
||||
self.slug = unique_slugify(
|
||||
self.name,
|
||||
slugs_in_use=self.__class__.objects.filter(
|
||||
chain=self.chain
|
||||
).values_list("slug", flat=True),
|
||||
)
|
||||
super().save(**kwargs)
|
||||
|
||||
|
||||
class Revenue(CampRelatedModel, UUIDModel):
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
Expenses | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Expenses</h3>
|
||||
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
Expenses | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Reimbursements</h3>
|
||||
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
Revenues | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Revenues</h3>
|
||||
|
||||
|
|
|
@ -4,12 +4,11 @@ import logging
|
|||
|
||||
import qrcode
|
||||
from django.contrib.gis.db.models import PointField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.text import slugify
|
||||
from maps.utils import LeafletMarkerChoices
|
||||
from utils.models import CampRelatedModel, UUIDModel
|
||||
from utils.slugs import unique_slugify
|
||||
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
@ -42,6 +41,7 @@ class FacilityType(CampRelatedModel):
|
|||
"""
|
||||
|
||||
class Meta:
|
||||
# we need a unique slug for each team due to the url structure in backoffice
|
||||
unique_together = [("slug", "responsible_team")]
|
||||
|
||||
name = models.CharField(max_length=100, help_text="The name of this facility type")
|
||||
|
@ -89,9 +89,12 @@ class FacilityType(CampRelatedModel):
|
|||
|
||||
def save(self, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
if not self.slug:
|
||||
raise ValidationError("Unable to slugify")
|
||||
self.slug = unique_slugify(
|
||||
self.name,
|
||||
slugs_in_use=self.__class__.objects.filter(
|
||||
responsible_team=self.responsible_team
|
||||
).values_list("slug", flat=True),
|
||||
)
|
||||
super().save(**kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from utils.models import CreatedUpdatedModel
|
||||
from utils.slugs import unique_slugify
|
||||
|
||||
|
||||
class NewsItem(CreatedUpdatedModel):
|
||||
|
@ -29,20 +29,13 @@ class NewsItem(CreatedUpdatedModel):
|
|||
published_at_string = self.published_at.strftime("%Y-%m-%d")
|
||||
base_slug = slugify(self.title)
|
||||
slug = "{}-{}".format(published_at_string, base_slug)
|
||||
incrementer = 1
|
||||
|
||||
# We have to make sure that the slug won't clash with current slugs
|
||||
while NewsItem.objects.filter(slug=slug).exists():
|
||||
if incrementer == 1:
|
||||
slug = "{}-1".format(slug)
|
||||
else:
|
||||
slug = "{}-{}".format(
|
||||
"-".join(slug.split("-")[:-1]), incrementer
|
||||
self.slug = unique_slugify(
|
||||
slug,
|
||||
slugs_in_use=self.__class__.objects.all().values_list(
|
||||
"slug", flat=True
|
||||
),
|
||||
)
|
||||
incrementer += 1
|
||||
self.slug = slug
|
||||
|
||||
super(NewsItem, self).save(**kwargs)
|
||||
super().save(**kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("news:detail", kwargs={"slug": self.slug})
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
Phonebook | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>{{ camp.title }} Phonebook</h2>
|
||||
|
@ -26,7 +21,7 @@ This is a list of all the registered DECT numbers in our phonebook for {{ camp.t
|
|||
{% endif %}
|
||||
|
||||
{% if dectregistration_list %}
|
||||
<table class="table table-hover table-striped">
|
||||
<table class="table table-hover table-striped datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Number</th>
|
||||
|
|
|
@ -7,27 +7,43 @@ from .models import (
|
|||
EventInstance,
|
||||
EventLocation,
|
||||
EventProposal,
|
||||
EventSession,
|
||||
EventSlot,
|
||||
EventTrack,
|
||||
EventType,
|
||||
Favorite,
|
||||
Speaker,
|
||||
SpeakerAvailability,
|
||||
SpeakerProposal,
|
||||
SpeakerProposalAvailability,
|
||||
Url,
|
||||
UrlType,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(SpeakerProposalAvailability)
|
||||
class SpeakerProposalAvailabilityAdmin(admin.ModelAdmin):
|
||||
list_display = ["speaker_proposal", "available", "when"]
|
||||
list_filter = ["speaker_proposal__camp", "available", "speaker_proposal"]
|
||||
|
||||
|
||||
@admin.register(SpeakerAvailability)
|
||||
class SpeakerAvailabilityAdmin(admin.ModelAdmin):
|
||||
list_display = ["speaker", "available", "when"]
|
||||
list_filter = ["speaker__camp", "available", "speaker"]
|
||||
readonly_fields = ["speaker"]
|
||||
|
||||
|
||||
@admin.register(SpeakerProposal)
|
||||
class SpeakerProposalAdmin(admin.ModelAdmin):
|
||||
def mark_speakerproposal_as_approved(self, request, queryset):
|
||||
def mark_speaker_proposal_as_approved(self, request, queryset):
|
||||
for sp in queryset:
|
||||
sp.mark_as_approved(request)
|
||||
|
||||
mark_speakerproposal_as_approved.description = (
|
||||
mark_speaker_proposal_as_approved.description = (
|
||||
"Approve and create Speaker object(s)"
|
||||
)
|
||||
|
||||
actions = ["mark_speakerproposal_as_approved"]
|
||||
actions = ["mark_speaker_proposal_as_approved"]
|
||||
list_filter = ("camp", "proposal_status", "user")
|
||||
|
||||
|
||||
|
@ -40,7 +56,7 @@ get_speakers_string.short_description = "Speakers"
|
|||
|
||||
@admin.register(EventProposal)
|
||||
class EventProposalAdmin(admin.ModelAdmin):
|
||||
def mark_eventproposal_as_approved(self, request, queryset):
|
||||
def mark_event_proposal_as_approved(self, request, queryset):
|
||||
for ep in queryset:
|
||||
if not ep.speakers.all():
|
||||
messages.error(
|
||||
|
@ -54,12 +70,12 @@ class EventProposalAdmin(admin.ModelAdmin):
|
|||
messages.error(request, e)
|
||||
return False
|
||||
|
||||
mark_eventproposal_as_approved.description = "Approve and create Event object(s)"
|
||||
mark_event_proposal_as_approved.description = "Approve and create Event object(s)"
|
||||
|
||||
def get_speakers(self):
|
||||
return
|
||||
|
||||
actions = ["mark_eventproposal_as_approved"]
|
||||
actions = ["mark_event_proposal_as_approved"]
|
||||
list_filter = ("event_type", "proposal_status", "track", "user")
|
||||
list_display = ["title", get_speakers_string, "event_type", "proposal_status"]
|
||||
|
||||
|
@ -67,7 +83,7 @@ class EventProposalAdmin(admin.ModelAdmin):
|
|||
@admin.register(EventLocation)
|
||||
class EventLocationAdmin(admin.ModelAdmin):
|
||||
list_filter = ("camp",)
|
||||
list_display = ("name", "camp")
|
||||
list_display = ("name", "camp", "capacity")
|
||||
|
||||
|
||||
@admin.register(EventTrack)
|
||||
|
@ -76,10 +92,23 @@ class EventTrackAdmin(admin.ModelAdmin):
|
|||
list_display = ("name", "camp")
|
||||
|
||||
|
||||
@admin.register(EventSession)
|
||||
class EventSessionAdmin(admin.ModelAdmin):
|
||||
list_display = ("camp", "event_type", "event_location", "when")
|
||||
list_filter = ("camp", "event_type", "event_location")
|
||||
search_fields = ["event__type", "event__location"]
|
||||
|
||||
|
||||
@admin.register(EventSlot)
|
||||
class EventSlotAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "event_session", "when", "event")
|
||||
list_filter = ("event_session__camp", "event_session__event_type", "event_session")
|
||||
|
||||
|
||||
@admin.register(EventInstance)
|
||||
class EventInstanceAdmin(admin.ModelAdmin):
|
||||
list_display = ("event", "when", "location")
|
||||
list_filter = ("event__track__camp", "event")
|
||||
list_display = ("event", "when", "location", "autoscheduled")
|
||||
list_filter = ("event__track__camp", "event", "autoscheduled")
|
||||
search_fields = ["event__title"]
|
||||
|
||||
|
||||
|
@ -91,12 +120,7 @@ class EventTypeAdmin(admin.ModelAdmin):
|
|||
@admin.register(Speaker)
|
||||
class SpeakerAdmin(admin.ModelAdmin):
|
||||
list_filter = ("camp",)
|
||||
readonly_fields = ["proposal"]
|
||||
|
||||
|
||||
@admin.register(Favorite)
|
||||
class FavoriteAdmin(admin.ModelAdmin):
|
||||
raw_id_fields = ("event_instance",)
|
||||
readonly_fields = ["proposal", "camp"]
|
||||
|
||||
|
||||
class SpeakerInline(admin.StackedInline):
|
||||
|
@ -105,8 +129,8 @@ class SpeakerInline(admin.StackedInline):
|
|||
|
||||
@admin.register(Event)
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
list_filter = ("track", "speakers")
|
||||
list_display = ["title", "event_type"]
|
||||
list_display = ["title", "event_type", "duration_minutes", "demand"]
|
||||
list_filter = ("track", "event_type", "speakers")
|
||||
|
||||
inlines = [SpeakerInline]
|
||||
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
from django.apps import AppConfig
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||
from django.db.models.signals import m2m_changed, post_save
|
||||
|
||||
|
||||
class ProgramConfig(AppConfig):
|
||||
name = "program"
|
||||
|
||||
def ready(self):
|
||||
from .models import Speaker, EventInstance
|
||||
from .models import Speaker, EventSession
|
||||
from .signal_handlers import (
|
||||
check_speaker_event_camp_consistency,
|
||||
check_speaker_camp_change,
|
||||
eventinstance_pre_save,
|
||||
eventinstance_post_save,
|
||||
event_session_post_save,
|
||||
)
|
||||
|
||||
m2m_changed.connect(
|
||||
check_speaker_event_camp_consistency, sender=Speaker.events.through
|
||||
)
|
||||
|
||||
pre_save.connect(check_speaker_camp_change, sender=Speaker)
|
||||
pre_save.connect(eventinstance_pre_save, sender=EventInstance)
|
||||
|
||||
post_save.connect(eventinstance_post_save, sender=EventInstance)
|
||||
post_save.connect(event_session_post_save, sender=EventSession)
|
||||
|
|
390
src/program/autoscheduler.py
Normal file
390
src/program/autoscheduler.py
Normal file
|
@ -0,0 +1,390 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from conference_scheduler import resources, scheduler
|
||||
from conference_scheduler.lp_problem import objective_functions
|
||||
from conference_scheduler.validator import is_valid_schedule, schedule_violations
|
||||
from psycopg2.extras import DateTimeTZRange
|
||||
|
||||
from .models import EventType
|
||||
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
class AutoScheduler:
|
||||
"""
|
||||
The BornHack AutoScheduler. Made with love by Tykling.
|
||||
|
||||
Built around https://github.com/PyconUK/ConferenceScheduler which works with lists
|
||||
of conference_scheduler.resources.Slot and conference_scheduler.resources.Event objects.
|
||||
|
||||
Most of the code in this class deals with massaging our data into a list of Slot and
|
||||
Event objects defining the data and constraints for the scheduler.
|
||||
|
||||
Initialising this class takes a while because all the objects have to be created.
|
||||
"""
|
||||
|
||||
def __init__(self, camp):
|
||||
""" Get EventTypes, EventSessions and Events, build autoslot and autoevent objects """
|
||||
self.camp = camp
|
||||
|
||||
# Get all EventTypes which support autoscheduling
|
||||
self.event_types = self.get_event_types()
|
||||
|
||||
# Get all EventSessions for the current event_types
|
||||
self.event_sessions = self.get_event_sessions(self.event_types)
|
||||
|
||||
# Build a lookup dict of lists of EventSession IDs per EventType (for easy lookups later)
|
||||
self.event_type_sessions = {}
|
||||
for session in self.event_sessions:
|
||||
if session.event_type not in self.event_type_sessions:
|
||||
self.event_type_sessions[session.event_type] = []
|
||||
self.event_type_sessions[session.event_type].append(session.id)
|
||||
|
||||
# Get all Events for the current event_types
|
||||
self.events = self.get_events(self.event_types)
|
||||
|
||||
# Get autoslots
|
||||
self.autoslots = self.get_autoslots(self.event_sessions)
|
||||
|
||||
# Build a lookup dict of autoslots per EventType
|
||||
self.event_type_slots = {}
|
||||
for autoslot in self.autoslots:
|
||||
# loop over event_type_sessions dict and find our
|
||||
for et, sessions in self.event_type_sessions.items():
|
||||
if autoslot.session in sessions:
|
||||
if et not in self.event_type_slots:
|
||||
self.event_type_slots[et] = []
|
||||
self.event_type_slots[et].append(autoslot)
|
||||
break
|
||||
|
||||
# get autoevents and a lookup dict which maps Event id to autoevent index
|
||||
self.autoevents, self.autoeventindex = self.get_autoevents(self.events)
|
||||
|
||||
def get_event_types(self):
|
||||
""" Return all EventTypes which support autoscheduling """
|
||||
return EventType.objects.filter(support_autoscheduling=True)
|
||||
|
||||
def get_event_sessions(self, event_types):
|
||||
""" Return all EventSessions for these EventTypes """
|
||||
return self.camp.event_sessions.filter(
|
||||
event_type__in=event_types,
|
||||
).prefetch_related("event_type", "event_location")
|
||||
|
||||
def get_events(self, event_types):
|
||||
""" Return all Events that need scheduling """
|
||||
# return all events for these event_types, but..
|
||||
return self.camp.events.filter(event_type__in=event_types).exclude(
|
||||
# exclude Events that have been sceduled already...
|
||||
event_slots__isnull=False,
|
||||
# ...unless those events are autoscheduled
|
||||
event_slots__autoscheduled=False,
|
||||
)
|
||||
|
||||
def get_autoslots(self, event_sessions):
|
||||
""" Return a list of autoslots for all slots in all EventSessions """
|
||||
autoslots = []
|
||||
# loop over the sessions
|
||||
for session in event_sessions:
|
||||
# loop over available slots in this session
|
||||
for slot in session.get_available_slots(count_autoscheduled_as_free=True):
|
||||
autoslots.append(slot.get_autoscheduler_slot())
|
||||
return autoslots
|
||||
|
||||
def get_autoevents(self, events):
|
||||
""" Return a list of resources.Event objects, one for each Event """
|
||||
autoevents = []
|
||||
autoeventindex = {}
|
||||
eventindex = {}
|
||||
for event in events:
|
||||
autoevents.append(
|
||||
resources.Event(
|
||||
name=event.id,
|
||||
duration=event.duration_minutes,
|
||||
tags=event.tags.names(),
|
||||
demand=event.demand,
|
||||
)
|
||||
)
|
||||
# create a dict of events with the autoevent index as key and the Event as value
|
||||
autoeventindex[autoevents.index(autoevents[-1])] = event
|
||||
# create a dict of events with the Event as key and the autoevent index as value
|
||||
eventindex[event] = autoevents.index(autoevents[-1])
|
||||
|
||||
# loop over all autoevents to add unavailability...
|
||||
# (we have to do this in a seperate loop because we need all the autoevents to exist)
|
||||
for autoevent in autoevents:
|
||||
# get the Event
|
||||
event = autoeventindex[autoevents.index(autoevent)]
|
||||
# loop over all other event_types...
|
||||
for et in self.event_types.all().exclude(pk=event.event_type.pk):
|
||||
if et in self.event_type_slots:
|
||||
# and add all slots for this EventType as unavailable for this event,
|
||||
# this means we don't schedule a talk in a workshop slot and vice versa.
|
||||
autoevent.add_unavailability(*self.event_type_slots[et])
|
||||
|
||||
# loop over all speakers for this event and add event conflicts
|
||||
for speaker in event.speakers.all():
|
||||
# loop over other events featuring this speaker, register each conflict,
|
||||
# this means we dont schedule two events for the same speaker at the same time
|
||||
conflict_ids = speaker.events.exclude(id=event.id).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
for conflictevent in autoevents:
|
||||
if conflictevent.name in conflict_ids:
|
||||
# only the event with the lowest index gets the unavailability,
|
||||
if autoevents.index(conflictevent) > autoevents.index(
|
||||
autoevent
|
||||
):
|
||||
autoevent.add_unavailability(conflictevent)
|
||||
|
||||
# loop over event_conflicts for this speaker, register unavailability for each,
|
||||
# this means we dont schedule this event at the same time as something the
|
||||
# speaker wishes to attend.
|
||||
# Only process Events which the AutoScheduler is handling
|
||||
for conflictevent in speaker.event_conflicts.filter(
|
||||
pk__in=events.values_list("pk", flat=True)
|
||||
):
|
||||
# only the event with the lowest index gets the unavailability
|
||||
if eventindex[conflictevent] > autoevents.index(autoevent):
|
||||
autoevent.add_unavailability(
|
||||
autoevents[eventindex[conflictevent]]
|
||||
)
|
||||
|
||||
# loop over event_conflicts for this speaker, register unavailability for each,
|
||||
# only process Events which the AutoScheduler is not handling, and which have
|
||||
# been scheduled in one or more EventSlots
|
||||
for conflictevent in speaker.event_conflicts.filter(
|
||||
event_slots__isnull=False
|
||||
).exclude(pk__in=events.values_list("pk", flat=True)):
|
||||
# loop over the EventSlots this conflict is scheduled in
|
||||
for conflictslot in conflictevent.event_slots.all():
|
||||
# loop over all slots
|
||||
for slot in self.autoslots:
|
||||
# check if this slot overlaps with the conflictevents slot
|
||||
if conflictslot.when & DateTimeTZRange(
|
||||
slot.starts_at,
|
||||
slot.starts_at + timedelta(minutes=slot.duration),
|
||||
):
|
||||
# this slot overlaps with the conflicting event
|
||||
autoevent.add_unavailability(slot)
|
||||
|
||||
# Register all slots where we have no positive availability
|
||||
# for this speaker as unavailable
|
||||
available = []
|
||||
for availability in speaker.availabilities.filter(
|
||||
available=True
|
||||
).values_list("when", flat=True):
|
||||
availability = DateTimeTZRange(
|
||||
availability.lower, availability.upper, "()"
|
||||
)
|
||||
for slot in self.autoslots:
|
||||
slotrange = DateTimeTZRange(
|
||||
slot.starts_at,
|
||||
slot.starts_at + timedelta(minutes=slot.duration),
|
||||
"()",
|
||||
)
|
||||
if slotrange in availability:
|
||||
# the speaker is available for this slot
|
||||
available.append(self.autoslots.index(slot))
|
||||
autoevent.add_unavailability(
|
||||
*[
|
||||
s
|
||||
for s in self.autoslots
|
||||
if not self.autoslots.index(s) in available
|
||||
]
|
||||
)
|
||||
|
||||
return autoevents, autoeventindex
|
||||
|
||||
def build_current_autoschedule(self):
|
||||
""" Build an autoschedule object based on the existing published schedule.
|
||||
Returns an autoschedule, which is a list of conference_scheduler.resources.ScheduledItem
|
||||
objects, one for each scheduled Event. This function is useful for creating an "original
|
||||
schedule" to base a new similar schedule off of. """
|
||||
|
||||
# loop over scheduled events and create a ScheduledItem object for each
|
||||
autoschedule = []
|
||||
for slot in self.camp.event_slots.filter(
|
||||
autoscheduled=True, event__in=self.events
|
||||
):
|
||||
# loop over all autoevents to find the index of this event
|
||||
for autoevent in self.autoevents:
|
||||
if autoevent.name == slot.event.id:
|
||||
# we need the index number of the event
|
||||
eventindex = self.autoevents.index(autoevent)
|
||||
break
|
||||
|
||||
# loop over the autoslots to find the index of the autoslot this event is scheduled in
|
||||
scheduled = False
|
||||
for autoslot in self.autoslots:
|
||||
if (
|
||||
autoslot.venue == slot.event_location.id
|
||||
and autoslot.starts_at == slot.when.lower
|
||||
and autoslot.session
|
||||
in self.event_type_sessions[slot.event.event_type]
|
||||
):
|
||||
# This autoslot starts at the same time as the EventSlot, and at the same
|
||||
# location. It also has the session ID of a session with the right EventType.
|
||||
autoschedule.append(
|
||||
resources.ScheduledItem(
|
||||
event=self.autoevents[eventindex],
|
||||
slot=self.autoslots[self.autoslots.index(autoslot)],
|
||||
)
|
||||
)
|
||||
scheduled = True
|
||||
break
|
||||
|
||||
# did we find a slot matching this EventInstance?
|
||||
if not scheduled:
|
||||
print(f"Could not find an autoslot for slot {slot} - skipping")
|
||||
|
||||
# The returned schedule might not be valid! For example if a speaker is no
|
||||
# longer available when their talk is scheduled. This is fine though, an invalid
|
||||
# schedule can still be used as a basis for creating a new similar schedule.
|
||||
return autoschedule
|
||||
|
||||
def calculate_autoschedule(self, original_schedule=None):
|
||||
""" Calculate autoschedule based on self.autoevents and self.autoslots,
|
||||
optionally using original_schedule to minimise changes """
|
||||
kwargs = {}
|
||||
kwargs["events"] = self.autoevents
|
||||
kwargs["slots"] = self.autoslots
|
||||
|
||||
# include another schedule in the calculation?
|
||||
if original_schedule:
|
||||
kwargs["original_schedule"] = original_schedule
|
||||
kwargs["objective_function"] = objective_functions.number_of_changes
|
||||
else:
|
||||
# otherwise use the capacity demand difference thing
|
||||
kwargs[
|
||||
"objective_function"
|
||||
] = objective_functions.efficiency_capacity_demand_difference
|
||||
# calculate the new schedule
|
||||
autoschedule = scheduler.schedule(**kwargs)
|
||||
return autoschedule
|
||||
|
||||
def calculate_similar_autoschedule(self, original_schedule=None):
|
||||
""" Convenience method for creating similar schedules. If original_schedule
|
||||
is omitted the new schedule is based on the current schedule instead """
|
||||
|
||||
if not original_schedule:
|
||||
# we do not have an original_schedule, use current EventInstances
|
||||
original_schedule = self.build_current_autoschedule()
|
||||
|
||||
# calculate and return
|
||||
autoschedule = self.calculate_autoschedule(original_schedule=original_schedule)
|
||||
diff = self.diff(original_schedule, autoschedule)
|
||||
return autoschedule, diff
|
||||
|
||||
def apply(self, autoschedule):
|
||||
""" Apply an autoschedule by creating EventInstance objects to match it """
|
||||
|
||||
# "The Clean Slate protocol sir?" - delete any existing autoscheduled Events
|
||||
# TODO: investigate how this affects the FRAB XML export (for which we added a UUID on
|
||||
# EventInstance objects). Make sure "favourite" functionality or bookmarks or w/e in
|
||||
# FRAB clients still work after a schedule "re"apply. We might need a smaller hammer here.
|
||||
deleted = self.camp.event_slots.filter(
|
||||
# get all autoscheduled EventSlots
|
||||
autoscheduled=True
|
||||
).update(
|
||||
# clear the Event
|
||||
event=None,
|
||||
# and autoscheduled status
|
||||
autoscheduled=None,
|
||||
)
|
||||
|
||||
# loop and schedule events
|
||||
scheduled = 0
|
||||
for item in autoschedule:
|
||||
# each item is an instance of conference_scheduler.resources.ScheduledItem
|
||||
event = self.camp.events.get(id=item.event.name)
|
||||
slot = self.camp.event_slots.get(
|
||||
event_session_id=item.slot.session,
|
||||
when=DateTimeTZRange(
|
||||
item.slot.starts_at,
|
||||
item.slot.starts_at + timedelta(minutes=item.slot.duration),
|
||||
"[)", # remember to use the correct bounds when comparing
|
||||
),
|
||||
)
|
||||
slot.event = event
|
||||
slot.autoscheduled = True
|
||||
slot.save()
|
||||
|
||||
scheduled += 1
|
||||
|
||||
# return the numbers
|
||||
return deleted, scheduled
|
||||
|
||||
def diff(self, original_schedule, new_schedule):
|
||||
"""
|
||||
This method returns a dict of Event differences and Slot differences between
|
||||
the two schedules.
|
||||
"""
|
||||
slot_diff = scheduler.slot_schedule_difference(original_schedule, new_schedule,)
|
||||
|
||||
slot_output = []
|
||||
for item in slot_diff:
|
||||
slot_output.append(
|
||||
{
|
||||
"event_location": self.camp.event_locations.get(pk=item.slot.venue),
|
||||
"starttime": item.slot.starts_at,
|
||||
"old": {},
|
||||
"new": {},
|
||||
}
|
||||
)
|
||||
if item.old_event:
|
||||
try:
|
||||
old_event = self.camp.events.get(pk=item.old_event.name)
|
||||
except self.camp.events.DoesNotExist:
|
||||
old_event = item.old_event.name
|
||||
slot_output[-1]["old"]["event"] = old_event
|
||||
if item.new_event:
|
||||
try:
|
||||
new_event = self.camp.events.get(pk=item.new_event.name)
|
||||
except self.camp.events.DoesNotExist:
|
||||
new_event = item.old_event.name
|
||||
slot_output[-1]["new"]["event"] = new_event
|
||||
|
||||
# then get a list of differences per event
|
||||
event_diff = scheduler.event_schedule_difference(
|
||||
original_schedule, new_schedule,
|
||||
)
|
||||
event_output = []
|
||||
# loop over the differences and build the dict
|
||||
for item in event_diff:
|
||||
try:
|
||||
event = self.camp.events.get(pk=item.event.name)
|
||||
except self.camp.events.DoesNotExist:
|
||||
event = item.event.name
|
||||
event_output.append(
|
||||
{"event": event, "old": {}, "new": {},}
|
||||
)
|
||||
# do we have an old slot for this event?
|
||||
if item.old_slot:
|
||||
event_output[-1]["old"][
|
||||
"event_location"
|
||||
] = self.camp.event_locations.get(id=item.old_slot.venue)
|
||||
event_output[-1]["old"]["starttime"] = item.old_slot.starts_at
|
||||
# do we have a new slot for this event?
|
||||
if item.new_slot:
|
||||
event_output[-1]["new"][
|
||||
"event_location"
|
||||
] = self.camp.event_locations.get(id=item.new_slot.venue)
|
||||
event_output[-1]["new"]["starttime"] = item.new_slot.starts_at
|
||||
|
||||
# all good
|
||||
return {"event_diffs": event_output, "slot_diffs": slot_output}
|
||||
|
||||
def is_valid(self, autoschedule, return_violations=False):
|
||||
""" Check if a schedule is valid, optionally returning a list of violations if invalid """
|
||||
valid = is_valid_schedule(
|
||||
autoschedule, slots=self.autoslots, events=self.autoevents
|
||||
)
|
||||
if not return_violations:
|
||||
return valid
|
||||
return (
|
||||
valid,
|
||||
schedule_violations(
|
||||
autoschedule, slots=self.autoslots, events=self.autoevents
|
||||
),
|
||||
)
|
|
@ -7,123 +7,123 @@ from utils.email import add_outgoing_email
|
|||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
def add_new_speakerproposal_email(speakerproposal):
|
||||
formatdict = {"proposal": speakerproposal}
|
||||
def add_new_speaker_proposal_email(speaker_proposal):
|
||||
formatdict = {"proposal": speaker_proposal}
|
||||
|
||||
try:
|
||||
content_team = Team.objects.get(camp=speakerproposal.camp, name="Content")
|
||||
content_team = Team.objects.get(camp=speaker_proposal.camp, name="Content")
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.info("There is no team with name Content: {}".format(e))
|
||||
return False
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/new_speakerproposal.txt",
|
||||
html_template="emails/new_speakerproposal.html",
|
||||
text_template="emails/new_speaker_proposal.txt",
|
||||
html_template="emails/new_speaker_proposal.html",
|
||||
to_recipients=content_team.mailing_list,
|
||||
formatdict=formatdict,
|
||||
subject="New speaker proposal '%s' was just submitted" % speakerproposal.name,
|
||||
subject="New speaker proposal '%s' was just submitted" % speaker_proposal.name,
|
||||
)
|
||||
|
||||
|
||||
def add_new_eventproposal_email(eventproposal):
|
||||
formatdict = {"proposal": eventproposal}
|
||||
def add_new_event_proposal_email(event_proposal):
|
||||
formatdict = {"proposal": event_proposal}
|
||||
|
||||
try:
|
||||
content_team = Team.objects.get(camp=eventproposal.camp, name="Content")
|
||||
content_team = Team.objects.get(camp=event_proposal.camp, name="Content")
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.info("There is no team with name Content: {}".format(e))
|
||||
return False
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/new_eventproposal.txt",
|
||||
html_template="emails/new_eventproposal.html",
|
||||
text_template="emails/new_event_proposal.txt",
|
||||
html_template="emails/new_event_proposal.html",
|
||||
to_recipients=content_team.mailing_list,
|
||||
formatdict=formatdict,
|
||||
subject="New event proposal '%s' was just submitted" % eventproposal.title,
|
||||
subject="New event proposal '%s' was just submitted" % event_proposal.title,
|
||||
)
|
||||
|
||||
|
||||
def add_speakerproposal_updated_email(speakerproposal):
|
||||
formatdict = {"proposal": speakerproposal}
|
||||
def add_speaker_proposal_updated_email(speaker_proposal):
|
||||
formatdict = {"proposal": speaker_proposal}
|
||||
|
||||
try:
|
||||
content_team = Team.objects.get(camp=speakerproposal.camp, name="Content")
|
||||
content_team = Team.objects.get(camp=speaker_proposal.camp, name="Content")
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.info("There is no team with name Content: {}".format(e))
|
||||
return False
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/update_speakerproposal.txt",
|
||||
html_template="emails/update_speakerproposal.html",
|
||||
text_template="emails/update_speaker_proposal.txt",
|
||||
html_template="emails/update_speaker_proposal.html",
|
||||
to_recipients=content_team.mailing_list,
|
||||
formatdict=formatdict,
|
||||
subject="Speaker proposal '%s' was just updated" % speakerproposal.name,
|
||||
subject="Speaker proposal '%s' was just updated" % speaker_proposal.name,
|
||||
)
|
||||
|
||||
|
||||
def add_eventproposal_updated_email(eventproposal):
|
||||
formatdict = {"proposal": eventproposal}
|
||||
def add_event_proposal_updated_email(event_proposal):
|
||||
formatdict = {"proposal": event_proposal}
|
||||
|
||||
try:
|
||||
content_team = Team.objects.get(camp=eventproposal.camp, name="Content")
|
||||
content_team = Team.objects.get(camp=event_proposal.camp, name="Content")
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.info("There is no team with name Content: {}".format(e))
|
||||
return False
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/update_eventproposal.txt",
|
||||
html_template="emails/update_eventproposal.html",
|
||||
text_template="emails/update_event_proposal.txt",
|
||||
html_template="emails/update_event_proposal.html",
|
||||
to_recipients=content_team.mailing_list,
|
||||
formatdict=formatdict,
|
||||
subject="Event proposal '%s' was just updated" % eventproposal.title,
|
||||
subject="Event proposal '%s' was just updated" % event_proposal.title,
|
||||
)
|
||||
|
||||
|
||||
def add_speakerproposal_rejected_email(speakerproposal):
|
||||
formatdict = {"proposal": speakerproposal}
|
||||
def add_speaker_proposal_rejected_email(speaker_proposal):
|
||||
formatdict = {"proposal": speaker_proposal}
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/speakerproposal_rejected.txt",
|
||||
html_template="emails/speakerproposal_rejected.html",
|
||||
to_recipients=speakerproposal.user.email,
|
||||
text_template="emails/speaker_proposal_rejected.txt",
|
||||
html_template="emails/speaker_proposal_rejected.html",
|
||||
to_recipients=speaker_proposal.user.email,
|
||||
formatdict=formatdict,
|
||||
subject=f"Your {speakerproposal.camp.title} speaker proposal '{speakerproposal.name}' was rejected",
|
||||
subject=f"Your {speaker_proposal.camp.title} speaker proposal '{speaker_proposal.name}' was rejected",
|
||||
)
|
||||
|
||||
|
||||
def add_speakerproposal_accepted_email(speakerproposal):
|
||||
formatdict = {"proposal": speakerproposal}
|
||||
def add_speaker_proposal_accepted_email(speaker_proposal):
|
||||
formatdict = {"proposal": speaker_proposal}
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/speakerproposal_accepted.txt",
|
||||
html_template="emails/speakerproposal_accepted.html",
|
||||
to_recipients=speakerproposal.user.email,
|
||||
text_template="emails/speaker_proposal_accepted.txt",
|
||||
html_template="emails/speaker_proposal_accepted.html",
|
||||
to_recipients=speaker_proposal.user.email,
|
||||
formatdict=formatdict,
|
||||
subject=f"Your {speakerproposal.camp.title} speaker proposal '{speakerproposal.name}' was accepted",
|
||||
subject=f"Your {speaker_proposal.camp.title} speaker proposal '{speaker_proposal.name}' was accepted",
|
||||
)
|
||||
|
||||
|
||||
def add_eventproposal_rejected_email(eventproposal):
|
||||
formatdict = {"proposal": eventproposal}
|
||||
def add_event_proposal_rejected_email(event_proposal):
|
||||
formatdict = {"proposal": event_proposal}
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/eventproposal_rejected.txt",
|
||||
html_template="emails/eventproposal_rejected.html",
|
||||
to_recipients=eventproposal.user.email,
|
||||
text_template="emails/event_proposal_rejected.txt",
|
||||
html_template="emails/event_proposal_rejected.html",
|
||||
to_recipients=event_proposal.user.email,
|
||||
formatdict=formatdict,
|
||||
subject=f"Your {eventproposal.camp.title} event proposal '{eventproposal.title}' was rejected",
|
||||
subject=f"Your {event_proposal.camp.title} event proposal '{event_proposal.title}' was rejected",
|
||||
)
|
||||
|
||||
|
||||
def add_eventproposal_accepted_email(eventproposal):
|
||||
formatdict = {"proposal": eventproposal}
|
||||
def add_event_proposal_accepted_email(event_proposal):
|
||||
formatdict = {"proposal": event_proposal}
|
||||
|
||||
return add_outgoing_email(
|
||||
text_template="emails/eventproposal_accepted.txt",
|
||||
html_template="emails/eventproposal_accepted.html",
|
||||
to_recipients=eventproposal.user.email,
|
||||
text_template="emails/event_proposal_accepted.txt",
|
||||
html_template="emails/event_proposal_accepted.html",
|
||||
to_recipients=event_proposal.user.email,
|
||||
formatdict=formatdict,
|
||||
subject=f"Your {eventproposal.camp.title} event proposal '{eventproposal.title}' was accepted!",
|
||||
subject=f"Your {event_proposal.camp.title} event proposal '{event_proposal.title}' was accepted!",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -3,14 +3,15 @@ import logging
|
|||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .models import EventProposal, EventTrack, SpeakerProposal, Url, UrlType
|
||||
from .models import Event, EventProposal, EventTrack, SpeakerProposal
|
||||
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
class SpeakerProposalForm(forms.ModelForm):
|
||||
"""
|
||||
The SpeakerProposalForm. Takes an EventType in __init__ and changes fields accordingly.
|
||||
The SpeakerProposalForm. Takes a list of EventTypes in __init__,
|
||||
and changes fields accordingly if the list has 1 element.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
@ -21,17 +22,41 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
"biography",
|
||||
"needs_oneday_ticket",
|
||||
"submission_notes",
|
||||
"event_conflicts",
|
||||
]
|
||||
|
||||
def __init__(self, camp, eventtype=None, *args, **kwargs):
|
||||
# initialise the form
|
||||
def __init__(self, camp, event_type=None, matrix={}, *args, **kwargs):
|
||||
"""
|
||||
initialise the form and adapt based on event_type
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# only show events from this camp
|
||||
self.fields["event_conflicts"].queryset = Event.objects.filter(
|
||||
track__camp=camp, event_type__support_speaker_event_conflicts=True,
|
||||
)
|
||||
|
||||
if matrix:
|
||||
# add speaker availability fields
|
||||
for date in matrix.keys():
|
||||
# do we need a column for this day?
|
||||
if matrix[date]:
|
||||
# loop over the daychunks for this day
|
||||
for daychunk in matrix[date]:
|
||||
if matrix[date][daychunk]:
|
||||
# add the field
|
||||
self.fields[
|
||||
matrix[date][daychunk]["fieldname"]
|
||||
] = forms.BooleanField(required=False)
|
||||
# add it to Meta.fields too
|
||||
self.Meta.fields.append(matrix[date][daychunk]["fieldname"])
|
||||
|
||||
# adapt form based on EventType?
|
||||
if not eventtype:
|
||||
if not event_type:
|
||||
# we have no event_type to customize the form, use the default form
|
||||
return
|
||||
|
||||
if eventtype.name == "Debate":
|
||||
if event_type.name == "Debate":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Guest Name"
|
||||
self.fields[
|
||||
|
@ -54,10 +79,10 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
"submission_notes"
|
||||
].help_text = "Private notes regarding this guest. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
# no free tickets for workshops
|
||||
# no free tickets for debates
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Lightning Talk":
|
||||
elif event_type.name == "Lightning Talk":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Speaker Name"
|
||||
self.fields[
|
||||
|
@ -83,7 +108,7 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
# no free tickets for lightning talks
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Music Act":
|
||||
elif event_type.name == "Music Act":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Artist Name"
|
||||
self.fields[
|
||||
|
@ -109,33 +134,7 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
# no oneday tickets for music acts
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Recreational Event":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Host Name"
|
||||
self.fields[
|
||||
"name"
|
||||
].help_text = "The name of the event host. Can be a real name or an alias."
|
||||
|
||||
# fix label and help_text for the email field
|
||||
self.fields["email"].label = "Host Email"
|
||||
self.fields[
|
||||
"email"
|
||||
].help_text = "The email for the host. Will default to the logged-in users email if left empty."
|
||||
|
||||
# fix label and help_text for the biograpy field
|
||||
self.fields["biography"].label = "Host Biography"
|
||||
self.fields["biography"].help_text = "The biography of the host."
|
||||
|
||||
# fix label and help_text for the submission_notes field
|
||||
self.fields["submission_notes"].label = "Host Notes"
|
||||
self.fields[
|
||||
"submission_notes"
|
||||
].help_text = "Private notes regarding this host. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
# no oneday tickets for music acts
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Talk":
|
||||
elif event_type.name == "Talk" or event_type.name == "Keynote":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Speaker Name"
|
||||
self.fields[
|
||||
|
@ -158,7 +157,7 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
"submission_notes"
|
||||
].help_text = "Private notes regarding this speaker. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
elif eventtype.name == "Workshop":
|
||||
elif event_type.name == "Workshop":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Host Name"
|
||||
self.fields[
|
||||
|
@ -186,7 +185,7 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
# no free tickets for workshops
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Slacking Off":
|
||||
elif event_type.name == "Recreational":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Host Name"
|
||||
self.fields["name"].help_text = "Can be a real name or an alias."
|
||||
|
@ -207,10 +206,10 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
"submission_notes"
|
||||
].help_text = "Private notes regarding this host. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
# no free tickets for workshops
|
||||
# no free tickets for recreational events
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
elif eventtype.name == "Meetup":
|
||||
elif event_type.name == "Meetup":
|
||||
# fix label and help_text for the name field
|
||||
self.fields["name"].label = "Host Name"
|
||||
self.fields[
|
||||
|
@ -233,12 +232,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
|||
"submission_notes"
|
||||
].help_text = "Private notes regarding this host. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
# no free tickets for workshops
|
||||
# no free tickets for meetups
|
||||
del self.fields["needs_oneday_ticket"]
|
||||
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"Unsupported event type, don't know which form class to use"
|
||||
f"Unsupported event type '{event_type.name}', don't know which form class to use"
|
||||
)
|
||||
|
||||
|
||||
|
@ -258,6 +257,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
"abstract",
|
||||
"allow_video_recording",
|
||||
"duration",
|
||||
"tags",
|
||||
"slides_url",
|
||||
"submission_notes",
|
||||
"track",
|
||||
|
@ -265,52 +265,35 @@ class EventProposalForm(forms.ModelForm):
|
|||
]
|
||||
|
||||
def clean_duration(self):
|
||||
duration = self.cleaned_data["duration"]
|
||||
if not duration or duration < 60 or duration > 180:
|
||||
""" Make sure duration has been specified, and make sure it is not too long """
|
||||
if not self.cleaned_data["duration"]:
|
||||
raise forms.ValidationError(f"Please specify a duration.")
|
||||
if (
|
||||
self.event_type.event_duration_minutes
|
||||
and self.cleaned_data["duration"] > self.event_type.event_duration_minutes
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
"Please keep duration between 60 and 180 minutes."
|
||||
f"Please keep duration under {self.event_type.event_duration_minutes} minutes."
|
||||
)
|
||||
return duration
|
||||
return self.cleaned_data["duration"]
|
||||
|
||||
def clean_track(self):
|
||||
track = self.cleaned_data["track"]
|
||||
# TODO: make sure the track is part of the current camp, needs camp as form kwarg to verify
|
||||
return track
|
||||
|
||||
def save(self, commit=True, user=None, event_type=None):
|
||||
eventproposal = super().save(commit=False)
|
||||
if user:
|
||||
eventproposal.user = user
|
||||
if event_type:
|
||||
eventproposal.event_type = event_type
|
||||
eventproposal.save()
|
||||
|
||||
if not event_type and hasattr(eventproposal, "event_type"):
|
||||
event_type = eventproposal.event_type
|
||||
|
||||
if self.cleaned_data.get("slides_url") and event_type.name in [
|
||||
"Talk",
|
||||
"Lightning Talk",
|
||||
]:
|
||||
url = self.cleaned_data.get("slides_url")
|
||||
if not eventproposal.urls.filter(url=url).exists():
|
||||
slides_url = Url()
|
||||
slides_url.eventproposal = eventproposal
|
||||
slides_url.url = url
|
||||
slides_url.urltype = UrlType.objects.get(name="Slides")
|
||||
slides_url.save()
|
||||
|
||||
return eventproposal
|
||||
|
||||
def __init__(self, camp, eventtype=None, *args, **kwargs):
|
||||
def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
|
||||
# initialise form
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# we need event_type for cleaning later
|
||||
self.event_type = event_type
|
||||
|
||||
TALK = "Talk"
|
||||
LIGHTNING_TALK = "Lightning Talk"
|
||||
DEBATE = "Debate"
|
||||
MUSIC_ACT = "Music Act"
|
||||
RECREATIONAL_EVENT = "Recreational Event"
|
||||
RECREATIONAL_EVENT = "Recreational"
|
||||
WORKSHOP = "Workshop"
|
||||
SLACKING_OFF = "Slacking Off"
|
||||
MEETUP = "Meetup"
|
||||
|
@ -322,15 +305,34 @@ class EventProposalForm(forms.ModelForm):
|
|||
# make sure video_recording checkbox defaults to checked
|
||||
self.fields["allow_video_recording"].initial = True
|
||||
|
||||
if eventtype.name not in [TALK, LIGHTNING_TALK]:
|
||||
if event_type.name not in [TALK, LIGHTNING_TALK]:
|
||||
# Only talk or lightning talk should show the slides_url field
|
||||
del self.fields["slides_url"]
|
||||
|
||||
if not eventtype.name == LIGHTNING_TALK:
|
||||
# better placeholder text for duration field
|
||||
self.fields["duration"].label = f"{event_type.name} Duration"
|
||||
if event_type.event_duration_minutes:
|
||||
self.fields[
|
||||
"duration"
|
||||
].help_text = f"Please enter the duration of this {event_type.name} (in minutes, max {event_type.event_duration_minutes})"
|
||||
self.fields["duration"].widget.attrs[
|
||||
"placeholder"
|
||||
] = f"{event_type.name} Duration (in minutes, max {event_type.event_duration_minutes})"
|
||||
else:
|
||||
self.fields[
|
||||
"duration"
|
||||
].help_text = (
|
||||
f"Please enter the duration of this {event_type.name} (in minutes)"
|
||||
)
|
||||
self.fields["duration"].widget.attrs[
|
||||
"placeholder"
|
||||
] = f"{event_type.name} Duration (in minutes)"
|
||||
|
||||
if not event_type.name == LIGHTNING_TALK:
|
||||
# Only lightning talks submissions will have to choose whether to use provided speaker laptop
|
||||
del self.fields["use_provided_speaker_laptop"]
|
||||
|
||||
if eventtype.name == DEBATE:
|
||||
if event_type.name == DEBATE:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Title of debate"
|
||||
self.fields["title"].help_text = "The title of this debate"
|
||||
|
@ -340,17 +342,12 @@ class EventProposalForm(forms.ModelForm):
|
|||
self.fields["abstract"].help_text = "The description of this debate"
|
||||
|
||||
# fix label and help_text for the submission_notes field
|
||||
self.fields["submission_notes"].label = "Debate Act Notes"
|
||||
self.fields["submission_notes"].label = "Debate Notes"
|
||||
self.fields[
|
||||
"submission_notes"
|
||||
].help_text = "Private notes regarding this debate. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
# better placeholder text for duration field
|
||||
self.fields["duration"].widget.attrs[
|
||||
"placeholder"
|
||||
] = "Debate Duration (minutes)"
|
||||
|
||||
elif eventtype.name == MUSIC_ACT:
|
||||
elif event_type.name == MUSIC_ACT:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Title of music act"
|
||||
self.fields["title"].help_text = "The title of this music act/concert/set."
|
||||
|
@ -368,10 +365,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no video recording for music acts
|
||||
del self.fields["allow_video_recording"]
|
||||
|
||||
# better placeholder text for duration field
|
||||
self.fields["duration"].widget.attrs["placeholder"] = "Duration (minutes)"
|
||||
|
||||
elif eventtype.name == RECREATIONAL_EVENT:
|
||||
elif event_type.name == RECREATIONAL_EVENT:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Event Title"
|
||||
self.fields["title"].help_text = "The title of this recreational event"
|
||||
|
@ -391,11 +385,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no video recording for music acts
|
||||
del self.fields["allow_video_recording"]
|
||||
|
||||
# better placeholder text for duration field
|
||||
self.fields["duration"].label = "Event Duration"
|
||||
self.fields["duration"].widget.attrs["placeholder"] = "Duration (minutes)"
|
||||
|
||||
elif eventtype.name in [TALK, LIGHTNING_TALK]:
|
||||
elif event_type.name in [TALK, LIGHTNING_TALK]:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Title of Talk"
|
||||
self.fields["title"].help_text = "The title of this talk/presentation."
|
||||
|
@ -412,7 +402,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
"submission_notes"
|
||||
].help_text = "Private notes regarding this talk. Only visible to yourself and the BornHack organisers."
|
||||
|
||||
if self.fields.get("slides_url") and eventtype.name == LIGHTNING_TALK:
|
||||
if self.fields.get("slides_url") and event_type.name == LIGHTNING_TALK:
|
||||
self.fields[
|
||||
"slides_url"
|
||||
].help_text += " You will only get assigned a slot if you have provided slides (a title slide is enough if you don't use slides for the talk). You can add an URL later if need be."
|
||||
|
@ -420,7 +410,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no duration for talks
|
||||
del self.fields["duration"]
|
||||
|
||||
elif eventtype.name == WORKSHOP:
|
||||
elif event_type.name == WORKSHOP:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Workshop Title"
|
||||
self.fields["title"].help_text = "The title of this workshop."
|
||||
|
@ -440,13 +430,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no video recording for workshops
|
||||
del self.fields["allow_video_recording"]
|
||||
|
||||
# duration field
|
||||
self.fields["duration"].label = "Workshop Duration"
|
||||
self.fields[
|
||||
"duration"
|
||||
].help_text = "How much time (in minutes) should we set aside for this workshop? Please keep it between 60 and 180 minutes (1-3 hours)."
|
||||
|
||||
elif eventtype.name == SLACKING_OFF:
|
||||
elif event_type.name == SLACKING_OFF:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Event Title"
|
||||
self.fields["title"].help_text = "The title of this recreational event."
|
||||
|
@ -466,13 +450,7 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no video recording for recreational events
|
||||
del self.fields["allow_video_recording"]
|
||||
|
||||
# duration field
|
||||
self.fields["duration"].label = "Event Duration"
|
||||
self.fields[
|
||||
"duration"
|
||||
].help_text = "How much time (in minutes) should we set aside for this event? Please keep it between 60 and 180 minutes (1-3 hours)."
|
||||
|
||||
elif eventtype.name == MEETUP:
|
||||
elif event_type.name == MEETUP:
|
||||
# fix label and help_text for the title field
|
||||
self.fields["title"].label = "Meetup Title"
|
||||
self.fields["title"].help_text = "The title of this meetup."
|
||||
|
@ -492,12 +470,6 @@ class EventProposalForm(forms.ModelForm):
|
|||
# no video recording for meetups
|
||||
del self.fields["allow_video_recording"]
|
||||
|
||||
# duration field
|
||||
self.fields["duration"].label = "Meetup Duration"
|
||||
self.fields[
|
||||
"duration"
|
||||
].help_text = "How much time (in minutes) should we set aside for this meetup? Please keep it between 60 and 180 minutes (1-3 hours)."
|
||||
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"Unsupported event type, don't know which form class to use"
|
||||
|
|
15
src/program/migrations/0085_btree_gist_extension.py
Normal file
15
src/program/migrations/0085_btree_gist_extension.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-12 18:15
|
||||
|
||||
from django.contrib.postgres.operations import BtreeGistExtension
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("program", "0084_auto_20200229_1801"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
BtreeGistExtension(),
|
||||
]
|
426
src/program/migrations/0086_sessions_slots_availability.py
Normal file
426
src/program/migrations/0086_sessions_slots_availability.py
Normal file
|
@ -0,0 +1,426 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-12 18:17
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.postgres.constraints
|
||||
import django.contrib.postgres.fields.ranges
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("camps", "0034_add_team_permission_sets"),
|
||||
("program", "0085_btree_gist_extension"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="EventSession",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"when",
|
||||
django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
help_text="A period of time where this type of event can be scheduled. Input format is <i>YYYY-MM-DD HH:MM</i>"
|
||||
),
|
||||
),
|
||||
(
|
||||
"event_duration_minutes",
|
||||
models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="The duration of events in this EventSession. Defaults to the value from the EventType of this EventSession.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Description of this session (optional)."
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["when", "event_type", "event_location"],},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EventSlot",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"when",
|
||||
django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
help_text="Start and end time of this slot"
|
||||
),
|
||||
),
|
||||
(
|
||||
"autoscheduled",
|
||||
models.NullBooleanField(
|
||||
default=None,
|
||||
help_text="True if the Event was scheduled by the AutoScheduler, False if it was scheduled manually, None if there is nothing scheduled in this EventSlot.",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["when"],},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SpeakerAvailability",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"when",
|
||||
django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
db_index=True,
|
||||
help_text="The period when this speaker is available or unavailable. Must be 1 hour!",
|
||||
),
|
||||
),
|
||||
(
|
||||
"available",
|
||||
models.BooleanField(
|
||||
db_index=True,
|
||||
help_text="Is the speaker available or unavailable during this hour? Check for available, uncheck for unavailable.",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SpeakerEventConflict",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={"abstract": False,},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SpeakerProposalAvailability",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"when",
|
||||
django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
db_index=True,
|
||||
help_text="The period when this speaker is available or unavailable. Must be 1 hour!",
|
||||
),
|
||||
),
|
||||
(
|
||||
"available",
|
||||
models.BooleanField(
|
||||
db_index=True,
|
||||
help_text="Is the speaker available or unavailable during this hour? Check for available, uncheck for unavailable.",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SpeakerProposalEventConflict",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("updated", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={"abstract": False,},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="demand",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="The estimated demand for this event. Used by the autoscheduler to pick the optimal location for events. Set to 0 to disable demand constraints for this event.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="duration_minutes",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="The duration of this event in minutes. Leave blank to use the default from the eventtype.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventinstance",
|
||||
name="autoscheduled",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if this was created by the autoscheduler.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventlocation",
|
||||
name="capacity",
|
||||
field=models.PositiveIntegerField(
|
||||
default=20,
|
||||
help_text="The capacity of this location. Used by the autoscheduler.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventlocation",
|
||||
name="conflicts",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Select the locations which this location conflicts with. Nothing can be scheduled in a location if a conflicting location has an EventInstance at the same time. Example: If one room can be split into two, then the big room would conflict with each of the two small rooms (but the small rooms would not conflict with eachother).",
|
||||
related_name="_eventlocation_conflicts_+",
|
||||
to="program.EventLocation",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventtype",
|
||||
name="event_duration_minutes",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="The default duration of an event of this type, in minutes. Optional. This default can be overridden in individual EventSessions as needed.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventtype",
|
||||
name="support_autoscheduling",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Check to enable this EventType in the autoscheduler",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventinstance",
|
||||
name="location",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventinstances",
|
||||
to="program.EventLocation",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventinstance",
|
||||
name="when",
|
||||
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
blank=True, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="event_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="The type of event",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventproposals",
|
||||
to="program.EventType",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="eventinstance",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[("when", "&&"), ("location", "=")],
|
||||
name="prevent_eventinstance_location_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakerproposaleventconflict",
|
||||
name="events",
|
||||
field=models.ManyToManyField(
|
||||
help_text="The conflict events",
|
||||
related_name="speakerproposalconflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakerproposaleventconflict",
|
||||
name="speakerproposal",
|
||||
field=models.OneToOneField(
|
||||
help_text="The SpeakerProposal",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventconflicts",
|
||||
to="program.SpeakerProposal",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakerproposalavailability",
|
||||
name="speakerproposal",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The speaker proposal object this availability belongs to",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="availabilities",
|
||||
to="program.SpeakerProposal",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakereventconflict",
|
||||
name="events",
|
||||
field=models.ManyToManyField(
|
||||
help_text="The conflict events",
|
||||
related_name="speakerconflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakereventconflict",
|
||||
name="speaker",
|
||||
field=models.OneToOneField(
|
||||
help_text="The Speaker",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventconflicts",
|
||||
to="program.Speaker",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakeravailability",
|
||||
name="speaker",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The speaker object this availability belongs to (if any)",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="availabilities",
|
||||
to="program.Speaker",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventslot",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The Event scheduled in this EventSlot",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_slots",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventslot",
|
||||
name="event_session",
|
||||
field=models.ForeignKey(
|
||||
help_text="The EventSession this EventSlot belongs to",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_slots",
|
||||
to="program.EventSession",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventsession",
|
||||
name="camp",
|
||||
field=models.ForeignKey(
|
||||
help_text="The Camp this EventSession belongs to",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventsessions",
|
||||
to="camps.Camp",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventsession",
|
||||
name="event_location",
|
||||
field=models.ForeignKey(
|
||||
help_text="The event location this session is for",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventsessions",
|
||||
to="program.EventLocation",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventsession",
|
||||
name="event_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="The type of event this session is for",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="eventsessions",
|
||||
to="program.EventType",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakerproposalavailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
(django.db.models.expressions.F("speakerproposal"), "="),
|
||||
("when", "&&"),
|
||||
],
|
||||
name="prevent_speakerproposalavailability_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakeravailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
(django.db.models.expressions.F("speaker"), "="),
|
||||
("when", "&&"),
|
||||
],
|
||||
name="prevent_speakeravailability_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="eventslot",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[("when", "&&"), ("event_session", "=")],
|
||||
name="prevent_slot_session_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="eventsession",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
("when", "&&"),
|
||||
("event_location", "="),
|
||||
("event_type", "="),
|
||||
],
|
||||
name="prevent_eventsession_eventtype_eventlocation_overlaps",
|
||||
),
|
||||
),
|
||||
]
|
215
src/program/migrations/0087_fk_and_related_name_underscores.py
Normal file
215
src/program/migrations/0087_fk_and_related_name_underscores.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-13 12:43
|
||||
|
||||
import django.contrib.postgres.constraints
|
||||
import django.db.models.deletion
|
||||
import django.db.models.expressions
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("camps", "0034_add_team_permission_sets"),
|
||||
("program", "0086_sessions_slots_availability"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="eventsession",
|
||||
name="prevent_eventsession_eventtype_eventlocation_overlaps",
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="speakeravailability",
|
||||
name="prevent_speakeravailability_overlaps",
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="speakerproposalavailability",
|
||||
name="prevent_speakerproposalavailability_overlaps",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="speakerproposalavailability",
|
||||
old_name="speakerproposal",
|
||||
new_name="speaker_proposal",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="url", old_name="eventproposal", new_name="event_proposal",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="url", old_name="speakerproposal", new_name="speaker_proposal",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="url", old_name="urltype", new_name="url_type",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="duration_minutes",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="The duration of this event in minutes. Leave blank to use the default from the event_type.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventlocation",
|
||||
name="camp",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_locations",
|
||||
to="camps.Camp",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="event_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="The type of event",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_proposals",
|
||||
to="program.EventType",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="speakers",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.",
|
||||
related_name="event_proposals",
|
||||
to="program.SpeakerProposal",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="track",
|
||||
field=models.ForeignKey(
|
||||
help_text="The track this event belongs to",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_proposals",
|
||||
to="program.EventTrack",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventsession",
|
||||
name="camp",
|
||||
field=models.ForeignKey(
|
||||
help_text="The Camp this EventSession belongs to",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_sessions",
|
||||
to="camps.Camp",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventsession",
|
||||
name="event_location",
|
||||
field=models.ForeignKey(
|
||||
help_text="The event location this session is for",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_sessions",
|
||||
to="program.EventLocation",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventsession",
|
||||
name="event_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="The type of event this session is for",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_sessions",
|
||||
to="program.EventType",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventtrack",
|
||||
name="camp",
|
||||
field=models.ForeignKey(
|
||||
help_text="The Camp this Track belongs to",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_tracks",
|
||||
to="camps.Camp",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakereventconflict",
|
||||
name="events",
|
||||
field=models.ManyToManyField(
|
||||
help_text="The conflict events",
|
||||
related_name="speaker_conflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakereventconflict",
|
||||
name="speaker",
|
||||
field=models.OneToOneField(
|
||||
help_text="The Speaker",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_conflicts",
|
||||
to="program.Speaker",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakerproposal",
|
||||
name="camp",
|
||||
field=models.ForeignKey(
|
||||
editable=False,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="speaker_proposals",
|
||||
to="camps.Camp",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakerproposaleventconflict",
|
||||
name="events",
|
||||
field=models.ManyToManyField(
|
||||
help_text="The conflict events",
|
||||
related_name="speaker_proposal_conflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakerproposaleventconflict",
|
||||
name="speakerproposal",
|
||||
field=models.OneToOneField(
|
||||
help_text="The SpeakerProposal",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="event_conflicts",
|
||||
to="program.SpeakerProposal",
|
||||
),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="speakerproposaleventconflict",
|
||||
old_name="speakerproposal",
|
||||
new_name="speaker_proposal",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="eventsession",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
("when", "&&"),
|
||||
("event_location", "="),
|
||||
("event_type", "="),
|
||||
],
|
||||
name="prevent_event_session_event_type_event_location_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakeravailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
(django.db.models.expressions.F("speaker"), "="),
|
||||
("when", "&&"),
|
||||
],
|
||||
name="prevent_speaker_availability_overlaps",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakerproposalavailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
(django.db.models.expressions.F("speaker_proposal"), "="),
|
||||
("when", "&&"),
|
||||
],
|
||||
name="prevent_speaker_proposal_availability_overlaps",
|
||||
),
|
||||
),
|
||||
]
|
114
src/program/migrations/0088_speaker_event_conflicts_and_more.py
Normal file
114
src/program/migrations/0088_speaker_event_conflicts_and_more.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-20 22:59
|
||||
|
||||
import django.contrib.postgres.constraints
|
||||
import django.db.models.deletion
|
||||
import utils.database
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("program", "0087_fk_and_related_name_underscores"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="speakereventconflict", name="events",),
|
||||
migrations.RemoveField(model_name="speakereventconflict", name="speaker",),
|
||||
migrations.RemoveField(
|
||||
model_name="speakerproposaleventconflict", name="events",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="speakerproposaleventconflict", name="speaker_proposal",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="eventtype",
|
||||
name="support_speaker_event_conflicts",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="True if Events of this type should be selectable in the EventConflict m2m for SpeakerProposal and Speaker objects.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speaker",
|
||||
name="event_conflicts",
|
||||
field=models.ManyToManyField(
|
||||
help_text="The Events this person wishes to attend. The AutoScheduler will avoid conflicts.",
|
||||
related_name="speaker_conflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="speakerproposal",
|
||||
name="event_conflicts",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Pick the Events this person wishes to attend, and we will attempt to avoid scheduling conflicts.",
|
||||
related_name="speaker_proposal_conflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="event_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="The type of this event",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="events",
|
||||
to="program.EventType",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventlocation",
|
||||
name="conflicts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Select the locations which this location conflicts with. Nothing can be scheduled in a location if a conflicting location has a scheduled Event at the same time. Example: If one room can be split into two, then the big room would conflict with each of the two small rooms (but the small rooms would not conflict with eachother).",
|
||||
related_name="_eventlocation_conflicts_+",
|
||||
to="program.EventLocation",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="duration",
|
||||
field=models.IntegerField(
|
||||
blank=True,
|
||||
help_text="How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="eventslot",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The Event scheduled in this EventSlot",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="event_slots",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakeravailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
("speaker", "="),
|
||||
(utils.database.CastToInteger("available"), "="),
|
||||
("when", "-|-"),
|
||||
],
|
||||
name="prevent_speaker_availability_adjacent_mergeable",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="speakerproposalavailability",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[
|
||||
("speaker_proposal", "="),
|
||||
(utils.database.CastToInteger("available"), "="),
|
||||
("when", "-|-"),
|
||||
],
|
||||
name="prevent_speaker_proposal_availability_adjacent_mergeable",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(name="SpeakerEventConflict",),
|
||||
migrations.DeleteModel(name="SpeakerProposalEventConflict",),
|
||||
]
|
23
src/program/migrations/0089_event_conflicts_blank.py
Normal file
23
src/program/migrations/0089_event_conflicts_blank.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-21 17:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("program", "0088_speaker_event_conflicts_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="speakerproposal",
|
||||
name="event_conflicts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Pick the Events this person wishes to attend, and we will attempt to avoid scheduling conflicts.",
|
||||
related_name="speaker_proposal_conflicts",
|
||||
to="program.Event",
|
||||
),
|
||||
),
|
||||
]
|
25
src/program/migrations/0090_event_tags.py
Normal file
25
src/program/migrations/0090_event_tags.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-21 20:54
|
||||
|
||||
import taggit.managers
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("taggit", "0003_taggeditem_add_unique_index"),
|
||||
("program", "0089_event_conflicts_blank"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="tags",
|
||||
field=taggit.managers.TaggableManager(
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
]
|
26
src/program/migrations/0091_eventproposal_tags.py
Normal file
26
src/program/migrations/0091_eventproposal_tags.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-21 20:54
|
||||
|
||||
import taggit.managers
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("taggit", "0003_taggeditem_add_unique_index"),
|
||||
("utils", "0004_uuidtaggeditem"),
|
||||
("program", "0090_event_tags"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="eventproposal",
|
||||
name="tags",
|
||||
field=taggit.managers.TaggableManager(
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="utils.UUIDTaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
]
|
25
src/program/migrations/0092_event_uuid.py
Normal file
25
src/program/migrations/0092_event_uuid.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-22 06:16
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("program", "0091_eventproposal_tags"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="uuid",
|
||||
field=models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="This field is not the PK of the model. It is used to create EventSlot UUID for FRAB and iCal and other calendaring purposes.",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
]
|
29
src/program/migrations/0093_proposal_reason_description.py
Normal file
29
src/program/migrations/0093_proposal_reason_description.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-03 12:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("program", "0092_event_uuid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="reason",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="The reason this proposal was accepted or rejected. This text will be included in the email to the submitter. Leave blank to send a standard email.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="speakerproposal",
|
||||
name="reason",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="The reason this proposal was accepted or rejected. This text will be included in the email to the submitter. Leave blank to send a standard email.",
|
||||
),
|
||||
),
|
||||
]
|
27
src/program/migrations/0094_tags_blank.py
Normal file
27
src/program/migrations/0094_tags_blank.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-03 16:57
|
||||
|
||||
import taggit.managers
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("utils", "0004_uuidtaggeditem"),
|
||||
("taggit", "0003_taggeditem_add_unique_index"),
|
||||
("program", "0093_proposal_reason_description"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventproposal",
|
||||
name="tags",
|
||||
field=taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="utils.UUIDTaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -4,6 +4,10 @@ from django.http import Http404
|
|||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from program.utils import (
|
||||
add_existing_availability_to_matrix,
|
||||
get_speaker_availability_form_matrix,
|
||||
)
|
||||
|
||||
from . import models
|
||||
|
||||
|
@ -77,11 +81,11 @@ class UrlViewMixin(object):
|
|||
"""
|
||||
# get the proposal
|
||||
if "event_uuid" in self.kwargs:
|
||||
self.eventproposal = get_object_or_404(
|
||||
self.event_proposal = get_object_or_404(
|
||||
models.EventProposal, uuid=self.kwargs["event_uuid"], user=request.user
|
||||
)
|
||||
elif "speaker_uuid" in self.kwargs:
|
||||
self.speakerproposal = get_object_or_404(
|
||||
self.speaker_proposal = get_object_or_404(
|
||||
models.SpeakerProposal,
|
||||
uuid=self.kwargs["speaker_uuid"],
|
||||
user=request.user,
|
||||
|
@ -96,20 +100,20 @@ class UrlViewMixin(object):
|
|||
Include the proposal in the template context
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
if hasattr(self, "eventproposal") and self.eventproposal:
|
||||
context["eventproposal"] = self.eventproposal
|
||||
if hasattr(self, "event_proposal") and self.event_proposal:
|
||||
context["event_proposal"] = self.event_proposal
|
||||
else:
|
||||
context["speakerproposal"] = self.speakerproposal
|
||||
context["speaker_proposal"] = self.speaker_proposal
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
Return to the detail view of the proposal
|
||||
"""
|
||||
if hasattr(self, "eventproposal"):
|
||||
return self.eventproposal.get_absolute_url()
|
||||
if hasattr(self, "event_proposal"):
|
||||
return self.event_proposal.get_absolute_url()
|
||||
else:
|
||||
return self.speakerproposal.get_absolute_url()
|
||||
return self.speaker_proposal.get_absolute_url()
|
||||
|
||||
|
||||
class EventViewMixin(CampViewMixin):
|
||||
|
@ -136,9 +140,69 @@ class EventFeedbackViewMixin(EventViewMixin):
|
|||
|
||||
def setup(self, *args, **kwargs):
|
||||
super().setup(*args, **kwargs)
|
||||
self.eventfeedback = get_object_or_404(
|
||||
self.event_feedback = get_object_or_404(
|
||||
models.EventFeedback, event=self.event, user=self.request.user,
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
return self.eventfeedback
|
||||
return self.event_feedback
|
||||
|
||||
|
||||
class AvailabilityMatrixViewMixin(CampViewMixin):
|
||||
"""
|
||||
Mixin with shared code between all availability matrix views,
|
||||
meaning all views that show an availability matrix (form or not)
|
||||
for a SpeakerProposal or Speaker object. Used by SpeakerProposal
|
||||
submitters and in backoffice.
|
||||
"""
|
||||
|
||||
def setup(self, *args, **kwargs):
|
||||
""" Get the availability matrix"""
|
||||
super().setup(*args, **kwargs)
|
||||
# do we have an Event or an EventProposal?
|
||||
if hasattr(self.get_object(), "events"):
|
||||
# we have an Event
|
||||
event_types = models.EventType.objects.filter(
|
||||
events__in=self.get_object().events.all()
|
||||
).distinct()
|
||||
else:
|
||||
# we have an EventProposal
|
||||
event_types = models.EventType.objects.filter(
|
||||
event_proposals__in=self.get_object().event_proposals.all()
|
||||
).distinct()
|
||||
# get the matrix and add any existing availability to it
|
||||
self.matrix = get_speaker_availability_form_matrix(
|
||||
sessions=self.camp.event_sessions.filter(event_type__in=event_types)
|
||||
)
|
||||
add_existing_availability_to_matrix(self.matrix, self.get_object())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
""" Add the matrix to form kwargs, only used if the view has a form """
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["matrix"] = self.matrix
|
||||
return kwargs
|
||||
|
||||
def get_initial(self, *args, **kwargs):
|
||||
""" Populate the speaker_availability checkboxes, only used if the view has a form """
|
||||
initial = super().get_initial(*args, **kwargs)
|
||||
|
||||
# add initial checkbox states
|
||||
for date in self.matrix.keys():
|
||||
# loop over daychunks and check if we need a checkbox
|
||||
for daychunk in self.matrix[date].keys():
|
||||
if not self.matrix[date][daychunk]:
|
||||
# we have no event_session here, carry on
|
||||
continue
|
||||
if self.matrix[date][daychunk]["initial"] in [True, None]:
|
||||
initial[self.matrix[date][daychunk]["fieldname"]] = True
|
||||
else:
|
||||
initial[self.matrix[date][daychunk]["fieldname"]] = False
|
||||
|
||||
# we are ready to render the form
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Add the matrix to context """
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["matrix"] = self.matrix
|
||||
return context
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -82,7 +82,7 @@ class UrlNode(DjangoObjectType):
|
|||
class Meta:
|
||||
model = Url
|
||||
interfaces = (relay.Node,)
|
||||
only_fields = ("url", "urltype")
|
||||
only_fields = ("url", "url_type")
|
||||
|
||||
|
||||
class UrlTypeNode(DjangoObjectType):
|
||||
|
|
|
@ -2,8 +2,6 @@ import logging
|
|||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .email import add_event_scheduled_email
|
||||
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
|
@ -36,32 +34,6 @@ def check_speaker_event_camp_consistency(sender, instance, **kwargs):
|
|||
)
|
||||
|
||||
|
||||
def check_speaker_camp_change(sender, instance, **kwargs):
|
||||
if instance.pk:
|
||||
for event in instance.events.all():
|
||||
if event.camp != instance.camp:
|
||||
raise ValidationError(
|
||||
{
|
||||
"camp": "You cannot change the camp a speaker belongs to if the speaker is associated with one or more events."
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def eventinstance_pre_save(sender, instance, **kwargs):
|
||||
""" Save the old instance.when value so we can later determine if it changed """
|
||||
try:
|
||||
# get the old instance from the database, if we have one
|
||||
instance.old_when = sender.objects.get(pk=instance.pk).when
|
||||
except sender.DoesNotExist:
|
||||
# nothing found in the DB with this pk, this is a new eventinstance
|
||||
instance.old_when = instance.when
|
||||
|
||||
|
||||
def eventinstance_post_save(sender, instance, created, **kwargs):
|
||||
""" Send an email if this is a new eventinstance, or if the "when" field changed """
|
||||
if created:
|
||||
add_event_scheduled_email(eventinstance=instance, action="scheduled")
|
||||
else:
|
||||
if instance.old_when != instance.when:
|
||||
# date/time for this eventinstance changed, send a rescheduled email
|
||||
add_event_scheduled_email(eventinstance=instance, action="rescheduled")
|
||||
def event_session_post_save(sender, instance, created, **kwargs):
|
||||
""" Make sure we have the number of EventSlots we need to have, adjust if not """
|
||||
instance.fixup_event_slots()
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
{% extends 'program_base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Use Existing {{ eventtype.host_title }} or Add New? | {{ block.super }}
|
||||
Use Existing {{ event_type.host_title }} or Add New? | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block program_content %}
|
||||
|
||||
<h3>Use Existing {{ eventtype.host_title }}?</h3>
|
||||
<h3>Use Existing {{ event_type.host_title }}?</h3>
|
||||
|
||||
<p class="lead">Pick a {{ eventtype.host_title }} from the list below, or press the button at the bottom to add a new {{ eventtype.host_title }} for this {{ eventtype.name }}.</p>
|
||||
<p class="lead">Pick a {{ event_type.host_title }} from the list below, or press the button at the bottom to add a new {{ event_type.host_title }} for this {{ event_type.name }}.</p>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Use an Existing {{ eventtype.host_title }}</h3>
|
||||
<h3 class="panel-title">Use an Existing {{ event_type.host_title }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="list-group">
|
||||
{% for speakerproposal in speakerproposal_list %}
|
||||
<a href="{% url 'program:eventproposal_create' camp_slug=camp.slug event_type_slug=eventtype.slug speaker_uuid=speakerproposal.uuid %}" class="list-group-item">
|
||||
{% for speaker_proposal in speaker_proposal_list %}
|
||||
<a href="{% url 'program:event_proposal_create' camp_slug=camp.slug event_type_slug=event_type.slug speaker_uuid=speaker_proposal.uuid %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
Use {{ speakerproposal.name }} as {{ eventtype.host_title }}
|
||||
Use {{ speaker_proposal.name }} as {{ event_type.host_title }}
|
||||
</h4>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@ -27,7 +27,7 @@ Use Existing {{ eventtype.host_title }} or Add New? | {{ block.super }}
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'program:proposal_combined_submit' camp_slug=camp.slug event_type_slug=eventtype.slug %}" class="btn btn-primary btn-success"><i class="fas fa-plus"></i> Add New {{ eventtype.host_title }}</a>
|
||||
<a href="{% url 'program:proposal_combined_submit' camp_slug=camp.slug event_type_slug=event_type.slug %}" class="btn btn-primary btn-success"><i class="fas fa-plus"></i> Add New {{ event_type.host_title }}</a>
|
||||
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
{% endblock %}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue