Compare commits

..

11 Commits

31 changed files with 1759 additions and 1420 deletions

View File

@@ -34,12 +34,12 @@ idna = "~=2.10"
Markdown = "~=3.3.3" Markdown = "~=3.3.3"
msgpack = "~=1.0.2" msgpack = "~=1.0.2"
pep517 = "~=0.9.1" pep517 = "~=0.9.1"
Pillow = "~=10.0.1" Pillow = "~=9.3.0"
premailer = "~=3.7.0" premailer = "~=3.7.0"
progress = "~=1.5" progress = "~=1.5"
psutil = "~=5.8.0" psutil = "~=5.8.0"
psycopg2 = "~=2.8.6" psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0" Pygments = "~=2.7.4"
pyparsing = "~=2.4.7" pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5" PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4" PyPOM = "~=2.2.4"
@@ -57,7 +57,7 @@ static3 = "~=0.7.0"
svg2rlg = "~=0.3" svg2rlg = "~=0.3"
tini = "~=3.0.1" tini = "~=3.0.1"
tornado = "~=6.3" tornado = "~=6.3"
urllib3 = "~=1.26.18" urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0" whitenoise = "~=5.2.0"
yolk = "~=0.4.3" yolk = "~=0.4.3"
zipp = "~=3.4.0" zipp = "~=3.4.0"

903
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,6 @@ if DEBUG:
ALLOWED_HOSTS.append('localhost') ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com') ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1') ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('.app.github.dev')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG: if not DEBUG:

View File

@@ -48,7 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count() context['rig_count'] = models.Event.objects.rig_count()
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False) context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(dry_hire=True).exclude(status=models.Event.CANCELLED)
return context return context
@@ -134,9 +134,6 @@ class SecureAPIRequest(generic.View):
results = [] results = []
query = reduce(operator.and_, queries) query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query) objects = self.models[model].objects.filter(query)
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
if model == "profile":
objects = objects.filter(is_active=True, is_approved=True)
for o in objects: for o in objects:
name = o.display_name if hasattr(o, 'display_name') else o.name name = o.display_name if hasattr(o, 'display_name') else o.name
data = { data = {

View File

@@ -154,9 +154,8 @@ class AssociateAdmin(VersionAdmin):
@admin.register(models.Profile) @admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin): class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login') list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username'] list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved',)
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('Personal info'), {

View File

@@ -121,7 +121,7 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date', fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic', 'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status', 'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'purchase_order', 'collector', 'forum_url'] 'purchase_order', 'collector']
class BaseClientEventAuthorisationForm(forms.ModelForm): class BaseClientEventAuthorisationForm(forms.ModelForm):

View File

@@ -1,6 +1,5 @@
# Generated by Django 3.2.19 on 2023-06-27 11:28 # Generated by Django 3.2.18 on 2023-06-26 17:46
import RIGS.models
from django.db import migrations, models from django.db import migrations, models
@@ -14,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',
name='forum_url', name='forum_url',
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]), field=models.URLField(blank=True, null=True),
), ),
] ]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-07-09 21:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0050_event_forum_url'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
]

View File

@@ -76,8 +76,7 @@ class Profile(AbstractUser):
@classmethod @classmethod
def users_awaiting_approval_count(cls): def users_awaiting_approval_count(cls):
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity return Profile.objects.filter(models.Q(is_approved=False)).count()
return Profile.objects.filter(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count()
def __str__(self): def __str__(self):
return self.name return self.name
@@ -309,14 +308,6 @@ class EventManager(models.Manager):
return qs return qs
def validate_forum_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('forum.nottinghamtec.co.uk'):
raise ValidationError('URL must point to a location on the TEC Forum')
@reversion.register(follow=['items']) @reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin): class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database # Done to make it much nicer on the database
@@ -366,7 +357,7 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='') auth_request_to = models.EmailField(blank=True, default='')
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url]) forum_url = models.URLField(null=True, blank=True)
@property @property
def display_id(self): def display_id(self):
@@ -516,7 +507,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk}) return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return f"{self.display_id} | {self.name}" return f"{self.display_id}: {self.name}"
def clean(self): def clean(self):
errdict = {} errdict = {}
@@ -688,11 +679,13 @@ class Payment(models.Model, RevisionMixin):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'
EXTERNAL = 'E' EXTERNAL = 'E'
SUCORE = 'SU'
ADJUSTMENT = 'T' ADJUSTMENT = 'T'
METHODS = ( METHODS = (
(CASH, 'Cash'), (CASH, 'Cash'),
(INTERNAL, 'Internal'), (INTERNAL, 'Internal'),
(EXTERNAL, 'External'), (EXTERNAL, 'External'),
(SUCORE, 'SU Core'),
(ADJUSTMENT, 'TEC Adjustment'), (ADJUSTMENT, 'TEC Adjustment'),
) )

View File

@@ -3,7 +3,6 @@ import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from io import BytesIO from io import BytesIO
import datetime
from PyPDF2 import PdfFileReader, PdfFileMerger from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings from django.conf import settings
@@ -111,7 +110,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now(): if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
context = { context = {
'request': request, 'request': request,
'link_suffix': reverse("admin:RIGS_profile_changelist") + f'?is_approved__exact=0&date_joined__date={timezone.now().date()}', 'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
'number_of_users': models.Profile.users_awaiting_approval_count(), 'number_of_users': models.Profile.users_awaiting_approval_count(),
'to_name': admin.first_name 'to_name': admin.first_name
} }

View File

@@ -26,7 +26,6 @@
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('calendar');
calendar = new FullCalendar.Calendar(calendarEl, { calendar = new FullCalendar.Calendar(calendarEl, {
firstDay: 1,
themeSystem: 'bootstrap', themeSystem: 'bootstrap',
aspectRatio: 1.5, aspectRatio: 1.5,
eventTimeFormat: { eventTimeFormat: {

View File

@@ -231,7 +231,7 @@
<label for="{{ form.start_date.id_for_label }}" <label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label> class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-10"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required"> <div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %} {% render_field form.start_date class+="form-control" %}
@@ -246,7 +246,7 @@
<label for="{{ form.end_date.id_for_label }}" <label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label> class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-10"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date"> <div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %} {% render_field form.end_date class+="form-control" %}
@@ -334,26 +334,12 @@
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)"> <div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}" <label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label> class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %} {% render_field form.purchase_order class+="form-control" %}
</div> </div>
</div> </div>
<div class="form-group" data-toggle="tooltip" title="The thread for this event on the TEC Forum">
<label for="{{ form.forum_url.id_for_label }}"
class="col-sm-4 col-form-label">Forum Thread</label>
<div class="col-sm-12">
<p class="small mb-0">Paste URL</p>
{% render_field form.forum_url class+="form-control" %}
{% if object.pk %}
<p class="small mb-0">or</p>
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank">
<span class="fas fa-plus"></span> <span class="hidden-xs">Create Forum Thread</span></a>
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -165,11 +165,11 @@
</div> </div>
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'edit' url='pt_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %} {% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="powertestrecord_history" %} {% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,6 @@
<h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5> <h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
<div class="row collapse" id="values"> <div class="row collapse" id="values">
<div class="col-md-6 col-sm-12"> <div class="table-responsive">
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
@@ -33,20 +33,17 @@
<thead> <thead>
<tr> <tr>
<th scope="row">Distro</th> <th scope="row">Distro</th>
<th scope="row">Max PSSC with Single Phase Supply (kA)</th> <th scope="row">Max PSSC (kA)</th>
<th scope="row">Max PSSC with Three Phase Supply (kA)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>Intel & Toblerone distros</td> <td>Intel & Toblerone distros</td>
<td>6</td> <td>6</td>
<td>3</td>
</tr> </tr>
<tr> <tr>
<td>All other distros</td> <td>All other distros</td>
<td>10</td> <td>10</td>
<td>5</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -79,10 +79,10 @@
{% endif %} {% endif %}
<dt class="col-6">Forum Thread</dt> <dt class="col-6">Forum Thread</dt>
{% if object.forum_url %} {% if event.forum_thread %}
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd> <dd class="col-6"><a href="{{event.forum_thread}}">{{event.forum_thread}}</a></dd>
{% else %} {% else %}
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank"><span <a href="{% url 'event_thread' event.pk %}" class="btn btn-primary" title="Create Forum Thread"><span
class="fas fa-plus"></span> <span class="fas fa-plus"></span> <span
class="hidden-xs">Create Forum Thread</span></a> class="hidden-xs">Create Forum Thread</span></a>
{% endif %} {% endif %}

View File

@@ -1,4 +1,4 @@
<div id="event_status"> <div>
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span> <span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
{% if event.is_rig %} {% if event.is_rig %}
{% if event.sum_total > 0 %} {% if event.sum_total > 0 %}

View File

@@ -1,183 +1,105 @@
{% load namewithnotes from filters %} {% load namewithnotes from filters %}
{% load markdown_tags %} {% load markdown_tags %}
<style> <div class="table-responsive">
#event_table { <table class="table mb-0" id="event_table">
display: grid; <thead>
grid-template-columns: max-content auto; <tr>
column-gap: 1em; <th scope="col">#</th>
} <th scope="col">Dates & Times</th>
.eventgrid { <th scope="col">Event Details</th>
display: inherit; <th scope="col">MIC</th>
grid-column: 1/5; </tr>
grid-template-columns: subgrid; </thead>
padding: 1em; <tbody>
} {% for event in events %}
.grid-header { <tr class="{% if event.cancelled %}
border-bottom: 1px solid grey; table-secondary
border-top: 1px solid grey; {% elif not event.is_rig %}
} table-info
#event_status { {% elif not event.mic %}
grid-column-start: 3; table-danger
} {% elif event.confirmed and event.authorised %}
#event_mic { {% if event.dry_hire or event.riskassessment %}
grid-row-start: 1; table-success
grid-column-start: 4; {% else %}
} table-warning
@media (max-width: 600px) { {% endif %}
#event_table { {% else %}
grid-template-columns: 1fr !important; table-warning
} {% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
.eventgrid { <!---Number-->
grid-column: 1/1 !important; <th scope="row" id="event_number">{{ event.display_id }}</th>
padding: 0.5em; <!--Dates & Times-->
} <td id="event_dates" style="text-align: justify;">
.grid-header { {% if not event.cancelled %}
display: none; {% if event.meet_at %}
} <span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
#event_dates { {% endif %}
order: 2; {% if event.access_at %}
} <br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
#event_status { {% endif %}
order: 3; {% endif %}
} <span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
#event_mic { {% if event.has_start_time %}
grid-row-start: auto; {{ event.start_time|date:"H:i" }}
grid-column-start: 4; {% endif %}</strong>
} </span>
} {% if event.end_date %}
@media (max-width: 900px) { <br>
#event_table { <span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
grid-template-columns: max-content; {% if event.has_end_time %}
column-gap: 0.5em; {{ event.end_time|date:"H:i" }}
} {% endif %}</strong>
.eventgrid { </span>
grid-column: 1/3; {% endif %}
border: 1px solid grey; </td>
} <!---Details-->
#event_dates { <td id="event_details" class="w-100">
grid-row: 2; <h4>
grid-column: 1; <a href="{% url 'event_detail' event.pk %}">
} {{ event.name }}
#event_number { </a>
grid-row: 1; {% if event.venue %}
grid-column: 1; <small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
} {% endif %}
#event_mic { {% if event.dry_hire %}
grid-column: 2; <span class="badge badge-secondary">Dry Hire</span>
} {% endif %}
#event_status { </h4>
grid-column: span 2; {% if event.is_rig and not event.cancelled %}
} <h5>
.grid-header { <a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
display: none; {% if event.organisation %}
} for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
} {% endif %}
dt { </h5>
float: left; {% endif %}
clear: left; {% if not event.cancelled and event.description %}
margin-right: 10px; <p>{{ event.description|markdown }}</p>
} {% endif %}
dd { {% include 'partials/event_status.html' %}
margin-left: 0px; </td>
} <!---MIC-->
</style> <td id="event_mic" class="text-nowrap">
<div id="event_table"> {% if event.mic %}
<div class="eventgrid grid-header font-weight-bold"> {% if perms.RIGS.view_profile %}
<div id="event_number">#</div> <a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
<div id="event_dates">Dates & Times</div> {% endif %}
<div>Event Details</div> <img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
<div id="event_mic">MIC</div> {{ event.mic }}
</div> {% if perms.RIGS.view_profile %}
{% for event in events %} </a>
<div class="eventgrid {% if event.cancelled %} {% endif %}
table-secondary {% elif event.is_rig %}
{% elif not event.is_rig %} <span class="fas fa-user-slash"></span>
table-info {% endif %}
{% elif not event.mic %} </td>
table-danger </tr>
{% elif event.confirmed and event.authorised %} {% empty %}
{% if event.dry_hire or event.riskassessment %} <tr class="bg-warning">
table-success <td colspan="4">No events found</td>
{% else %} </tr>
table-warning {% endfor %}
{% endif %} </tbody>
{% else %} </table>
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<div class="font-weight-bold d-none d-lg-block" id="event_number">{{ event.display_id }}</div>
<!--Dates & Times-->
<div id="event_dates" style="min-width: 180px;">
<dl>
{% if not event.cancelled %}
{% if event.meet_at %}
<dt class="font-weight-normal">Meet:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.meet_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% if event.access_at %}
<dt class="font-weight-normal">Access:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.access_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% endif %}
<dt class="font-weight-normal">Start:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}
</dd>
{% if event.end_date %}
<dt class="font-weight-normal">End:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.end_date|date:"D d/m/Y" }}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}
</dd>
{% endif %}
</dl>
</div>
<!---Details-->
<div id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
<span class="d-inline d-lg-none">{{ event }}</span><span class="d-none d-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
<br class="d-none d-lg-inline">
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</div>
{% include 'partials/event_status.html' %}
<!---MIC-->
<div id="event_mic" class="text-nowrap">
<span class="d-md-none align-middle">MIC:</span>
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
{% endif %}
</div>
</div>
{% endfor %}
</div> </div>

View File

@@ -29,15 +29,7 @@
</div> </div>
<div class="row pt-3"> <div class="row pt-3">
<label class="col-sm-4 col-form-label" <label class="col-sm-4 col-form-label"
for="{{ form.method.id_for_label }}">{{ form.method.label }} for="{{ form.method.id_for_label }}">{{ form.method.label }}</label>
<span class="fas fa-info-circle text-info" data-toggle="collapse" data-target="#collapse" aria-expanded="false" aria-controls="collapse"></span>
<ul class="collapse" id="collapse">
<li>Cash - Self Explanatory</li>
<li>Internal - Transfers within the Students' Union only</li>
<li>External - All other transfers (<em>including</em> the University)</li>
<li>TEC Adjustment - Manual corrections</li>
</ul>
</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.method class+="form-control" %} {% render_field form.method class+="form-control" %}
</div> </div>

View File

@@ -3,8 +3,8 @@
{% block content %} {% block content %}
<div class="row align-items-center justify-content-between py-2 align-middle"> <div class="row align-items-center justify-content-between py-2 align-middle">
<div class="col-sm-12 col-md align-middle d-flex flex-wrap"> <div class="col-sm-12 col-md align-middle">
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded text-nowrap">Action Required</span><span class="table-danger mr-1 px-2 rounded text-nowrap">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded text-nowrap">Non-Rig</span> Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded">Action Required</span><span class="table-danger mr-1 px-2 rounded">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded">Non-Rig</span>
</div> </div>
{% if perms.RIGS.add_event %} {% if perms.RIGS.add_event %}
<div class="col text-right"> <div class="col text-right">

View File

@@ -5,7 +5,6 @@ import premailer
import simplejson import simplejson
import urllib import urllib
import hmac import hmac
import hashlib
from envparse import env from envparse import env
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -409,16 +408,13 @@ class RecieveForumWebhook(generic.View):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}" computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, digestmod='sha256').hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed): if request.POST.get('X-Discourse-Event-Signature') == computed: # and request.POST.get('X-Discourse-Event') == "topic_created":
return HttpResponseForbidden('Invalid signature header') body = json.loads(request.body.decode('utf-8'))
# Check if this is the right kind of event. The webhook filters by category on the forum side event_id = int(body['title'][1:5]) # find the ID, force convert it to an int to eliminate leading zeros
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first() event = models.Event.objects.filter(pk=event_id).first()
if event: if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}" event.forum_url = "https://forum.nottinghamtec.co.uk/t/{}"
event.save() event.save()
return HttpResponse(status=202) return HttpResponse(status=200)
return HttpResponse(status=204) return HttpResponse(status=204)

View File

@@ -4,7 +4,7 @@
"scripts": { "scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData" "postdeploy": "python manage.py migrate && python manage.py generateSampleData"
}, },
"stack": "heroku-22", "stack": "heroku-20",
"env": { "env": {
"DEBUG": { "DEBUG": {
"required": true "required": true
@@ -51,7 +51,7 @@
"url": "heroku/nodejs" "url": "heroku/nodejs"
}, },
{ {
"url": "heroku/python" "url": "https://github.com/nottinghamtec/heroku-buildpack-python"
} }
] ]
} }

View File

@@ -38,17 +38,3 @@ def test_asset(db, category, status):
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100) asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
yield asset yield asset
asset.delete() asset.delete()
@pytest.fixture
def test_status_2(db):
status = models.AssetStatus.objects.create(name="Lost", should_show=False)
yield status
status.delete()
@pytest.fixture
def test_asset_2(db, category, test_status_2):
asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000)
yield asset
asset.delete()

View File

@@ -1,6 +1,5 @@
import time import time
import datetime import datetime
import pytest
from django.utils import timezone from django.utils import timezone
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@@ -54,45 +53,45 @@ class TestAssetList(AutoLoginTest):
self.assertEqual("10", asset_ids[2]) self.assertEqual("10", asset_ids[2])
self.assertEqual("C1", asset_ids[3]) self.assertEqual("C1", asset_ids[3])
def test_search(self):
self.page.set_query("10")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("Working Mic", self.page.assets[0].description)
self.assertEqual("10", self.page.assets[0].id)
@pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError) self.page.set_query("light")
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type): self.page.search()
page = pages.AssetList(logged_in_browser.driver, live_server.url).open() self.assertTrue(len(self.page.assets) == 1)
page.set_query(test_asset.asset_id) self.assertEqual("A light", self.page.assets[0].description)
page.search()
assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
assert page.assets[0].id == test_asset.asset_id
page.set_query(test_asset.description) self.page.set_query("Random string")
page.search() self.page.search()
assert len(page.assets) == 1 self.assertTrue(len(self.page.assets) == 0)
assert page.assets[0].description == test_asset.description
page.set_query("Random string") self.page.set_query("")
page.search() self.page.search()
assert len(page.assets) == 0 # Only working stuff shown by default
self.assertTrue(len(self.page.assets) == 2)
page.set_query("") self.page.status_selector.toggle()
page.search() self.assertTrue(self.page.status_selector.is_open)
# Only working stuff shown by default self.page.status_selector.select_all()
assert len(page.assets) == 1 self.page.status_selector.toggle()
self.assertFalse(self.page.status_selector.is_open)
self.page.filter()
self.assertTrue(len(self.page.assets) == 4)
page.status_selector.toggle() self.page.category_selector.toggle()
assert page.status_selector.is_open self.assertTrue(self.page.category_selector.is_open)
page.status_selector.select_all() self.page.category_selector.set_option("Sound", True)
page.status_selector.toggle() self.page.category_selector.close()
assert not page.status_selector.is_open self.assertFalse(self.page.category_selector.is_open)
page.filter() self.page.filter()
assert len(page.assets) == 2 self.assertTrue(len(self.page.assets) == 2)
asset_ids = list(map(lambda x: x.id, self.page.assets))
page.category_selector.toggle() self.assertEqual("1", asset_ids[0])
assert page.category_selector.is_open self.assertEqual("10", asset_ids[1])
page.category_selector.set_option(category.name, True)
page.category_selector.close()
assert not page.category_selector.is_open
page.filter()
assert len(page.assets) == 2
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type): def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):

View File

@@ -79,7 +79,7 @@ function browserSync(done) {
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'}); spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
// TODO Wait for Django server to come up before browsersync, it seems inconsistent // TODO Wait for Django server to come up before browsersync, it seems inconsistent
browsersync.init({ browsersync.init({
notify: true, notify: false,
open: false, open: false,
port: 8001, port: 8001,
proxy: '127.0.0.1:8000' proxy: '127.0.0.1:8000'

1717
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,13 +28,13 @@
"jquery": "^3.6.0", "jquery": "^3.6.0",
"konami": "^1.6.3", "konami": "^1.6.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-sass": "^9.0.0", "node-sass": "^7.0.3",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss": "^8.4.31", "postcss": "^8.4.5",
"uglify-js": "^3.14.5" "uglify-js": "^3.14.5"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^3.0.2" "browser-sync": "^2.27.11"
}, },
"scripts": { "scripts": {
"gulp": "gulp", "gulp": "gulp",

View File

@@ -77,8 +77,17 @@
border-collapse: separate !important; border-collapse: separate !important;
border-spacing: 0; border-spacing: 0;
} }
#event_table tr th {
border-right: 0 !important;
}
#event_table tr td {
border-left: 0 !important;
}
#event_table tr td:not(:last-child) {
border-right: 0 !important;
}
@each $color, $value in $theme-colors { @each $color, $value in $theme-colors {
table.table-#{$color} { .table-#{$color} {
> td,th { > td,th {
border: 0.3em solid theme-color-level($color, -6) !important; border: 0.3em solid theme-color-level($color, -6) !important;
} }
@@ -87,11 +96,6 @@
background-color: #222 !important; background-color: #222 !important;
} }
} }
#event_row.table-#{$color} {
border: 0.3em solid theme-color-level($color, -6) !important;
background-color: #222 !important;
color: white !important;
}
} }
del { del {
color: black; color: black;
@@ -152,7 +156,4 @@
.modal { .modal {
overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back
} }
.text-muted {
color: #c9c9c9 !important;
}
} }

View File

@@ -11,7 +11,7 @@
{% if now %} {% if now %}
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto"> <div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
{% for event in now %} {% for event in now %}
Event {{ event }} is happening today! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/> Event {{ event }} is happening now! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -78,10 +78,10 @@
</tr> </tr>
{% endfor %} {% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr> <tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr> <tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -12,8 +12,8 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
for person in Profile.objects.all(): for person in Profile.objects.all():
# Inactivate users that have not logged in for a year # Inactivate users that have not logged in for a year (or have never logged in)
if person.last_login is not None and (timezone.now() - person.last_login).days > 365: if person.last_login is None or (timezone.now() - person.last_login).days > 365:
person.is_active = False person.is_active = False
person.is_approved = False person.is_approved = False
person.save() person.save()

View File

@@ -1,4 +1,3 @@
import logging
from diff_match_patch import diff_match_patch from diff_match_patch import diff_match_patch
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -148,9 +147,9 @@ class ModelComparison:
@cached_property @cached_property
def item_changes(self): def item_changes(self):
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
if self.follow and self.version.object is not None: if self.follow and self.version.object is not None:
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
item_type = ContentType.objects.get_for_model(self.version.object) item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \ old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification)) .exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
@@ -161,14 +160,13 @@ class ModelComparison:
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk item_dict = {} # build a list of items, key is the item_pk
for version in old_item_versions: # put all the old versions in a list for version in old_item_versions: # put all the old versions in a list
if version._model is None: old = version._object_version.object
continue if old is None:
compare = ModelComparison(old=version._object_version.object, **comparisonParams) pass
compare = ModelComparison(old=old, **comparisonParams)
item_dict[version.object_id] = compare item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions for version in new_item_versions: # go through the new versions
if version._model is None:
continue
try: try:
compare = item_dict[version.object_id] # see if there's a matching old version compare = item_dict[version.object_id] # see if there's a matching old version
compare.new = version._object_version.object # then add the new version to the dictionary compare.new = version._object_version.object # then add the new version to the dictionary