import math
from datetime import timedelta
from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Avg, Count
from django.forms.models import inlineformset_factory
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.template.loader import render_to_string
from django.urls.base import reverse
from django.utils import timezone
from django.utils.text import slugify
from django.views.generic import CreateView, DeleteView, UpdateView
from django.views.decorators.http import require_GET, require_POST
from reversion.models import Version
from accounts.models import UserPreferences
from emails.generators import (ReportReminderEmailGenerator, EventEmailGenerator, BillingEmailGenerator,
DefaultLNLEmailGenerator as DLEG, send_survey_if_necessary)
from slack.views import cc_report_reminder
from slack.api import lookup_user, slack_post
from events.forms import (
AttachmentForm, BillingForm, BillingUpdateForm, MultiBillingForm,
MultiBillingUpdateForm, CCIForm, CrewAssign, EventApprovalForm,
EventDenialForm, EventReviewForm, ExtraForm, InternalReportForm, MKHoursForm,
BillingEmailForm, MultiBillingEmailForm, ServiceInstanceForm, WorkdayForm, CrewCheckinForm,
CrewCheckoutForm, CheckoutHoursForm, BulkCheckinForm
)
from events.models import (BaseEvent, Billing, MultiBilling, BillingEmail, MultiBillingEmail, Category, CCReport, Event,
Event2019, EventArbitrary, EventAttachment, EventCCInstance, ExtraInstance, Hours,
ReportReminder, ServiceInstance, PostEventSurvey, CCR_DELTA, CrewAttendanceRecord)
from helpers.mixins import (ConditionalFormMixin, HasPermMixin, HasPermOrTestMixin,
LoginRequiredMixin, SetFormMsgMixin)
from helpers.challenges import is_officer
from helpers.revision import set_revision_comment
from helpers.util import curry_class
from pdfs.views import (generate_pdfs_standalone, generate_event_bill_pdf_standalone,
generate_multibill_pdf_standalone)
from ..cal import generate_ics
[docs]@login_required
@permission_required('events.approve_event', raise_exception=True)
def approval(request, id):
""" Approve an event (agree to provide services for the event) """
context = {'msg': "Approve Event"}
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.approve_event', event):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.approved:
messages.add_message(request, messages.INFO, 'Event has already been approved!')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
is_event2019 = isinstance(event, Event2019)
context['is_event2019'] = is_event2019
if is_event2019:
mk_serviceinstance_formset = inlineformset_factory(BaseEvent, ServiceInstance, extra=3, exclude=[])
mk_serviceinstance_formset.form = curry_class(ServiceInstanceForm, event=event)
if request.method == 'POST':
form = EventApprovalForm(request.POST, instance=event)
if is_event2019:
services_formset = mk_serviceinstance_formset(request.POST, request.FILES, instance=event)
if form.is_valid() and (not is_event2019 or services_formset.is_valid()):
e = form.save(commit=False)
e.approved = True
e.approved_on = timezone.now()
e.approved_by = request.user
e.save()
form.save_m2m()
if is_event2019:
services_formset.save()
# Automatically add the event contact to the client (if the event has only one client)
if e.contact is not None and e.org.count() == 1 and e.contact not in e.org.get().associated_users.all():
set_revision_comment("Approved {}. Event contact {} automatically added to {}.".format(
e.event_name, e.contact, e.org.get()), form)
e.org.get().associated_users.add(e.contact)
else:
set_revision_comment("Approved", form)
# confirm with user and notify VP
messages.add_message(request, messages.INFO, 'Approved Event: No longer auto notifying clients. Please give them the good news!')
email_body = '"%s" has been approved!' % event.event_name
email = DLEG(subject="Event Approved", to_emails=[settings.EMAIL_TARGET_VP_DB], body=email_body)
email.send()
return HttpResponseRedirect(reverse('events:detail', args=(e.id,)))
else:
context['form'] = form
if is_event2019:
context['services_formset'] = services_formset
else:
# has a bill, but no paid bills, and is not otherwise closed
unbilled_events = Event.objects.filter(org__in=event.org.all())\
.exclude(billings__date_paid__isnull=False)\
.filter(billings__date_billed__isnull=False)\
.filter(closed=False)\
.filter(cancelled=False)\
.filter(test_event=False)\
.distinct()
unbilled_events = map(str, unbilled_events)
if event.org.exists() and unbilled_events:
messages.add_message(request, messages.WARNING, "Organization has unbilled events: %s" % ", ".join(
unbilled_events))
for org in event.org.filter(delinquent=True):
messages.add_message(request, messages.WARNING, "The client '%s' has been marked as delinquent. \
This means that the client has one or more long-outstanding bills which they should be required to \
pay before you approve this event." % org)
context['form'] = EventApprovalForm(instance=event)
if is_event2019:
context['services_formset'] = mk_serviceinstance_formset(instance=event)
return render(request, 'form_crispy_approval.html', context)
[docs]@login_required
def denial(request, id):
""" Deny an incoming event (LNL will not provide services for the event) """
context = {'msg': "Deny Event"}
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.decline_event', event):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.cancelled:
messages.add_message(request, messages.INFO, 'Event has already been cancelled!')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if request.method == 'POST':
form = EventDenialForm(request.POST, instance=event)
if form.is_valid():
set_revision_comment("Denied", form)
e = form.save(commit=False)
e.cancelled = True
e.cancelled_by = request.user
e.cancelled_on = timezone.now()
e.closed = True
e.closed_by = request.user
e.closed_on = timezone.now()
e.save()
# confirm with user
messages.add_message(request, messages.INFO, 'Denied Event')
if e.contact and e.contact.email and form.cleaned_data['send_email']:
email_body = 'Sorry, but your event "%s" has been denied. \n Reason: "%s"' % (
event.event_name, event.cancelled_reason)
email = DLEG(subject="Event Denied", to_emails=[e.contact.email], body=email_body,
bcc=[settings.EMAIL_TARGET_VP_DB])
email.send()
elif (not e.contact or not e.contact.email):
messages.add_message(request, messages.INFO,
'Event Denied. No contact info on file for denial. Please give them the bad news.')
else:
messages.add_message(request, messages.INFO, 'Event Denied. No email sent.')
return HttpResponseRedirect(reverse('events:detail', args=(e.id,)))
form = EventDenialForm(instance=event)
context['form'] = form
return render(request, 'form_crispy.html', context)
[docs]@login_required
def review(request, id):
""" Review an event for billing """
context = {'h2': "Review Event for Billing"}
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.review_event', event):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.reviewed:
messages.add_message(request, messages.INFO, 'Event has already been reviewed!')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
if request.method == 'POST':
form = EventReviewForm(request.POST, instance=event, event=event)
if form.is_valid():
set_revision_comment("Reviewed for billing", form)
e = form.save(commit=False)
e.reviewed = True
e.reviewed_on = timezone.now()
e.reviewed_by = request.user
e.save()
form.save_m2m()
# Remove prefilled hours that were never finished
Hours.objects.filter(event=e, hours__isnull=True).delete()
# Close any "active" crew member records for this event
if isinstance(e, Event2019):
e.crew_attendance.filter(active=True).update(active=False)
# confirm with user
messages.add_message(request, messages.INFO, 'Event has been reviewed and is ready for billing!')
return HttpResponseRedirect(reverse('events:detail', args=(e.id,)) + "#billing")
else:
context['formset'] = form
else:
form = EventReviewForm(instance=event, event=event)
context['formset'] = form
return render(request, 'event_review.html', context)
[docs]@login_required
def reviewremind(request, id, uid):
"""
Remind a crew chief to complete their CC report
:param id: The primary key value of the respective event
:param uid: The primary key value of the user to send a reminder to
"""
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.review_event') or
request.user.has_perm('events.review_event', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.reviewed or not event.approved:
messages.add_message(request, messages.ERROR, 'You can only send reminders for an event that is approved and '
'has not yet been reviewed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
cci = event.ccinstances.filter(crew_chief_id=uid).first()
if cci:
# only do heavy lifting if we need to
prefs, created = UserPreferences.objects.get_or_create(user=cci.crew_chief)
send_notification = True
if cci.crew_chief == request.user and prefs.ignore_user_action:
send_notification = False
if send_notification and prefs.cc_report_reminders in ['email', 'all']:
pdf_handle = generate_pdfs_standalone([event.id])
filename = "%s.workorder.pdf" % slugify(event.event_name)
attachments = [{"file_handle": pdf_handle, "name": filename}]
reminder = ReportReminder.objects.create(event=cci.event, crew_chief=cci.crew_chief)
email = ReportReminderEmailGenerator(reminder=reminder, attachments=attachments)
email.send()
if send_notification and prefs.cc_report_reminders in ['slack', 'all']:
message = "This is a reminder that you have a pending crew chief report for %s." % event.event_name
blocks = cc_report_reminder(cci)
slack_user = lookup_user(cci.crew_chief.email)
if slack_user:
slack_post(slack_user, text=message, content=blocks)
messages.add_message(request, messages.INFO, 'Reminder Sent')
return HttpResponseRedirect(reverse("events:review", args=(event.id,)))
else:
return HttpResponse("Bad Call")
[docs]@login_required
def remindall(request, id):
""" Remind any crew chiefs who have not yet completed a CC report for a particular event to do so """
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.review_event') or
request.user.has_perm('events.review_event', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.reviewed or not event.approved:
messages.add_message(request, messages.ERROR, 'Can only send reminders for an event that is approved and not '
'reviewed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.num_crew_needing_reports == 0:
messages.add_message(request, messages.INFO, 'All crew chiefs have already submitted reports.')
return HttpResponseRedirect(reverse("events:review", args=(event.id,)))
pdf_handle = generate_pdfs_standalone([event.id])
filename = "%s.workorder.pdf" % slugify(event.event_name)
attachments = [{"file_handle": pdf_handle, "name": filename}]
for cci in event.crew_needing_reports:
prefs, created = UserPreferences.objects.get_or_create(user=cci.crew_chief)
if cci.crew_chief == request.user and prefs.ignore_user_action:
continue
if prefs.cc_report_reminders in ['email', 'all']:
reminder = ReportReminder.objects.create(event=event, crew_chief=cci.crew_chief)
email = ReportReminderEmailGenerator(reminder=reminder, attachments=attachments)
email.send()
if prefs.cc_report_reminders in ['slack', 'all']:
message = "This is a reminder that you have a pending crew chief report for %s." % event.event_name
blocks = cc_report_reminder(cci)
slack_user = lookup_user(cci.crew_chief.email)
if slack_user:
slack_post(slack_user, text=message, content=blocks)
messages.add_message(request, messages.INFO, 'Reminders sent to all crew chiefs needing reports for %s' %
event.event_name)
if 'next' in request.GET:
return HttpResponseRedirect(request.GET['next'])
else:
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
[docs]@login_required
@require_POST
def close(request, id):
""" Close an event (not to be confused with cancel). POST only. """
set_revision_comment("Closed", None)
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.close_event', event):
raise PermissionDenied
event.closed = True
event.closed_by = request.user
event.closed_on = timezone.now()
event.save()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
[docs]@login_required
@require_POST
def cancel(request, id):
""" Cancel an event (LNL will no longer provide services for this event). POST only. """
set_revision_comment("Cancelled", None)
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.cancel_event', event):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
event.cancelled = True
event.cancelled_by = request.user
event.cancelled_on = timezone.now()
event.save()
if request.POST.get('notify', '') == "on":
if event.contact and event.contact.email:
targets = [event.contact.email]
email_body = 'The event "%s" has been cancelled by %s. If this is incorrect, please contact our vice president ' \
'at lnl-vp@wpi.edu.' % (event.event_name, str(request.user))
if request.user.email:
email_body = email_body[:-1]
email_body += " or try them at %s." % request.user.email
email = DLEG(subject="Event Cancelled", to_emails=targets, body=email_body, bcc=[settings.EMAIL_TARGET_VP])
email.send()
messages.add_message(request, messages.INFO, 'Event Closed and Client Notification Sent')
else:
messages.add_message(request, messages.INFO, 'Event Closed. Client email not found. Client has not been notified.')
else:
messages.add_message(request, messages.INFO, 'Event Closed. Client has not been notified.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
[docs]@login_required
@require_POST
def reopen(request, id):
""" Reopen an event that has already been closed. POST only. """
set_revision_comment("Reopened", None)
event = get_object_or_404(BaseEvent, pk=id)
if not request.user.has_perm('events.reopen_event', event):
raise PermissionDenied
event.closed = False
event.closed_by = None
event.closed_on = None
event.save()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
[docs]@login_required
def rmcrew(request, id, user):
"""
Remove a user from the list of crew members who attended an event (pre-2019 events only)
:param id: The primary key value of the event
:param user: The primary key value of the user
"""
event = get_object_or_404(Event, pk=id)
if not (request.user.has_perm('events.edit_event_hours') or
request.user.has_perm('events.edit_event_hours', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
event.crew.remove(user)
return HttpResponseRedirect(reverse("events:add-crew", args=(event.id,)))
[docs]@login_required
def assigncrew(request, id):
"""
- Pre-2019 Events: Assign crew members to an event
- Newer Events: Redirect to crew list
"""
context = {'msg': "Crew"}
event = get_object_or_404(BaseEvent, pk=id)
# The new events model doesn't really use this anymore
if isinstance(event, Event2019):
return HttpResponseRedirect(reverse('events:detail', args=[event.id]) + "#crew")
if not (request.user.has_perm('events.edit_event_hours') or
request.user.has_perm('events.edit_event_hours', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
if request.method == 'POST':
formset = CrewAssign(request.POST, instance=event)
if formset.is_valid():
formset.save()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
else:
formset = CrewAssign(instance=event)
context['formset'] = formset
return render(request, 'form_crew_add.html', context)
[docs]@login_required
def hours_bulk_admin(request, id):
""" Update crew member hours in bulk """
context = {'msg': "Bulk Hours Entry"}
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.edit_event_hours') or
request.user.has_perm('events.edit_event_hours', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
context['oldevent'] = isinstance(event, Event)
mk_hours_formset = inlineformset_factory(BaseEvent, Hours, extra=3, exclude=[])
mk_hours_formset.form = curry_class(MKHoursForm, event=event)
if request.method == 'POST':
formset = mk_hours_formset(request.POST, instance=event)
if formset.is_valid():
formset.save()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
else:
formset = mk_hours_formset(instance=event)
context['formset'] = formset
return render(request, 'formset_hours_bulk.html', context)
[docs]@login_required
@require_GET
def hours_prefill_self(request, id):
"""
Log the current user as a crew member for an event. This only works after setup begins and will continue to work up
until 3 hours after the event. GET requests only.
"""
event = get_object_or_404(BaseEvent, pk=id)
if not event.ccinstances.exists():
raise PermissionDenied
if event.hours.filter(user=request.user).exists():
messages.add_message(request, messages.ERROR, 'You already have hours for this event.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if timezone.now() < min(event.ccinstances.values_list('setup_start', flat=True)):
messages.add_message(request, messages.ERROR, 'You cannot use this feature until the event setup has started.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if timezone.now() > event.datetime_end + timedelta(hours=3):
messages.add_message(request, messages.ERROR, 'This feature is disabled 3 hours after the event end time.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
Hours.objects.create(event=event, user=request.user)
messages.add_message(request, messages.SUCCESS, 'You have been added as crew for this event.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
[docs]@login_required
@require_GET
def crew_tracker(request):
""" Event checkin / checkout menu for crew members. GET requests only. """
context = {'NO_FOOT': True, 'NO_NAV': True, 'NO_API': True, 'LIGHT_THEME': True}
return render(request, 'crew_checkin.html', context)
[docs]@login_required
def checkin(request):
""" Event checkin page for crew members """
context = {'NO_FOOT': True, 'NO_NAV': True, 'NO_API': True, 'LIGHT_THEME': True}
if not request.user.is_lnl:
raise PermissionDenied
events = Event2019.objects.filter(
sensitive=False, closed=False, ccinstances__setup_start__lte=timezone.now(),
datetime_end__gt=timezone.now() + timezone.timedelta(hours=-5), cancelled=False).distinct()
for event in events:
if event.max_crew and event.crew_attendance.filter(active=True).count() == event.max_crew:
events = events.exclude(pk=event.pk)
if events.count() == 0:
context['page'] = {"title": "There are currently no events available for checkin",
"body": "<a href='" + reverse("events:crew-tracker") + "' class='btn btn-primary'>Back</a>"}
return render(request, 'static_page.html', context)
if request.user.event_records.filter(active=True).exists():
context['page'] = {"title": "You are already checked into an event",
"body": "<a href='" + reverse("events:crew-tracker") + "' class='btn btn-primary'>Back</a>"}
return render(request, 'static_page.html', context)
if request.method == 'POST':
form = CrewCheckinForm(request.POST, events=events, title="Checkin")
if form.is_valid():
event = Event2019.objects.get(pk=request.POST['event'])
event.crew_attendance.create(user=request.user)
if not event.hours.filter(user=request.user).exists():
event.hours.create(user=request.user)
name = request.user.nickname
if not name:
name = request.user.first_name
messages.success(request, "Welcome, %s. You have checked in successfully!" % name, extra_tags="success")
return HttpResponseRedirect(reverse("events:detail", args=[event.pk]))
else:
form = CrewCheckinForm(events=events, title="Checkin")
context['form'] = form
return render(request, 'form_crispy_static.html', context)
[docs]@login_required
def checkout(request):
""" Event checkout page for crew members """
context = {'NO_FOOT': True, 'NO_NAV': True, 'NO_API': True, 'LIGHT_THEME': True,
'styles': ".control {\n\tpadding: .375rem .75rem;\n\tfont-size: 1rem;\n\tline-height: 1.5;\n\t"
"border-radius: .25rem;\n\tborder: 1px solid #cde4da;\n\tbackground-clip: padding-box;}"}
if not request.user.is_lnl:
raise PermissionDenied
record = CrewAttendanceRecord.objects.filter(user=request.user, active=True).first()
if record is None:
context['page'] = {"title": "Whoops! It appears you haven't checked in yet.",
"body": "<a href='" + reverse("events:crew-tracker") + "' class='btn btn-primary'>Back</a>"}
return render(request, 'static_page.html', context)
categories = []
for category in ServiceInstance.objects.filter(event=record.event).values_list('service__category', flat=True) \
.distinct():
categories.append(Category.objects.get(pk=int(category)))
# Calculate hours worked
if record.checkout:
delta = record.checkout - record.checkin
quarter_hrs = delta.seconds / 900
hours = math.floor(delta.seconds / 3600)
remainder = math.floor(quarter_hrs % 4) * 0.25
total = hours + remainder
if request.method == 'POST':
if not record.checkout:
form = CrewCheckoutForm(request.POST)
if form.is_valid():
checkin_datetime = timezone.make_aware(timezone.datetime.strptime(
"%s%s" % (request.POST['checkin_0'], request.POST['checkin_1']), "%Y-%m-%d%I:%M %p"))
checkout_datetime = timezone.make_aware(timezone.datetime.strptime(
"%s%s" % (request.POST['checkout_0'], request.POST['checkout_1']), "%Y-%m-%d%I:%M %p"))
# Ensure that no one can have a checkin time before setup starts
if checkin_datetime < min(record.event.ccinstances.values_list('setup_start', flat=True)):
checkin_datetime = min(record.event.ccinstances.values_list('setup_start', flat=True))
record.checkin = checkin_datetime
record.checkout = checkout_datetime
record.save()
delta = checkout_datetime - checkin_datetime
quarter_hrs = delta.seconds / 900
hours = math.floor(delta.seconds / 3600)
remainder = math.floor(quarter_hrs % 4) * 0.25
total = hours + remainder
form = CheckoutHoursForm(categories=categories, total_hrs=total)
else:
form = CheckoutHoursForm(request.POST, categories=categories, total_hrs=total)
if form.is_valid():
record.active = False
record.save()
hour_fields = []
# Get the hours fields
for field in request.POST:
if "hour" in field:
hour_fields.append(field)
# If values were entered, save to hours log
for field in hour_fields:
hour_input = request.POST[field]
if hour_input not in [None, ''] and Decimal(hour_input) != 0:
cat = field.replace('hours_', '')
category = Category.objects.get(name=cat)
# Check for pending hour record
placeholder = Hours.objects.filter(category__isnull=True, event=record.event,
user=request.user).first()
if placeholder:
placeholder.category = category
placeholder.hours = 0
placeholder.save()
hour_record, created = Hours.objects.get_or_create(event=record.event, user=request.user,
category=category)
if created:
hour_record.hours = Decimal(hour_input)
else:
hour_record.hours += Decimal(hour_input)
hour_record.save()
messages.success(request, "You have successfully checked out!", extra_tags="success")
return HttpResponseRedirect(reverse("events:detail", args=[record.event.pk]))
else:
if not record.checkout:
form = CrewCheckoutForm(initial={'checkin': record.checkin, 'checkout': timezone.now()})
else:
form = CheckoutHoursForm(categories=categories, total_hrs=total)
context['form'] = form
return render(request, 'form_crispy_static.html', context)
[docs]@login_required
def bulk_checkin(request):
""" Scan or swipe a registered WPI ID to checkin or checkout of an event as a crew member """
context = {'NO_FOOT': True, 'NO_NAV': True, 'NO_API': True, 'LIGHT_THEME': True}
if not request.user.is_lnl:
raise PermissionDenied
event_id = request.GET.get('id', None)
if event_id:
event = get_object_or_404(Event2019, pk=event_id)
if request.method == 'POST':
form = BulkCheckinForm(request.POST, result=None, user_name=None, event=event.event_name)
if form.is_valid():
student_id = request.POST['id']
user = get_user_model().objects.filter(student_id=student_id).first()
name = ""
if not user:
# Could not match user to id
result = "fail"
else:
existing_checkin = CrewAttendanceRecord.objects.filter(user=user, active=True).first()
if existing_checkin and existing_checkin.event != event:
# User is checked in somewhere else
result = "checkin-fail"
else:
# Check for crew limit
if event.max_crew and event.crew_attendance.filter(active=True).count() == event.max_crew and \
not existing_checkin:
result = "checkin-limit"
else:
record, created = CrewAttendanceRecord.objects.get_or_create(
event=event, user=user, active=True
)
if not created:
# Check out
record.checkout = timezone.now()
record.active = False
record.save()
result = "checkout-success"
else:
# Checked in (get user's name to display)
name = user.nickname
if not name:
name = user.first_name
if not event.hours.filter(user=user).exists():
# Record placeholder hour if hours are not already recorded
event.hours.create(user=user)
result = "checkin-success"
form = BulkCheckinForm(result=result, user_name=name, event=event.event_name)
else:
form = BulkCheckinForm(result=None, user_name=None, event=event.event_name)
else:
events = Event2019.objects.filter(
datetime_end__gt=timezone.now() + timezone.timedelta(hours=-5), sensitive=False, closed=False,
cancelled=False, ccinstances__setup_start__lte=timezone.now()).distinct()
# Officers can use this tool even if they aren't a CC for the event
if not is_officer(request.user):
events = events.filter(ccinstances__crew_chief=request.user)
for event in events:
if event.max_crew and event.crew_attendance.filter(active=True).count() == event.max_crew:
events = events.exclude(pk=event.pk)
if events.count() == 0:
context['page'] = {"title": "Hmm. We couldn't find any eligible events.",
"body": "<p>If you are not a crew chief, please use the standard checkin / checkout "
"tools.<br><br></p><a href='" + reverse("events:crew-tracker") +
"' class='btn btn-primary'>Back</a>"}
return render(request, 'static_page.html', context)
if request.method == 'POST':
form = CrewCheckinForm(request.POST, events=events, title="Bulk Checkin")
if form.is_valid():
event = request.POST['event']
return HttpResponseRedirect(reverse("events:crew-bulk") + "?id=" + event)
else:
form = CrewCheckinForm(events=events, title="Bulk Checkin")
context['form'] = form
return render(request, 'form_crispy_static.html', context)
[docs]@login_required
def rmcc(request, id, user):
"""
Remove a crew chief from an event (pre-2019 events only)
:param id: The primary key value of the event
:param user: The primary key value of the user
"""
event = get_object_or_404(Event, pk=id)
if not (request.user.has_perm('events.edit_event_hours') or
request.user.has_perm('events.edit_event_hours', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
event.crew_chief.remove(user)
return HttpResponseRedirect(reverse("events:chiefs", args=(event.id,)))
[docs]@login_required
def assigncc(request, id):
""" Assign crew chiefs to an event """
context = {}
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.edit_event_hours') or
request.user.has_perm('events.edit_event_hours', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
context['oldevent'] = isinstance(event, Event)
cc_formset = inlineformset_factory(Event, EventCCInstance, extra=3, exclude=[])
cc_formset.form = curry_class(CCIForm, event=event)
if request.method == 'POST':
formset = cc_formset(request.POST, instance=event)
if formset.is_valid():
formset.save()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
else:
formset = cc_formset(instance=event)
context['formset'] = formset
return render(request, 'formset_crispy_helpers.html', context)
[docs]@login_required
def assignattach(request, id):
""" Update attachments for an event (file uploads) """
context = {}
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.event_attachments') or
request.user.has_perm('events.event_attachments', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
att_formset = inlineformset_factory(BaseEvent, EventAttachment, extra=2, exclude=[])
att_formset.form = curry_class(AttachmentForm, event=event)
if request.method == 'POST':
set_revision_comment("Edited attachments", None)
formset = att_formset(request.POST, request.FILES, instance=event)
if formset.is_valid():
formset.save()
event.save() # for revision to be created
should_send_email = not event.test_event
if should_send_email:
to = [settings.EMAIL_TARGET_VP_DB]
if hasattr(event, 'projection') and event.projection \
or event.serviceinstance_set.filter(service__category__name='Projection').exists():
to.append(settings.EMAIL_TARGET_HP)
for ccinstance in event.ccinstances.all():
if ccinstance.crew_chief.email:
prefs, created = UserPreferences.objects.get_or_create(user=ccinstance.crew_chief)
if not prefs.ignore_user_action or ccinstance.crew_chief != request.user:
to.append(ccinstance.crew_chief.email)
subject = "Event Attachments"
email_body = "Attachments for the following event were modified by %s." % request.user.get_full_name()
email = EventEmailGenerator(event=event, subject=subject, to_emails=to, body=email_body)
email.send()
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
else:
formset = att_formset(instance=event)
context['formset'] = formset
return render(request, 'formset_crispy_attachments.html', context)
[docs]@login_required
def assignattach_external(request, id):
""" Update attachments for an event (client-view) """
context = {}
event = get_object_or_404(BaseEvent, pk=id)
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
context['event'] = event
mk_att_formset = inlineformset_factory(Event, EventAttachment, extra=1, exclude=[])
# mk_att_formset.queryset = mk_att_formset.queryset.filter(externally_uploaded=True)
mk_att_formset.form = curry_class(AttachmentForm, event=event, externally_uploaded=True)
if request.method == 'POST':
set_revision_comment("Edited attachments", None)
formset = mk_att_formset(request.POST, request.FILES, instance=event,
queryset=EventAttachment.objects.filter(externally_uploaded=True))
if formset.is_valid():
f = formset.save(commit=False)
for i in f:
i.externally_uploaded = True
i.save()
event.save() # for revision to be created
return HttpResponseRedirect(reverse('my:workorders', ))
else:
formset = mk_att_formset(instance=event, queryset=EventAttachment.objects.filter(externally_uploaded=True))
context['formset'] = formset
return render(request, 'formset_crispy_attachments.html', context)
[docs]@login_required
def download_ics(request, pk):
""" Generate and download an ics file """
event = get_object_or_404(BaseEvent, pk=pk)
invite = generate_ics([event], None)
response = HttpResponse(invite, content_type="text/calendar")
response['Content-Disposition'] = "attachment; filename=event.ics"
return response
[docs]@login_required
def oneoff(request, id):
""" This form is for adding oneoff fees to an event """
context = {'msg': "One-Off Charges"}
event = get_object_or_404(BaseEvent, pk=id)
context['event'] = event
if not (request.user.has_perm('events.adjust_event_charges') or
request.user.has_perm('events.adjust_event_charges', event)):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
mk_oneoff_formset = inlineformset_factory(BaseEvent, EventArbitrary, extra=3, exclude=[])
if request.method == 'POST':
set_revision_comment("Edited billing charges", None)
formset = mk_oneoff_formset(request.POST, request.FILES, instance=event)
if formset.is_valid():
formset.save()
event.save() # for revision to be created
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)) + "#billing")
else:
formset = mk_oneoff_formset(instance=event)
context['formset'] = formset
return render(request, 'formset_crispy_arbitrary.html', context)
[docs]@login_required
def viewevent(request, id):
""" View event details """
context = {}
event = get_object_or_404(BaseEvent, pk=id)
if not (request.user.has_perm('events.view_events') or request.user.has_perm('events.view_events', event)):
raise PermissionDenied
if event.sensitive and not request.user.has_perm('events.view_hidden_event', event):
raise PermissionDenied
send_survey_if_necessary(event)
context['event'] = event
# do not use .get_unique() because it does not follow relations
context['history'] = Version.objects.get_for_object(event)
if isinstance(event, Event2019):
context['crew_count'] = event.crew_attendance.filter(active=True).values('user').count()
if event.serviceinstance_set.exists():
context['categorized_services_and_extras'] = {}
for category in Category.objects.all():
# tuple (serviceinstances, extrainstances)
services_and_extras = (
list(event.serviceinstance_set.filter(service__category=category)),
list(event.extrainstance_set.filter(extra__category=category))
)
# do not add category if it is empty
if services_and_extras[0] or services_and_extras[1]:
context['categorized_services_and_extras'][category.name] = services_and_extras
if event.surveys.exists() and request.user.has_perm('events.view_posteventsurveyresults', event.surveys.all().first()):
context['survey_takers'] = get_user_model().objects.filter(pk__in=event.surveys.values_list('person', flat=True))
work_order_method_choices = dict(PostEventSurvey._meta.get_field('work_order_method').choices)
context['survey_workorder_methods'] = [work_order_method_choices[choice] for choice in event.surveys.values_list('work_order_method', flat=True)]
context['survey_workorder_comments'] = [c for c in event.surveys.values_list('work_order_comments', flat=True) if c != '']
context['survey_comments'] = [c for c in event.surveys.values_list('comments', flat=True) if c != '']
context['survey_results'] = {}
context['survey_results'].update(event.surveys.filter(services_quality__gte=0).aggregate(
Avg('services_quality'),
Count('services_quality'),
))
context['survey_results']['services_quality__verbose_name'] = PostEventSurvey._meta.get_field('services_quality').verbose_name
context['survey_results'].update(event.surveys.filter(lighting_quality__gte=0).aggregate(
Avg('lighting_quality'),
Count('lighting_quality'),
))
context['survey_results']['lighting_quality__verbose_name'] = PostEventSurvey._meta.get_field('lighting_quality').verbose_name
context['survey_results'].update(event.surveys.filter(sound_quality__gte=0).aggregate(
Avg('sound_quality'),
Count('sound_quality'),
))
context['survey_results']['sound_quality__verbose_name'] = PostEventSurvey._meta.get_field('sound_quality').verbose_name
context['survey_results'].update(event.surveys.filter(work_order_experience__gte=0).aggregate(
Avg('work_order_experience'),
Count('work_order_experience'),
))
context['survey_results']['work_order_experience__verbose_name'] = PostEventSurvey._meta.get_field('work_order_experience').verbose_name
context['survey_results'].update(event.surveys.filter(work_order_ease__gte=0).aggregate(
Avg('work_order_ease'),
Count('work_order_ease'),
))
context['survey_results']['work_order_ease__verbose_name'] = PostEventSurvey._meta.get_field('work_order_ease').verbose_name
context['survey_results'].update(event.surveys.filter(communication_responsiveness__gte=0).aggregate(
Avg('communication_responsiveness'),
Count('communication_responsiveness'),
))
context['survey_results']['communication_responsiveness__verbose_name'] = PostEventSurvey._meta.get_field('communication_responsiveness').verbose_name
context['survey_results'].update(event.surveys.filter(pricelist_ux__gte=0).aggregate(
Avg('pricelist_ux'),
Count('pricelist_ux'),
))
context['survey_results']['pricelist_ux__verbose_name'] = PostEventSurvey._meta.get_field('pricelist_ux').verbose_name
context['survey_results'].update(event.surveys.filter(setup_on_time__gte=0).aggregate(
Avg('setup_on_time'),
Count('setup_on_time'),
))
context['survey_results']['setup_on_time__verbose_name'] = PostEventSurvey._meta.get_field('setup_on_time').verbose_name
context['survey_results'].update(event.surveys.filter(crew_respectfulness__gte=0).aggregate(
Avg('crew_respectfulness'),
Count('crew_respectfulness'),
))
context['survey_results']['crew_respectfulness__verbose_name'] = PostEventSurvey._meta.get_field('crew_respectfulness').verbose_name
context['survey_results'].update(event.surveys.filter(price_appropriate__gte=0).aggregate(
Avg('price_appropriate'),
Count('price_appropriate'),
))
context['survey_results']['price_appropriate__verbose_name'] = PostEventSurvey._meta.get_field('price_appropriate').verbose_name
context['survey_results'].update(event.surveys.filter(customer_would_return__gte=0).aggregate(
Avg('customer_would_return'),
Count('customer_would_return'),
))
context['survey_results']['customer_would_return__verbose_name'] = PostEventSurvey._meta.get_field('customer_would_return').verbose_name
context['survey_composites'] = {}
try:
context['survey_composites']['vp'] = context['survey_results']['communication_responsiveness__avg']
except TypeError:
context['survey_composites']['vp'] = None
crew_avg = 0
crew_count = 4
try:
crew_avg += context['survey_results']['setup_on_time__avg']
except TypeError:
crew_count -= 1
try:
crew_avg += context['survey_results']['crew_respectfulness__avg']
except TypeError:
crew_count -= 1
try:
crew_avg += context['survey_results']['lighting_quality__avg']
except TypeError:
crew_count -= 1
try:
crew_avg += context['survey_results']['sound_quality__avg']
except TypeError:
crew_count -= 1
if crew_count == 0:
context['survey_composites']['crew'] = None
else:
context['survey_composites']['crew'] = crew_avg / crew_count
price_avg = 0
price_count = 2
try:
price_avg += context['survey_results']['pricelist_ux__avg']
except TypeError:
price_count -= 1
try:
price_avg += context['survey_results']['price_appropriate__avg']
except TypeError:
price_count -= 1
if price_count == 0:
context['survey_composites']['pricelist'] = None
else:
context['survey_composites']['pricelist'] = price_avg / price_count
overall_avg = 0
overall_count = 2
try:
overall_avg += context['survey_results']['services_quality__avg']
except TypeError:
overall_count -= 1
try:
overall_avg += context['survey_results']['customer_would_return__avg']
except TypeError:
overall_count -= 1
if overall_count == 0:
context['survey_composites']['overall'] = None
else:
context['survey_composites']['overall'] = overall_avg / overall_count
# Determine which apps will be visible
apps = []
if isinstance(event, Event2019):
# Spotify
if event.approved and request.user.has_perm('spotify.view_session'):
if not event.session_set.first() and not event.reviewed and not event.cancelled and not event.closed \
and (request.user in [cc.crew_chief for cc in event.ccinstances.all()] or
request.user.has_perm('spotify.add_session')):
apps.append('spotify')
elif event.session_set.first():
apps.append('spotify')
# Automatically stop accepting song requests if event is no longer eligible
if event.session_set.first():
session = event.session_set.first()
if session.accepting_requests and (not event.approved or event.reviewed or event.cancelled or event.closed):
session.accepting_requests = False
session.save()
context['apps'] = apps
return render(request, 'uglydetail.html', context)
[docs]class CCRCreate(SetFormMsgMixin, HasPermOrTestMixin, ConditionalFormMixin, LoginRequiredMixin, CreateView):
""" Create a new crew chief report """
model = CCReport
form_class = InternalReportForm
template_name = "form_crispy_cbv.html"
msg = "New Crew Chief Report"
perms = 'events.add_event_report'
[docs] def dispatch(self, request, *args, **kwargs):
self.event = get_object_or_404(BaseEvent, pk=kwargs['event'])
return super(CCRCreate, self).dispatch(request, *args, **kwargs)
[docs] def user_passes_test(self, request, *args, **kwargs):
return request.user.has_perm(self.perms, self.event)
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.kwargs['event'],))
[docs]class CCRUpdate(SetFormMsgMixin, HasPermOrTestMixin, ConditionalFormMixin, LoginRequiredMixin, UpdateView):
""" Update an existing crew chief report """
model = CCReport
form_class = InternalReportForm
template_name = "form_crispy_cbv.html"
msg = "Update Crew Chief Report"
perms = 'events.change_ccreport'
[docs] def user_passes_test(self, request, *args, **kwargs):
obj = self.get_object()
return obj.event.reports_editable and request.user.has_perm(self.perms, obj)
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.kwargs['event'],))
[docs]class CCRDelete(SetFormMsgMixin, HasPermOrTestMixin, LoginRequiredMixin, DeleteView):
""" Delete a crew chief report """
model = CCReport
template_name = "form_delete_cbv.html"
msg = "Delete Crew Chief Report"
perms = 'events.delete_ccreport'
[docs] def user_passes_test(self, request, *args, **kwargs):
obj = self.get_object()
return obj.event.reports_editable and request.user.has_perm(self.perms, obj)
[docs] def get_object(self, queryset=None):
""" Hook to ensure object isn't closed """
obj = super(CCRDelete, self).get_object()
if obj.event.closed:
raise ValidationError("Event is closed")
else:
return obj
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.kwargs['event'],))
[docs]class BillingCreate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, CreateView):
model = Billing
form_class = BillingForm
template_name = "form_crispy_cbv.html"
msg = "New Bill"
perms = 'events.bill_event'
[docs] def dispatch(self, request, *args, **kwargs):
self.event = get_object_or_404(BaseEvent, pk=self.kwargs['event'])
if self.event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(self.kwargs['event'],)))
return super(BillingCreate, self).dispatch(request, *args, **kwargs)
[docs] def get_context_data(self, **kwargs):
context = super(BillingCreate, self).get_context_data(**kwargs)
if self.event.closed:
raise PermissionDenied
orgs = self.event.org.all()
orgstrings = ",".join(["%s's billing account was last verified: %s" % (
o.name, o.verifications.latest().date if o.verifications.exists() else "Never") for o in orgs])
context['extra'] = orgstrings
return context
[docs] def get_success_url(self):
if 'save-and-make-email' in self.request.POST:
return reverse("events:bills:email", args=(self.kwargs['event'], self.object.pk))
else:
return reverse("events:detail", args=(self.kwargs['event'],)) + "#billing"
[docs]class BillingUpdate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, UpdateView):
model = Billing
form_class = BillingUpdateForm
template_name = "form_crispy_cbv.html"
msg = "Update Bill"
perms = 'events.bill_event'
[docs] def dispatch(self, request, *args, **kwargs):
self.event = get_object_or_404(BaseEvent, pk=self.kwargs['event'])
if self.event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(self.kwargs['event'],)))
return super(BillingUpdate, self).dispatch(request, *args, **kwargs)
[docs] def get_object(self, *args, **kwargs):
""" Validate preconditions for editing a bill """
obj = super(BillingUpdate, self).get_object(*args, **kwargs)
if obj.event.closed:
raise PermissionDenied
else:
return obj
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.kwargs['event'],)) + "#billing"
[docs]class BillingDelete(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, DeleteView):
model = Billing
template_name = "form_delete_cbv.html"
msg = "Delete Bill"
perms = 'events.bill_event'
[docs] def dispatch(self, request, *args, **kwargs):
self.event = get_object_or_404(BaseEvent, pk=self.kwargs['event'])
if self.event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(self.kwargs['event'],)))
return super(BillingDelete, self).dispatch(request, *args, **kwargs)
[docs] def get_object(self, *args, **kwargs):
""" Validate preconditions for deleting a bill """
obj = super(BillingDelete, self).get_object(*args, **kwargs)
if obj.date_paid:
raise PermissionDenied
if obj.event.closed:
raise PermissionDenied
else:
return obj
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.kwargs['event'],)) + "#billing"
[docs]class MultiBillingCreate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, CreateView):
model = MultiBilling
form_class = MultiBillingForm
template_name = "form_crispy.html"
msg = "New MultiBill"
perms = 'events.bill_event'
[docs] def get_success_url(self):
if 'save-and-make-email' in self.request.POST:
return reverse("events:multibillings:email", args=(self.object.pk,))
else:
return reverse("events:multibillings:list")
[docs]class MultiBillingUpdate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, UpdateView):
model = MultiBilling
form_class = MultiBillingUpdateForm
template_name = "form_crispy.html"
msg = "Update MultiBill"
perms = 'events.bill_event'
[docs] def get_object(self, *args, **kwargs):
""" Validate preconditions for editing a multibill """
obj = super(MultiBillingUpdate, self).get_object(*args, **kwargs)
if obj.events.filter(closed=True).exists():
raise PermissionDenied
else:
return obj
[docs] def get_success_url(self):
return reverse("events:multibillings:list")
[docs]class MultiBillingDelete(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, DeleteView):
model = MultiBilling
template_name = "form_delete_cbv.html"
msg = "Delete MultiBill"
perms = 'events.bill_event'
[docs] def get_object(self, *args, **kwargs):
""" Validate preconditions for deleting a multibill """
obj = super(MultiBillingDelete, self).get_object(*args, **kwargs)
if obj.date_paid:
raise PermissionDenied
if obj.events.filter(closed=True).exists():
raise PermissionDenied
else:
return obj
[docs] def get_success_url(self):
return reverse("events:multibillings:list")
[docs]@require_POST
@login_required
def pay_bill(request, event, pk):
"""
Quietly pays a bill, showing a message on the next page. POST only.
"""
bill = get_object_or_404(Billing, event_id=event, id=pk)
if not request.user.has_perm('events.bill_event', bill.event):
raise PermissionDenied
if bill.event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(bill.event_id,)))
if bill.date_paid:
messages.info(request, "Bill has already been paid", extra_tags="info")
else:
bill.date_paid = timezone.now()
bill.save()
if 'next' in request.GET:
messages.success(request, "Marked latest bill paid for %s" % bill.event.event_name, extra_tags="success")
else:
messages.success(request, "Bill paid!", extra_tags="success")
if 'next' in request.GET:
return HttpResponseRedirect(request.GET['next'])
else:
return HttpResponseRedirect(reverse('events:detail', args=(bill.event_id,)) + "#billing")
[docs]class BillingEmailCreate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, CreateView):
model = BillingEmail
form_class = BillingEmailForm
template_name = "form_crispy.html"
msg = "New Billing Email"
perms = 'events.bill_event'
[docs] def dispatch(self, request, *args, **kwargs):
self.billing = get_object_or_404(Billing, pk=self.kwargs['billing'])
return super(BillingEmailCreate, self).dispatch(request, *args, **kwargs)
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.billing.event_id,)) + "#billing"
[docs]class MultiBillingEmailCreate(SetFormMsgMixin, HasPermMixin, LoginRequiredMixin, CreateView):
model = MultiBillingEmail
form_class = MultiBillingEmailForm
template_name = "form_crispy.html"
msg = "New Billing Email"
perms = 'events.bill_event'
[docs] def dispatch(self, request, *args, **kwargs):
self.multibilling = get_object_or_404(MultiBilling, pk=self.kwargs['multibilling'])
return super(MultiBillingEmailCreate, self).dispatch(request, *args, **kwargs)
[docs] def get_success_url(self):
return reverse("events:multibillings:list")
[docs]class WorkdayEntry(HasPermOrTestMixin, LoginRequiredMixin, UpdateView):
model = Event2019
form_class = WorkdayForm
template_name = "form_crispy_workday.html"
perms = 'events.edit_org_billing'
[docs] def user_passes_test(self, request, *args, **kwargs):
obj = self.get_object()
return request.user.has_perm(self.perms, obj.org_to_be_billed) or request.GET.get('verification') == obj.workday_form_hash
[docs] def get_initial(self):
initial = self.initial
obj = self.get_object()
org = obj.org_to_be_billed
if obj.workday_fund is None:
initial['workday_fund'] = org.workday_fund
if obj.worktag is None:
initial['worktag'] = org.worktag
return initial
[docs] def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
is_update = self.object.workday_fund is not None or self.object.worktag is not None
if self.object.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(self.object.pk,)))
if not self.object.reviewed:
messages.add_message(request, messages.ERROR, 'Event has not been reviewed for billing.')
return HttpResponseRedirect(reverse('events:detail', args=(self.object.pk,)))
if not self.object.billings.exists():
messages.add_message(request, messages.ERROR, 'Event has not yet been billed.')
return HttpResponseRedirect(reverse('events:detail', args=(self.object.pk,)))
if self.object.paid:
messages.add_message(request, messages.ERROR, 'Event has already been paid.')
return HttpResponseRedirect(reverse('events:detail', args=(self.object.pk,)))
if self.object.entered_into_workday:
messages.add_message(request, messages.ERROR, 'An Internal Service Delivery has already been created in '
'Workday for this event. The worktag to charge can no longer '
'be edited through this website.')
return HttpResponseRedirect(reverse('events:detail', args=(self.object.pk,)))
if is_update:
messages.add_message(request, messages.INFO, 'This bill payment form has already been filled out by {}. '
'You are editing it.'.format(self.object.workday_entered_by))
return super(WorkdayEntry, self).dispatch(request, *args, **kwargs)
[docs] def form_valid(self, form):
# Automatically add the user who submitted the workday form to the client
org = self.object.org_to_be_billed
if org is not None and self.request.user not in org.associated_users.all():
set_revision_comment("Entered Workday billing info for {}. User automatically added to client.".format(
self.object.event_name), form)
org.associated_users.add(self.request.user)
else:
set_revision_comment("Entered Workday billing info", form)
# Add workday info to organization info if not already saved
if org.workday_fund is None or org.worktag is None:
org.workday_fund = self.object.workday_fund
org.worktag = self.object.worktag
org.save()
# If the workday info is being updated as opposed to entered for the first time, send an email to the Treasurer
if self.object.workday_fund is not None or self.object.worktag is not None:
email_body = "The workday billing info for the following event was updated by {}. The previous version " \
"had been entered by {}.".format(self.request.user, self.object.workday_entered_by)
if len(form.changed_data) > 0:
email_body += "\nFields changed: "
for field_name in form.changed_data:
email_body += field_name + ", "
email_body = email_body[:-2]
email = EventEmailGenerator(
event=self.object,
subject='Event Workday Info Updated',
to_emails=settings.EMAIL_TARGET_T,
body=email_body
)
email.send()
self.object.workday_entered_by = self.request.user
messages.success(self.request, "Billing info received. Please approve the Internal Service Delivery in "
"Workday when you receive it.", extra_tags='success')
return super(WorkdayEntry, self).form_valid(form)
[docs] def get_success_url(self):
return reverse("events:detail", args=(self.object.pk,))
[docs]@require_POST
@login_required
def mark_entered_into_workday(request, id):
"""
Marks an event as entered into Workday. POST only.
"""
event = get_object_or_404(Event2019, id=id)
if not request.user.has_perm('events.bill_event', event):
raise PermissionDenied
if event.closed:
messages.add_message(request, messages.ERROR, 'Event is closed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if event.entered_into_workday:
messages.info(request, "Event has already been marked entered into Workday.", extra_tags="info")
if not event.reviewed:
messages.add_message(request, messages.ERROR, 'Event has not been reviewed for billing.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
if not event.last_bill:
messages.add_message(request, messages.ERROR, 'Event has not been billed.')
return HttpResponseRedirect(reverse('events:detail', args=(event.id,)))
else:
event.entered_into_workday = True
set_revision_comment("Marked entered into Workday", None)
event.save()
messages.success(request, "Marked %s as entered into Workday." % event.event_name, extra_tags="success")
return HttpResponseRedirect(reverse('events:awaitingworkday'))