Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions events/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Model, Q
from django.forms import ModelChoiceField, ModelMultipleChoiceField, ModelForm, SelectDateWidget, TextInput
from django.forms import ModelChoiceField, ModelMultipleChoiceField, ModelForm, SelectDateWidget, TextInput, URLInput
from django.utils import timezone
# python multithreading bug workaround
from easymde.widgets import EasyMDEEditor

from data.forms import DynamicFieldContainer, FieldAccessForm, FieldAccessLevel
from events.fields import GroupedModelChoiceField
from events.models import (BaseEvent, Billing, MultiBilling, BillingEmail, MultiBillingEmail,
Category, CCReport, Event, Event2019, EventAttachment, EventCCInstance, Extra,
Category, CCReport, Event, Event2019, EventAttachment, EventResourceLink, EventCCInstance, Extra,
ExtraInstance, Hours, Lighting, Location, Organization, OrganizationTransfer,
OrgBillingVerificationEvent, Workshop, WorkshopDate, Projection, Service, ServiceInstance,
Sound, PostEventSurvey, OfficeHour)
Expand Down Expand Up @@ -1419,6 +1419,36 @@ class Meta:
model = EventAttachment
fields = ('for_service', 'attachment', 'note')

class ResourceLinkForm(forms.ModelForm):
def __init__(self, event, *args, **kwargs):
self.event = event
self.helper = FormHelper()
self.helper.form_class = "form-inline"
self.helper.template = 'bootstrap/table_inline_formset.html'
self.helper.form_tag = False
self.helper.layout = Layout(
Field('note'),
Field('url'),
HTML('<hr>'),
)
super(ResourceLinkForm, self).__init__(*args, **kwargs)

def save(self, commit=True):
obj = super(ResourceLinkForm, self).save(commit=False)
obj.event = self.event
if commit:
obj.save()
self.save_m2m()
return obj

class Meta:
model = EventResourceLink
fields = ('note', 'url')
widgets = {
'note': TextInput(attrs={"style": "width: 100%"}),
'url': URLInput(attrs={"style": "width: 100%"}),
}


class ExtraForm(forms.ModelForm):
class Meta:
Expand Down
23 changes: 23 additions & 0 deletions events/migrations/0014_eventresourcelink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2025-06-27 20:54

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('events', '0013_alter_baseevent_polymorphic_ctype_and_more'),
]

operations = [
migrations.CreateModel(
name='EventResourceLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('note', models.TextField(blank=True, default='', null=True)),
('url', models.URLField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='events.baseevent')),
],
),
]
6 changes: 6 additions & 0 deletions events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,12 @@ class EventAttachment(models.Model):
externally_uploaded = models.BooleanField(default=False)


class EventResourceLink(models.Model):
event = models.ForeignKey(BaseEvent, on_delete=models.CASCADE, related_name="links")
note = models.TextField(null=True, blank=True, default="")
url = models.URLField()


@reversion.register()
class EventArbitrary(models.Model):
""" Additional "OneOff" charges (i.e. rentals, additional fees) """
Expand Down
40 changes: 40 additions & 0 deletions events/tests/test_event_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,46 @@ def test_assignattach(self):
self.assertRedirects(self.client.post(reverse("events:files", args=[self.e.pk]), valid_data),
reverse("events:detail", args=[self.e.pk]))

def test_event_resources(self):
self.setup()

# By default, should not have permission to modify resource links
self.assertOk(self.client.get(reverse("events:resource-links", args=[self.e.pk])), 403)

permission = Permission.objects.get(codename="event_attachments")
self.user.user_permissions.add(permission)

# Will need view_event permission for redirect
permission = Permission.objects.get(codename="view_events")
self.user.user_permissions.add(permission)

# Check that we redirect to detail page if event is closed
self.e.closed = True
self.e.save()

self.assertRedirects(self.client.get(reverse("events:resource-links", args=[self.e.pk])),
reverse("events:detail", args=[self.e.pk]))

self.e.closed = False
self.e.save()

# Everything should load ok
self.assertOk(self.client.get(reverse("events:resource-links", args=[self.e.pk])))

valid_data = {
"links-TOTAL_FORMS": 1,
"links-INITIAL_FORMS": 0,
"links-MIN_NUM_FORMS": 0,
"links-MAX_NUM_FORMS": 1000,
"links-0-note": "test note",
"links-0-url": "https://www.wikipedia.org"
}

# Check that we can add attachment ok
self.assertRedirects(self.client.post(reverse("events:resource-links", args=[self.e.pk]), valid_data),
reverse("events:detail", args=[self.e.pk]))
self.assertTrue(models.EventResourceLink.objects.filter(event=self.e).exists())

def test_ics_download(self):
self.setup()

Expand Down
4 changes: 2 additions & 2 deletions events/urls/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def generate_date_patterns(func, name):
re_path(r'^selfcrew/(?P<id>[0-9]+)/$', flow_views.hours_prefill_self, name="selfcrew"),
re_path(r'^crewchief/(?P<id>[0-9a-f]+)/$', flow_views.assigncc, name="chiefs"),
re_path(r'^rmcc/(?P<id>[0-9a-f]+)/(?P<user>[0-9a-f]+)/$', flow_views.rmcc, name="remove-chief"),
re_path(r'^attachments/(?P<id>[0-9a-f]+)/$', flow_views.assignattach,
name="files"),
re_path(r'^attachments/(?P<id>[0-9a-f]+)/$', flow_views.assignattach, name="files"),
re_path(r'^links/(?P<id>[0-9a-f]+)/$', flow_views.event_resources, name="resource-links"),
re_path(r'^extras/(?P<id>[0-9a-f]+)/$', flow_views.extras, name="extras"),
re_path(r'^oneoff/(?P<id>[0-9a-f]+)/$', flow_views.oneoff, name="oneoffs"),
re_path(r'^enter-worktag/(?P<pk>[0-9a-f]+)/$', flow_views.WorkdayEntry.as_view(), name="worktag-form"),
Expand Down
52 changes: 49 additions & 3 deletions events/views/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
MultiBillingUpdateForm, CCIForm, CrewAssign, EventApprovalForm,
EventDenialForm, EventReviewForm, ExtraForm, InternalReportForm, MKHoursForm,
BillingEmailForm, MultiBillingEmailForm, ServiceInstanceForm, WorkdayForm, CrewCheckinForm,
CrewCheckoutForm, CheckoutHoursForm, BulkCheckinForm
CrewCheckoutForm, CheckoutHoursForm, BulkCheckinForm, ResourceLinkForm
)
from events.models import (BaseEvent, Billing, MultiBilling, BillingEmail, MultiBillingEmail, Category, CCReport, Event,
Event2019, EventArbitrary, EventAttachment, EventCCInstance, ExtraInstance, Hours,
ReportReminder, ServiceInstance, PostEventSurvey, CCR_DELTA, CrewAttendanceRecord)
Event2019, EventArbitrary, EventAttachment, EventResourceLink, EventCCInstance, ExtraInstance,
Hours, ReportReminder, ServiceInstance, PostEventSurvey, CCR_DELTA, CrewAttendanceRecord)
from helpers.mixins import (ConditionalFormMixin, HasPermMixin, HasPermOrTestMixin,
LoginRequiredMixin, SetFormMsgMixin)
from helpers.challenges import is_officer
Expand Down Expand Up @@ -833,6 +833,52 @@ def assignattach_external(request, id):

return render(request, 'formset_crispy_attachments.html', context)

@login_required
def event_resources(request, id):
""" Update resources for an event (links to useful files) """
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

links_formset = inlineformset_factory(BaseEvent, EventResourceLink, extra=2, exclude=[])
links_formset.form = curry_class(ResourceLinkForm, event=event)

if request.method == 'POST':
set_revision_comment("Edited attachments", None)
formset = links_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 = links_formset(instance=event)

context['formset'] = formset

return render(request, 'formset_crispy_event_resource_links.html', context)


@login_required
def download_ics(request, pk):
Expand Down
60 changes: 60 additions & 0 deletions site_tmpl/formset_crispy_event_resource_links.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{% extends 'base_admin.html' %}
{% load crispy_forms_tags %}

{% block title %}Links for "{{event}}" | Lens and Lights at WPI{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h2>Links for "{{ event }}"</h2>
<form method="post" action="" class="form-inline" enctype="multipart/form-data">
{% csrf_token %}
{{ formset.management_form }}
<table class="table table-striped" valign="top"> <tbody id="form_data">
<tr>
<th width="30%"">Name</th>
<th width="60%">URL</th>
<th width="10%">Delete?</th>
</tr>
{% for form in formset %}
{% if form.errors %}
<tr class="warning">
<td>{% for e in form.note.errors %} {{ e }} <i class="glyphicon glyphicon-chevron-down"></i> {% endfor %}</td>
<td>{% for e in form.url.errors %} {{ e }} <i class="glyphicon glyphicon-chevron-down"></i> {% endfor %}</td>
<td>{% for e in form.DELETE.errors %} {{ e }} <i class="glyphicon glyphicon-chevron-down"></i> {% endfor %}</td>
</tr>
{% endif %}
<tr>
<td>{{ form.id }}{{ form.note }}</td>
<td>{{ form.url }} </td>
<td>{{ form.DELETE }} </td>
</tr>
{% endfor %}
</tbody></table>

<div class="form-actions"><input name="save" value="Save Changes" class="btn btn-primary" id="submit-id-save" type="submit">
<input type="button" class="btn btn-success" value="Add Row" id="add_form"></div>
</form>

</div>
</div>
{% include "js_formset_add_row.tmpl" %}
{% with formset.empty_form as form %}
<table style="display:none;"> <tbody id="empty_form">
<tr>
<td>{{ form.id }}{{ form.note }}</td>
<td>{{ form.url }} </td>
<td>{{ form.DELETE }} </td>
</tr>
</tbody>
</table>
{% endwith %}
{% endblock %}

{% block extras %}
{{ formset.media }}
{% include "js_datetimepick.tmpl" %}
{% endblock %}

{% block finalsay %}

{% endblock %}
26 changes: 24 additions & 2 deletions site_tmpl/uglydetail.html
Original file line number Diff line number Diff line change
Expand Up @@ -883,11 +883,33 @@ <h3>Internal Notes</h3>
<div id="files" class="tab-pane">
{% if not event.closed %}
{% permission request.user has 'events.event_attachments' of event %}
<div class="pull-right">
<a class="btn btn-warning" href="{% url "events:files" event.id %}">Edit</a>
<div class="button-group">
<div class="pull-right">
<a class="btn btn-info" href="{% url "events:resource-links" event.id %}">Edit Links</a>
<a class="btn btn-warning" href="{% url "events:files" event.id %}">Edit Attachments</a>
</div>
</div>
{% endpermission %}
{% endif %}

<h2> Links </h2>
<table class="table">
<tr>
<th>Name</th>
<th>URL</th>
<th></th>
</tr>
{% for a in event.links.all %}
<tr>
<td>{{ a.note }}</td>
<td><a href="{{ a.url }}">{{ a.url }}</a></td>
</tr>

{% empty %}
<tr><td colspan="4">No links have been submitted</td></tr>
{% endfor %}
</table>

<h2> Files </h2>
<table class="table">
<tr>
Expand Down