Compare commits

..

2 Commits

Author SHA1 Message Date
cbe651957d Create 'upcoming events list' with very limited data
UoN users (with nottingham.ac.uk emails) are allowed to access this page only. Not a huge fan of the implementation, hoping someone (maybe future me) comes up with a less nasty implementation
2023-09-23 19:47:00 +00:00
ef2826ab0a Added 'is on campus' field to venue 2023-09-23 18:54:29 +00:00
30 changed files with 669 additions and 676 deletions

View File

@@ -34,7 +34,7 @@ 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"
@@ -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"

755
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -121,3 +121,7 @@ def nottinghamtec_address_required(function):
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
return wrap return wrap
def not_estates():
return user_passes_test_with_403(lambda u: not u.email.endswith('@nottingham.ac.uk'))

View File

View File

@@ -35,7 +35,8 @@ 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') ALLOWED_HOSTS.append('.github.dev')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

View File

@@ -6,6 +6,8 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from PyRIGS.decorators import not_estates
from PyRIGS import views from PyRIGS import views
urlpatterns = [ urlpatterns = [
@@ -14,17 +16,17 @@ urlpatterns = [
path('assets/', include('assets.urls')), path('assets/', include('assets.urls')),
path('training/', include('training.urls')), path('training/', include('training.urls')),
path('', login_required(views.Index.as_view()), name='index'), path('', not_estates()(views.Index.as_view()), name='index'),
# API # API
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()), path('api/<str:model>/', not_estates()(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()), path('api/<str:model>/<int:pk>/', not_estates()(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'), path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', login_required(views.Search.as_view()), name='search'), path('search/', not_estates()(views.Search.as_view()), name='search'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'), path('search_help/', not_estates()(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')), path('', include('users.urls')),

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.21 on 2023-09-05 22:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0051_alter_payment_method'),
]
operations = [
migrations.AddField(
model_name='venue',
name='on_campus',
field=models.BooleanField(default=False, verbose_name='Is this venue on a UoN campus?'),
),
]

View File

@@ -77,7 +77,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 # last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
return Profile.objects.filter(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count() return Profile.objects.filter(is_approved=False, last_login=None).count()
def __str__(self): def __str__(self):
return self.name return self.name
@@ -213,6 +213,7 @@ class Venue(models.Model, RevisionMixin):
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False) three_phase_available = models.BooleanField(default=False)
on_campus = models.BooleanField(default=False, verbose_name="Is this venue on a UoN campus?")
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, default='')

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

@@ -0,0 +1,5 @@
{% extends 'base_client.html' %}
{% block content %}
{% include 'estates/estates_event_table.html' %}
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="table-responsive">
<table class="table mb-0" id="event_table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Dates & Times</th>
<th scope="col">Event Details</th>
<th scope="col">Status</th>
<th scope="col">Member In Charge</th>
<th scope="col">Power Plan</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
<td id="event_dates" style="text-align: justify;">
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% if event.end_date %}
<br>
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% endif %}
</td>
<!---Details-->
<td id="event_details" class="w-100">
<h4>
{{ event.name }}
{% if event.venue %}
<small>at {{ event.venue }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
{{ event.person.name }}
{% if event.organisation %}
for {{ event.organisation.name }}
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</td>
<td>
{{ event.get_status_display }}
</td>
<!---MIC-->
<td id="event_mic" class="text-nowrap">
{% if event.mic %}
{{ event.mic }}
{% elif event.is_rig %}
<span class="fas fa-user-slash"></span>
{% endif %}
</td>
<td>
{{ event.riskassessment.power_plan|default:"Pending" }}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="4">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

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

@@ -4,7 +4,7 @@ from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView from django.views.generic import RedirectView
from PyRIGS.decorators import (api_key_required, has_oembed, from PyRIGS.decorators import (api_key_required, has_oembed,
permission_required_with_403) permission_required_with_403, not_estates)
from . import views from . import views
urlpatterns = [ urlpatterns = [
@@ -42,21 +42,22 @@ urlpatterns = [
name='venue_update'), name='venue_update'),
# Rigboard # Rigboard
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'), path('rigboard/', not_estates()(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()), path('rigboard/calendar/', not_estates()(views.WebCalendar.as_view()),
name='web_calendar'), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$', re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(views.WebCalendar.as_view()), name='web_calendar'), not_estates()(views.WebCalendar.as_view()), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$', re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
login_required()(views.WebCalendar.as_view()), name='web_calendar'), not_estates()(views.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
path('estates/', login_required()(views.EstatesEventList.as_view()), name='estates'),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()), path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()),
name='event_detail'), name='event_detail'),
path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()), path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()),
name='event_create'), name='event_create'),
path('event/archive/', login_required()(views.EventArchive.as_view()), path('event/archive/', not_estates()(views.EventArchive.as_view()),
name='event_archive'), name='event_archive'),
path('event/<int:pk>/embed/', path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())), xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())),
@@ -75,7 +76,7 @@ urlpatterns = [
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()), path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
name='event_ra'), name='event_ra'),
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()), path('event/ra/<int:pk>/', not_estates()(views.EventRiskAssessmentDetail.as_view()),
name='ra_detail'), name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()), path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'), name='ra_edit'),
@@ -85,7 +86,7 @@ urlpatterns = [
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()), path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
name='event_ec'), name='event_ec'),
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()), path('event/checklist/<int:pk>/', not_estates()(views.EventChecklistDetail.as_view()),
name='ec_detail'), name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()), path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'), name='ec_edit'),
@@ -94,20 +95,20 @@ urlpatterns = [
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()), path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
name='event_pt'), name='event_pt'),
path('event/power/<int:pk>/', login_required(views.PowerTestDetail.as_view()), path('event/power/<int:pk>/', not_estates()(views.PowerTestDetail.as_view()),
name='pt_detail'), name='pt_detail'),
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()), path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
name='pt_edit'), name='pt_edit'),
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()), path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
name='pt_review', kwargs={'model': 'PowerTestRecord'}), name='pt_review', kwargs={'model': 'PowerTestRecord'}),
path('event/<int:pk>/checkin/', login_required(views.EventCheckIn.as_view()), path('event/<int:pk>/checkin/', not_estates()(views.EventCheckIn.as_view()),
name='event_checkin'), name='event_checkin'),
path('event/checkout/', login_required(views.EventCheckOut.as_view()), path('event/checkout/', not_estates()(views.EventCheckOut.as_view()),
name='event_checkout'), name='event_checkout'),
path('event/<int:pk>/checkin/edit/', login_required(views.EventCheckInEdit.as_view()), path('event/<int:pk>/checkin/edit/', not_estates()(views.EventCheckInEdit.as_view()),
name='edit_checkin'), name='edit_checkin'),
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()), path('event/<int:pk>/checkin/add/', not_estates()(views.EventCheckInOverride.as_view()),
name='event_checkin_override'), name='event_checkin_override'),
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'), path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),

View File

@@ -115,7 +115,7 @@ class VenueDetail(GenericDetailView):
class VenueCreate(GenericCreateView, ModalURLMixin): class VenueCreate(GenericCreateView, ModalURLMixin):
model = models.Venue model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available', 'on_campus']
def get_success_url(self): def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail') return self.get_close_url('venue_update', 'venue_detail')
@@ -123,7 +123,7 @@ class VenueCreate(GenericCreateView, ModalURLMixin):
class VenueUpdate(GenericUpdateView, ModalURLMixin): class VenueUpdate(GenericUpdateView, ModalURLMixin):
model = models.Venue model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available', 'on_campus']
def get_success_url(self): def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail') return self.get_close_url('venue_update', 'venue_detail')

View File

@@ -26,6 +26,7 @@ from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.mixins import UserPassesTestMixin
from PyRIGS import decorators from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
@@ -422,3 +423,17 @@ class RecieveForumWebhook(generic.View):
event.save() event.save()
return HttpResponse(status=202) return HttpResponse(status=202)
return HttpResponse(status=204) return HttpResponse(status=204)
class EstatesEventList(UserPassesTestMixin, generic.TemplateView):
template_name = 'estates/estates_event_list.html'
def get_context_data(self, **kwargs):
# get super context
context = super().get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events().filter(venue__on_campus=True, dry_hire=False, is_rig=True)
context['page_title'] = "Upcoming Campus Events"
return context
def test_func(self):
return self.request.user.email.endswith('@nottingham.ac.uk')

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

@@ -2,7 +2,7 @@ from django.contrib.auth.decorators import login_required
from django.urls import path, register_converter from django.urls import path, register_converter
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403 from PyRIGS.decorators import has_oembed, permission_required_with_403, not_estates
from PyRIGS.views import OEmbedView from PyRIGS.views import OEmbedView
from . import views, converters from . import views, converters
@@ -10,8 +10,8 @@ register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list') register_converter(converters.ListConverter, 'list')
urlpatterns = [ urlpatterns = [
path('', login_required(views.AssetList.as_view()), name='asset_index'), path('', not_estates()(views.AssetList.as_view()), name='asset_index'),
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'), path('asset/list/', not_estates()(views.AssetList.as_view()), name='asset_list'),
path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'), path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset') path('asset/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'), (views.AssetCreate.as_view()), name='asset_create'),
@@ -19,14 +19,14 @@ urlpatterns = [
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'), path('asset/id/<asset:pk>/label', not_estates()(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'), path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cables/list/', login_required(views.CableList.as_view()), name='cable_list'), path('cables/list/', not_estates()(views.CableList.as_view()), name='cable_list'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', not_estates()(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'), path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/detail/', login_required(views.CableTypeDetail.as_view()), name='cable_type_detail'), path('cabletype/<int:pk>/detail/', not_estates()(views.CableTypeDetail.as_view()), name='cable_type_detail'),
path('asset/id/<str:pk>/embed/', path('asset/id/<str:pk>/embed/',
xframe_options_exempt( xframe_options_exempt(
@@ -37,8 +37,8 @@ urlpatterns = [
path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'), path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'), path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),
path('supplier/list/', login_required(views.SupplierList.as_view()), name='supplier_list'), path('supplier/list/', not_estates()(views.SupplierList.as_view()), name='supplier_list'),
path('supplier/<int:pk>/', login_required(views.SupplierDetail.as_view()), name='supplier_detail'), path('supplier/<int:pk>/', not_estates()(views.SupplierDetail.as_view()), name='supplier_detail'),
path('supplier/create/', permission_required_with_403('assets.add_supplier') path('supplier/create/', permission_required_with_403('assets.add_supplier')
(views.SupplierCreate.as_view()), name='supplier_create'), (views.SupplierCreate.as_view()), name='supplier_create'),
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier') path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')

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'

14
package-lock.json generated
View File

@@ -34,7 +34,7 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"node-sass": "^9.0.0", "node-sass": "^9.0.0",
"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": {
@@ -6318,9 +6318,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.31", "version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -13840,9 +13840,9 @@
"integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg=="
}, },
"postcss": { "postcss": {
"version": "8.4.31", "version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"requires": { "requires": {
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",

View File

@@ -30,7 +30,7 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"node-sass": "^9.0.0", "node-sass": "^9.0.0",
"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": {

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

@@ -1,6 +1,7 @@
{% extends override|default:"base_rigs.html" %} {% extends override|default:"base_rigs.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load button from filters %} {% load button from filters %}
{% load verbose_name from filters %}
{% load markdown_tags %} {% load markdown_tags %}
{% block content %} {% block content %}
@@ -30,6 +31,11 @@
<dd>{{ object.three_phase_available|yesno|capfirst }}</dd> <dd>{{ object.three_phase_available|yesno|capfirst }}</dd>
{% endif%} {% endif%}
{% if object.on_campus is not None %}
<dt>{{ object|verbose_name:"on_campus" }}</dt>
<dd>{{ object.on_campus|yesno|capfirst }}</dd>
{% endif%}
{% if object.union_account is not None %} {% if object.union_account is not None %}
<dt>Union Account</dt> <dt>Union Account</dt>
<dd>{{ object.union_account|yesno|capfirst }}</dd> <dd>{{ object.union_account|yesno|capfirst }}</dd>

View File

@@ -78,6 +78,20 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if form.on_campus is not None %}
<div class="form-group form-row">
<div class="col-sm-10 col-sm-offset-2">
<div class="checkbox">
<label>
{% render_field form.on_campus %} {{ form.on_campus.label }}
</label>
</div>
</div>
<div class="alert alert-danger">
<span class="fas fa-exclamation"></span> Selecting this option will add <em>all</em> events at this venue to the calendar viewable by UoN Estates.
</div>
</div>
{% endif %}
{% if form.union_account is not None %} {% if form.union_account is not None %}
<div class="form-group form-row"> <div class="form-group form-row">
<div class="col-sm-10 col-sm-offset-2"> <div class="col-sm-10 col-sm-offset-2">

View File

@@ -78,11 +78,6 @@
</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>
<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.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.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %}</li>{% endfor %}</ul></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -1,33 +1,34 @@
from django.urls import path from django.urls import path
from django.contrib.auth.decorators import login_required
from training.decorators import is_supervisor from training.decorators import is_supervisor
from training import views, models from training import views, models
from versioning.views import VersionHistory from versioning.views import VersionHistory
urlpatterns = [ from PyRIGS.decorators import not_estates
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'), urlpatterns = [
path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()), path('items/', not_estates()(views.ItemList.as_view()), name='item_list'),
path('items/export/', not_estates()(views.ItemListExport.as_view()), name='item_list_export'),
path('item/<int:pk>/qualified_users/', not_estates()(views.ItemQualifications.as_view()), name='item_qualification'),
path('trainee/list/', not_estates()(views.TraineeList.as_view()), name='trainee_list'),
path('trainee/<int:pk>/', not_estates()(views.TraineeDetail.as_view()),
name='trainee_detail'), name='trainee_detail'),
path('trainee/<int:pk>/history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think) path('trainee/<int:pk>/history', not_estates()(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()), path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
name='add_qualification'), name='add_qualification'),
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()), path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
name='edit_qualification'), name='edit_qualification'),
path('levels/', login_required(views.LevelList.as_view()), name='level_list'), path('levels/', not_estates()(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/user/<int:u>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'), path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'), path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'), path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'), path('trainee/<int:pk>/item_record', not_estates()(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'), path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
] ]

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

@@ -5,7 +5,7 @@ from django.urls import path
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from registration.backends.default.views import RegistrationView from registration.backends.default.views import RegistrationView
from PyRIGS.decorators import permission_required_with_403 from PyRIGS.decorators import permission_required_with_403, not_estates
from users import forms, views from users import forms, views
urlpatterns = [ urlpatterns = [
@@ -14,11 +14,11 @@ urlpatterns = [
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'), path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'), path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
# User editing # User editing
path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()), path('user/edit/', not_estates()(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'), name='profile_update_self'),
path('user/reset_api_key', login_required(views.ResetApiKey.as_view(permanent=False)), path('user/reset_api_key', not_estates()(views.ResetApiKey.as_view(permanent=False)),
name='reset_api_key'), name='reset_api_key'),
path('user/', login_required(views.ProfileDetail.as_view()), name='profile_detail'), path('user/', not_estates()(views.ProfileDetail.as_view()), name='profile_detail'),
path('user/<int:pk>/', path('user/<int:pk>/',
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()), permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
name='profile_detail'), name='profile_detail'),