from statistics import mean
from collections import defaultdict, OrderedDict
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.forms.models import modelformset_factory
from vanilla import FormView, UpdateView
from crispy_forms.layout import Submit
from braces.views import FormValidMessageMixin, SetHeadlineMixin
from .models import (
Trip, TripTemplate, TripType, Campsite, Section,
NUM_BAGELS_SUPPLEMENT, NUM_BAGELS_REGULAR
)
from .forms import (
SectionForm, LeaderAssignmentForm,
TrippeeAssignmentForm, FoodboxFormsetHelper
)
from fyt.applications.models import LeaderSupplement, GeneralApplication
from fyt.incoming.models import IncomingStudent, Registration
from fyt.db.views import (
DatabaseCreateView, DatabaseUpdateView, DatabaseDeleteView,
DatabaseListView, DatabaseDetailView, DatabaseTemplateView,
TripsYearMixin
)
from fyt.permissions.views import (
ApplicationEditPermissionRequired,
DatabaseEditPermissionRequired
)
from fyt.utils.views import PopulateMixin
from fyt.utils.cache import cache_as
from fyt.utils.forms import crispify
from fyt.transport.models import ExternalBus, ScheduledTransport
FIRST_CHOICE = 'first choice'
PREFER = 'prefer'
AVAILABLE = 'available'
class _SectionMixin():
"""
Utility mixin for CBVs which have a section_pk url kwarg.
"""
@cache_as('_section')
def get_section(self):
return Section.objects.get(pk=self.kwargs['section_pk'])
def get_context_data(self, **kwargs):
kwargs['section'] = self.get_section()
return super(_SectionMixin, self).get_context_data(**kwargs)
class _TripMixin():
"""
Mixin to pull a trip object from a trip_pk url kwarg
"""
@cache_as('_trip')
def get_trip(self):
return Trip.objects.get(pk=self.kwargs['trip_pk'])
def get_context_data(self, **kwargs):
kwargs['trip'] = self.get_trip()
return super(_TripMixin, self).get_context_data(**kwargs)
class TripList(DatabaseTemplateView):
template_name = 'trips/trip_index.html'
def extra_context(self):
return {'matrix': Trip.objects.matrix(self.kwargs['trips_year'])}
class TripUpdate(DatabaseUpdateView):
model = Trip
fields = [
'dropoff_time', 'pickup_time',
'dropoff_route', 'pickup_route', 'return_route',
'notes'
]
def get_headline(self):
return mark_safe(
"Edit %s <small> Trip </small>" % self.object
)
class TripDetail(DatabaseDetailView):
model = Trip
template_name = 'trips/trip_detail.html'
fields = [
'section', 'template',
'leaders', 'trippees',
'notes',
('dropoff route', 'get_dropoff_route'),
'dropoff_time',
('pickup route', 'get_pickup_route'),
'pickup_time',
('return route', 'get_return_route')
]
triptemplate_fields = [
'triptype', 'max_trippees',
'non_swimmers_allowed',
'desc_intro',
'dropoff_stop', 'desc_day1', 'campsite1',
'desc_day2', 'campsite2',
'desc_day3', 'pickup_stop',
'desc_conc',
'revisions']
class TripCreate(PopulateMixin, DatabaseCreateView):
model = Trip
fields = ['section', 'template']
class TripDelete(DatabaseDeleteView):
model = Trip
success_url_pattern = 'db:trip_index'
class TripTemplateList(DatabaseListView):
model = TripTemplate
context_object_name = 'templates'
template_name = 'trips/template_index.html'
def get_queryset(self):
qs = super(TripTemplateList, self).get_queryset()
return qs.select_related(
'triptype', 'campsite1', 'campsite2', 'dropoff_stop', 'pickup_stop'
)
class TripTemplateCreate(DatabaseCreateView):
model = TripTemplate
class TripTemplateDetail(DatabaseDetailView):
model = TripTemplate
fields = ['name', 'description_summary', 'triptype',
'max_trippees', 'non_swimmers_allowed', 'dropoff_stop',
'campsite1', 'campsite2', 'pickup_stop', 'return_route',
'desc_intro', 'desc_day1',
'desc_day2', 'desc_day3',
'desc_conc', 'revisions']
class TripTemplateUpdate(DatabaseUpdateView):
model = TripTemplate
class TripTemplateDelete(DatabaseDeleteView):
model = TripTemplate
success_url_pattern = 'db:triptemplate_index'
class TripTypeList(DatabaseListView):
model = TripType
context_object_name = 'triptypes'
template_name = 'trips/triptype_index.html'
class TripTypeCreate(DatabaseCreateView):
model = TripType
class TripTypeDetail(DatabaseDetailView):
model = TripType
fields = ['name', 'trippee_description', 'leader_description',
'packing_list']
class TripTypeUpdate(DatabaseUpdateView):
model = TripType
class TripTypeDelete(DatabaseDeleteView):
model = TripType
success_url_pattern = 'db:triptype_index'
class CampsiteList(DatabaseListView):
model = Campsite
context_object_name = 'campsites'
template_name = 'trips/campsite_index.html'
def extra_context(self):
return {
'camping_dates': Section.dates.camping_dates(self.kwargs['trips_year'])
}
class CampsiteCreate(DatabaseCreateView):
model = Campsite
class CampsiteDetail(DatabaseDetailView):
model = Campsite
fields = ['name', 'capacity', 'directions', 'bugout', 'secret']
class CampsiteUpdate(DatabaseUpdateView):
model = Campsite
class CampsiteDelete(DatabaseDeleteView):
model = Campsite
success_url_pattern = 'db:campsite_index'
class SectionList(DatabaseListView):
model = Section
context_object_name = 'sections'
template_name = 'trips/section_index.html'
class SectionCreate(DatabaseCreateView):
model = Section
form_class = SectionForm
template_name = 'trips/section_create.html'
class SectionDetail(DatabaseDetailView):
model = Section
fields = ['name', 'leaders_arrive', 'is_local', 'is_exchange',
'is_transfer', 'is_international', 'is_native', 'is_fysep']
class SectionUpdate(DatabaseUpdateView):
model = Section
form_class = SectionForm
template_name = 'trips/section_update.html'
class SectionDelete(DatabaseDeleteView):
model = Section
success_url_pattern = 'db:section_index'
[docs]class LeaderTrippeeIndexView(DatabaseListView):
"""
Show all Trips with leaders and trippees.
Links to pages to assign leaders and trippees.
"""
model = Trip
template_name = 'trips/assignments.html'
context_object_name = 'trips'
def get_queryset(self):
return (
super(LeaderTrippeeIndexView, self).get_queryset()
.prefetch_related('leaders', 'leaders__applicant', 'trippees')
)
[docs]class AssignTrippee(_TripMixin, DatabaseListView):
"""
Assign trippees to a trip.
The trip's pk is passed in the url arg.
Each trippee passed to the context has the following properties:
* ``assignment_url`` - the url to assign trippee to this trip
* ``triptype_pref`` - 'first choice', 'prefer', or 'available'
* ``section_pref`` - 'prefer' or 'available'
* ``bus_available`` - ``True`` if trippee requested a bus and it is
scheduled this section, otherwise ``False``
Because of our database structure, preferences are not easy to
compute efficiently. See below...
"""
model = IncomingStudent
template_name = 'trips/assign_trippee.html'
context_object_name = 'available_trippees'
[docs] def get_queryset(self):
"""
All trippees who prefer, are available, or chose this
trip as their first choice.
Only pull in required fields because a whole application
queryset is big enough to slow down performance.
"""
return (
self.model.objects.available_for_trip(
self.get_trip()
).select_related(
'trip_assignment__template',
'trip_assignment__section',
'registration__bus_stop_round_trip',
'registration__bus_stop_to_hanover',
'registration__bus_stop_from_hanover'
).only(
'name',
'address',
'trips_year',
'trip_assignment__template__name',
'trip_assignment__section__name',
'trip_assignment__trips_year',
'registration__bus_stop_round_trip__route_id',
'registration__bus_stop_round_trip__trips_year_id',
'registration__bus_stop_round_trip__name',
'registration__bus_stop_to_hanover__route_id',
'registration__bus_stop_to_hanover__trips_year_id',
'registration__bus_stop_to_hanover__name',
'registration__bus_stop_from_hanover__route_id',
'registration__bus_stop_from_hanover__trips_year_id',
'registration__bus_stop_from_hanover__name',
'registration__gender',
)
)
[docs] def get_context_data(self, **kwargs):
"""
In order to compute each trippee's triptype or section
preference,
Initially I tried using ``prefetch_related`` to load all
``preferred_triptypes``, ``available_triptypes``, etc.
However, this requires the database to load each trip, in the
worst case, on *every* tripppee, up to ``O(n)`` times for the
entire, queryset. Multiply this by the total number of trips,
and there goes performance.
Perhaps a SQL guru can figure out a clever way to compute this
in-database, but I could not.
The solution: use the ``through`` objects created by ``M2M`` fields.
We iterate through these objects for the sections and triptypes in
question and save each trippee's preference in a dict.
This technique is ``O(1)`` for queries and ``O(n)`` for in-memory
processing, which is quite acceptable. See http://goo.gl/QbK99D
"""
context = super(AssignTrippee, self).get_context_data(**kwargs)
context['trip'] = trip = self.get_trip()
section = trip.section
triptype = trip.template.triptype
trips_year = self.kwargs['trips_year']
triptype_pref = {}
for pair in (Registration.available_triptypes
.through.objects.filter(triptype=triptype)):
triptype_pref[pair.registration_id] = AVAILABLE
for pair in (Registration.preferred_triptypes
.through.objects.filter(triptype=triptype)):
triptype_pref[pair.registration_id] = PREFER
for registration in Registration.objects.filter(trips_year=trips_year):
if registration.firstchoice_triptype_id == triptype.id:
triptype_pref[registration.id] = FIRST_CHOICE
section_pref = {}
for pair in (Registration.available_sections
.through.objects.filter(section=section)):
section_pref[pair.registration_id] = AVAILABLE
for pair in (Registration.preferred_sections
.through.objects.filter(section=section)):
section_pref[pair.registration_id] = PREFER
# all external buses for this section
buses = ExternalBus.objects.filter(trips_year=trips_year, section=section)
# all ids of routes running on this section
route_ids = [bus.route_id for bus in buses]
for trippee in self.object_list:
reg = trippee.registration
url = reverse('db:assign_trippee_to_trip', kwargs={
'trips_year': trips_year,
'trippee_pk': trippee.pk
})
trippee.assignment_url = '%s?assign_to=%s' % (url, trip.pk)
trippee.triptype_pref = triptype_pref[reg.id]
trippee.section_pref = section_pref[reg.id]
bus_requests = (
reg.bus_stop_round_trip,
reg.bus_stop_to_hanover,
reg.bus_stop_from_hanover
)
if not any(bus_requests): # don't want a bus
trippee.bus_available = False
else:
trippee.bus_available = all([
bus.route_id in route_ids for bus in bus_requests if bus
])
return context
class AssignTrippeeToTrip(FormValidMessageMixin, DatabaseUpdateView):
model = IncomingStudent
lookup_url_kwarg = 'trippee_pk'
template_name = 'db/update.html'
form_class = TrippeeAssignmentForm
def get(self, request, *args, **kwargs):
"""
Pull the 'assign_to' trip from GET qs
"""
data = {'trip_assignment': request.GET['assign_to']}
form = self.get_form(data=data)
context = self.get_context_data(form=form)
return self.render_to_response(context)
def get_form(self, **kwargs):
return self.get_form_class()(self.get_trips_year(), **kwargs)
def get_form_valid_message(self):
""" Flash success message """
return '{} assigned to {}'.format(
self.object, self.object.trip_assignment
)
def get_headline(self):
self.object = self.get_object()
return 'Assign {} to trip'.format(self.object)
def get_success_url(self):
""" Override DatabaseUpdateView default """
return reverse('db:leader_index',
kwargs={'trips_year': self.get_trips_year()})
[docs]class AssignLeader(_TripMixin, DatabaseListView):
"""
Assign a leader to a trip.
The trip's pk is passed in the url kwargs.
The template is passed a list of tuples of the form
``(LeaderApplication, assign_url, triptype_pref, section_pref)``
* ``assign_url`` will be None if the leader is already assigned to a trip.
* ``triptype_pref`` - 'prefer' or 'available'
* ``section_pref`` - 'prefer' or 'available'
"""
model = GeneralApplication
template_name = 'trips/assign_leader.html'
context_object_name = 'leader_applications'
def get_queryset(self):
qs = (
self.model.objects.prospective_leaders_for_trip(self.get_trip())
.select_related(
'applicant',
'assigned_trip',
'assigned_trip__template',
'assigned_trip__section'
).prefetch_related(
'leader_supplement__grades'
).only(
'trips_year', 'gender',
'applicant__name',
'assigned_trip__trips_year_id',
'assigned_trip__template__name',
'assigned_trip__section__name',
'status'
)
)
# For some reason, annotating grades using Avg adds an
# expensive GROUP BY clause to the query, killing the site.
# See https://code.djangoproject.com/ticket/17144.
# Does this need to be reopened?
# TODO: check with 1.8
# This is a hackish workaround to explicitly compute the
# average for all applications with reasonable performance:
for app in qs:
if app.leader_supplement.grades.exists():
app.avg_grade = mean(
map(lambda g: g.grade, app.leader_supplement.grades.all())
)
else:
app.avg_grade = None
# return 0 in case someone has no grades
return sorted(qs, key=lambda x: x.avg_grade or 0, reverse=True)
[docs] def get_assign_url(self, leader, trip):
"""
Return the url used to assign leader to trip
"""
url = reverse('db:assign_leader_to_trip', kwargs={
'trips_year': self.kwargs['trips_year'],
'leader_pk': leader.pk
})
return '%s?assigned_trip=%s' % (url, trip.pk)
[docs] def get_context_data(self, **kwargs):
"""
Compute whether each leader prefers or is available for this
trip's section and triptype. We use the through fields of the
``M2M`` models because ``prefetch_related`` pulls in *all* related
objects--and all fields of the related objects--which is a huge
query and kills performance.
See :class:`~fyt.trips.views.AssignTrippee` for a similar situation
and more explanation.
"""
context = super(AssignLeader, self).get_context_data(**kwargs)
context['trip'] = trip = self.get_trip()
triptype = trip.template.triptype
section = trip.section
triptype_pref = {}
for pair in (LeaderSupplement.available_triptypes
.through.objects.filter(triptype=triptype)):
triptype_pref[pair.leadersupplement_id] = AVAILABLE
for pair in (LeaderSupplement.preferred_triptypes
.through.objects.filter(triptype=triptype)):
triptype_pref[pair.leadersupplement_id] = PREFER
section_pref = {}
for pair in (LeaderSupplement.available_sections
.through.objects.filter(section=section)):
section_pref[pair.leadersupplement_id] = AVAILABLE
for pair in (LeaderSupplement.preferred_sections
.through.objects.filter(section=section)):
section_pref[pair.leadersupplement_id] = PREFER
def process_leader(leader):
return (
leader,
self.get_assign_url(leader, trip),
triptype_pref[leader.id],
section_pref[leader.id]
)
leaders = [process_leader(x) for x in self.object_list]
context[self.context_object_name] = leaders
return context
class AssignLeaderToTrip(ApplicationEditPermissionRequired, PopulateMixin,
SetHeadlineMixin, FormValidMessageMixin,
TripsYearMixin, UpdateView):
model = GeneralApplication
lookup_url_kwarg = 'leader_pk'
template_name = 'db/update.html'
def get_form(self, **kwargs):
form = LeaderAssignmentForm(self.kwargs['trips_year'], **kwargs)
label = 'Assign to %s' % (
Trip.objects.get(pk=self.request.GET['assigned_trip'])
)
return crispify(form, label)
def get_form_valid_message(self):
return '{} assigned to lead {}'.format(
self.object.applicant, self.object.assigned_trip
)
def get_headline(self):
self.object = self.get_object()
return 'Assign {} to trip'.format(
self.object.applicant
)
def get_success_url(self):
return reverse('db:leader_index', kwargs={
'trips_year': self.kwargs['trips_year']
})
[docs]class RemoveAssignedTrip(ApplicationEditPermissionRequired,
FormValidMessageMixin, TripsYearMixin, UpdateView):
"""
Remove a leader's assigned trip
"""
model = GeneralApplication
lookup_url_kwarg = 'leader_pk'
template_name = 'trips/remove_leader_assignment.html'
def get_form(self, **kwargs):
# save old assignment so we can show it after deletion
self._assigned_trip = kwargs['instance'].assigned_trip
form = LeaderAssignmentForm(
self.kwargs['trips_year'], initial={'assigned_trip': None}, **kwargs
)
return crispify(form, 'Remove', 'btn-danger')
def get_form_valid_message(self):
return 'Leader {} removed from Trip {}'.format(
self.object.applicant, self._assigned_trip
)
def get_success_url(self):
return self.object.detail_url()
[docs]class TrippeeLeaderCounts(DatabaseTemplateView):
"""
Shows a matrix of the number of tripees and leaders for all trips
"""
template_name = 'trips/trippee_leader_counts.html'
def extra_context(self):
return {
'matrix': Trip.objects.matrix(self.kwargs['trips_year'])
}
class FoodboxCounts(DatabaseListView):
model = Trip
template_name = 'trips/foodboxes.html'
context_object_name = 'trips'
def get_queryset(self):
return Trip.objects.filter(
trips_year=self.kwargs['trips_year']
).select_related(
'template__triptype'
)
def extra_context(self):
qs = self.object_list
return {
'full': len(qs),
'half': len(list(filter(lambda x: x.half_foodbox, qs))),
'supp': len(list(filter(lambda x: x.supp_foodbox, qs))),
'bagels': sum(map(lambda x: x.bagels, qs)),
'bagel_ratio': NUM_BAGELS_REGULAR,
'supp_bagel_ratio': NUM_BAGELS_SUPPLEMENT,
}
class FoodboxRules(DatabaseEditPermissionRequired, TripsYearMixin, FormView):
template_name = 'trips/foodbox_rules.html'
def get_queryset(self):
return TripType.objects.filter(trips_year=self.kwargs['trips_year'])
def get_form(self, **kwargs):
FoodRulesFormset = modelformset_factory(
TripType, fields=['name', 'half_kickin', 'gets_supplemental'],
extra=0
)
formset = FoodRulesFormset(queryset=self.get_queryset(), **kwargs)
formset.helper = FoodboxFormsetHelper()
formset.helper.form_class = 'form-inline'
formset.helper.add_input(Submit('submit', 'Save'))
return formset
def form_valid(self, formset):
formset.save()
return super(FoodboxRules, self).form_valid(formset)
def get_success_url(self):
return self.request.path
[docs]class LeaderPacket(DatabaseDetailView):
"""
All information that leader's need: schedule, directions,
medical info, etc.
"""
model = Trip
template_name = 'trips/leader_packet.html'
[docs]class PacketsForSection(_SectionMixin, DatabaseListView):
"""
All leader packets for a section.
"""
model = Trip
template_name = 'trips/section_packet.html'
context_object_name = 'trips'
def get_queryset(self):
return super(PacketsForSection, self).get_queryset().filter(
section=self.get_section()
).select_related(
'template__campsite1',
'template__campsite2',
'template__dropoff_stop',
'template__pickup_stop'
).prefetch_related(
'leaders',
'leaders__applicant',
'trippees',
'trippees__registration'
)
[docs]class MedicalInfoForSection(PacketsForSection):
"""
Packets for croos, by section.
Contains leader and trippee med information.
"""
template_name = 'trips/medical_packet.html'
[docs]class TrippeeChecklist(_SectionMixin, DatabaseListView):
"""
Checklist of a trippees for a section.
"""
model = IncomingStudent
template_name = 'trips/person_checklist.html'
header_text = 'Trippee'
def get_queryset(self):
qs = super(TrippeeChecklist, self).get_queryset()
return qs.filter(trip_assignment__section=self.get_section())
[docs]class LeaderChecklist(_SectionMixin, DatabaseListView):
"""
All leaders for a section.
"""
model = GeneralApplication
template_name = 'trips/person_checklist.html'
header_text = 'Leader'
def get_queryset(self):
qs = super(LeaderChecklist, self).get_queryset()
return qs.filter(assigned_trip__section=self.get_section())
[docs]class Checklists(DatabaseTemplateView):
"""
Central list of all checklists"
"""
template_name = 'trips/checklists.html'
def extra_context(self):
trips_year = self.kwargs['trips_year']
dates = Section.dates.leader_dates(trips_year)
d = OrderedDict([date, []] for date in dates)
for sxn in Section.objects.filter(trips_year=trips_year):
kwargs = {
'trips_year': trips_year,
'section_pk': sxn.pk
}
d[sxn.leaders_arrive].append((
'Section %s Leader Checkin' % sxn.name,
reverse('db:checklists:leaders', kwargs=kwargs)))
d[sxn.trippees_arrive].append((
'Section %s Trippee Checkin' % sxn.name,
reverse('db:checklists:trippees', kwargs=kwargs)))
d[sxn.trippees_arrive].append((
'Section %s Leader Packets' % sxn.name,
reverse('db:packets:section', kwargs=kwargs)))
d[sxn.trippees_arrive].append((
'Section %s Medical Information' % sxn.name,
reverse('db:packets:medical', kwargs=kwargs)))
buses = ScheduledTransport.objects.filter(trips_year=trips_year)
for date in set(map(lambda x: x.date, buses)):
d[date].append((
'Internal Bus Directions for %s' % date.strftime('%m/%d'),
reverse('db:internal_packet_for_date', kwargs={
'trips_year': trips_year, 'date': date
})
))
buses = ExternalBus.objects.filter(trips_year=trips_year)
route_dict = defaultdict(set)
for bus in buses:
route_dict[bus.date_to_hanover].add(bus.route)
route_dict[bus.date_from_hanover].add(bus.route)
for date, routes in route_dict.items():
for route in routes:
d[date].append((
'%s Directions for %s' % (route, date.strftime('%m/%d')),
reverse('db:external_packet_for_date_and_route', kwargs={
'trips_year': trips_year, 'date': date, 'route_pk': route.pk
})
))
return {'date_dict': d}