Compare commits

..

39 Commits

Author SHA1 Message Date
dependabot[bot]
731ccc0516 Bump eazy-logger from 4.0.1 to 4.1.0
Bumps [eazy-logger](https://github.com/shakyshane/easy-logger) from 4.0.1 to 4.1.0.
- [Commits](https://github.com/shakyshane/easy-logger/compare/v4.0.1...v4.1.0)

---
updated-dependencies:
- dependency-name: eazy-logger
  dependency-version: 4.1.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-28 18:36:13 +00:00
Joe Banks
fa60c4421f Merge pull request #627 from nottinghamtec/jb3/pipenv-to-uv
Migrate from pipenv to uv
2025-09-28 19:35:06 +01:00
Joe Banks
97b3663909 Add .venv to pycodestyle ignore 2025-09-28 19:28:54 +01:00
Joe Banks
50f6ff4bfd Migrate CI flows to using UV 2025-09-28 19:20:24 +01:00
Joe Banks
0945a893d5 Add uv files 2025-09-28 19:20:11 +01:00
Joe Banks
bc3611db9e Remove pipenv files 2025-09-28 19:20:03 +01:00
Joe Banks
a6c76ee24f no harm in a cheeky CSS animation 2025-09-25 23:29:56 +01:00
Joe Banks
a4ab2992a4 FrankenRIGS 2025-09-25 23:18:42 +01:00
Joe Banks
33ac604d10 Add override functionality to database url via FRANKENRIGS_DATABASE_URL 2025-09-25 23:07:55 +01:00
Joe Banks
9f25fe7bf0 Upgrade psycopg2 and switch to psycopg2-binary 2025-09-25 20:49:56 +01:00
Joe Banks
68b28a6df2 Supposedly fix the zope.interface problem 2025-09-24 20:52:01 +01:00
Joe Banks
b022a0541e Attempt to improve list stylings 2025-09-24 20:25:39 +01:00
Joe Banks
51deadc192 Merge pull request #624 from jamesatjaminit/master
fix: use earliest time instead of start time
2025-09-22 00:59:35 +01:00
James Cook
444f64ddc1 fix: use earliest time instead of start time 2025-09-21 22:40:44 +01:00
Joe Banks
3f38ce77e0 Filter Rigboard context data with new cancelled query parameter 2025-03-30 15:34:21 +01:00
Joe Banks
39a2401ec9 Add button to toggle cancelled event filtering 2025-03-30 15:34:02 +01:00
Joe Banks
032768d3b0 Add day of week to rigboard 2025-03-27 01:16:08 +00:00
Joe Banks
007571fab6 Attempt to fix some of the failing tests 2025-03-17 00:16:44 +00:00
Joe Banks
311189470d Remove erroneous whitespace 2025-03-16 22:59:21 +00:00
Joe Banks
db387c768a Potentially support cmd+P on macOS for printing events 2025-03-16 22:58:16 +00:00
Joe Banks
3c4901f86a Hide MIC when not required 2025-03-16 22:55:17 +00:00
Joe Banks
385a3bd979 Update colours for rigboard 2025-03-16 22:55:07 +00:00
Joe Banks
ded8925ef5 Add needs_mic property to events 2025-03-16 22:54:58 +00:00
Joe Banks
0bb7418f17 Merge pull request #621 from nottinghamtec/jb3/bunch-o-fixes
Bunch o' Fixes
2025-03-16 22:25:05 +00:00
Joe Banks
30a053b813 Update some locked NPM packages (npm audit) 2025-03-16 22:17:49 +00:00
Joe Banks
bd5af7c390 Update interaction tests for only MIC filter 2025-03-16 22:11:00 +00:00
Joe Banks
22bba7e8af Introduce new rigboard template 2025-03-16 22:06:06 +00:00
Joe Banks
dcf19ff773 Don't show full POs in event status 2025-03-16 22:05:54 +00:00
Joe Banks
6e1ac4e203 Create template for each row of new rigboard 2025-03-16 22:05:45 +00:00
Joe Banks
899f958aa3 Store legacy rigboard in new template file 2025-03-16 22:05:35 +00:00
Joe Banks
ee83fd2064 Add link to legacy rigboard on rigboard template 2025-03-16 22:05:27 +00:00
Joe Banks
87dd32da6c Add shortcut to event page for opening PDF with Ctrl+P
Closes #457. You can still use browser default print if you are so
inclined (however it looks trash). This is a quick way to export the
PDF.
2025-03-16 20:05:03 +00:00
Joe Banks
25593f3e6c Surface only_mic in frontend ICS URl generator 2025-03-16 20:00:24 +00:00
Joe Banks
89d50fbd49 Update ICS feed with ?only_mic filter 2025-03-16 19:59:24 +00:00
Joe Banks
7313c905ea Attach user object when using api_key_required decorator 2025-03-16 19:58:56 +00:00
Joe Banks
074d40ea03 Select current user for supervisor field
(Closes #615 and #546)
2025-03-16 19:47:58 +00:00
Joe Banks
1496a75826 Add .venv to gitignore 2025-03-16 19:33:16 +00:00
Joe Banks
1f22470bfd Add missing endif 2025-01-25 23:47:57 +00:00
Joe Banks
47babff6d4 Notes on power test export 2025-01-25 23:43:39 +00:00
30 changed files with 2685 additions and 2903 deletions

View File

@@ -18,34 +18,37 @@ jobs:
- name: Install build dependencies
run: |
sudo apt-get install libcairo2-dev
- name: Set up Python
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: 'pipenv'
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install Dependencies
run: |
python3 -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
run: uv sync --locked --all-extras --dev
- name: Cache Static Files
id: static-cache
uses: actions/cache@v4
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- uses: bahmutov/npm-install@v1
if: steps.static-cache.outputs.cache-hit != 'true'
- run: node node_modules/gulp/bin/gulp build
if: steps.static-cache.outputs.cache-hit != 'true'
- name: Basic Checks
run: |
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
uv run pycodestyle . --exclude=.venv,migrations,node_modules
uv run python3 manage.py check
uv run python3 manage.py makemigrations --check --dry-run
uv run python3 manage.py collectstatic --noinput
- name: Run Tests
run: pipenv run pytest -n auto --cov
run: uv run pytest -n auto --cov
- uses: actions/upload-artifact@v4
if: failure()
with:
@@ -53,4 +56,4 @@ jobs:
path: screenshots/
retention-days: 5
- name: Coveralls
run: pipenv run coveralls --service=github
run: uv run coveralls --service=github

3
.gitignore vendored
View File

@@ -101,3 +101,6 @@ crashlytics.properties
crashlytics-build.properties
.vscode/
screenshots/
# Virutal Environments
.venv/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.10

104
Pipfile
View File

@@ -1,104 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ansicolors = "~=1.1.8"
asgiref = "~=3.3.1"
"backports.tempfile" = "~=1.0"
"backports.weakref" = "~=1.0.post1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=4.0.0"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "*"
gunicorn = "~=22.0.0"
icalendar = "~=4.0.7"
idna = "~=3.7"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=10.0.1"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
requests = "~=2.32.3"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.5.0"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.3"
urllib3 = "~=1.26.19"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
"zope.deprecation" = "~=4.4.0"
"zope.event" = "~=4.5.0"
"zope.hookable" = "~=5.0.1"
"zope.interface" = "~=5.2.0"
"zope.proxy" = "~=4.3.5"
"zope.schema" = "~=6.0.1"
sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=4.9.1"
[dev-packages]
pycodestyle = "~=2.9.1"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.10"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

2309
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -79,7 +79,9 @@ def api_key_required(function):
"""
Decorator for views that checks api_pk and api_key.
Failed users will be given a 403 error.
Should only be used for urls which include <api_pk> and <api_key> kwargs
Should only be used for urls which include <api_pk> and <api_key> kwargs.
Will update the kwargs to include the user object if successful (under the key 'user').
"""
def wrap(request, *args, **kwargs):
@@ -97,6 +99,7 @@ def api_key_required(function):
try:
user_object = models.Profile.objects.get(pk=userid)
kwargs = {**kwargs, 'user': user_object}
except models.Profile.DoesNotExist:
return error_resp

View File

@@ -104,7 +104,10 @@ DATABASES = {
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
if env("FRANKENRIGS_DATABASE_URL") is not None:
DATABASES['default'] = dj_database_url.config(env="FRANKENRIGS_DATABASE_URL")
else:
DATABASES['default'] = dj_database_url.config()
# Logging
LOGGING = {

View File

@@ -379,6 +379,10 @@ class Event(models.Model, RevisionMixin):
return self.pk
return "????"
@property
def needs_mic(self):
return self.is_rig and not self.dry_hire
# Calculated values
"""
EX Vat
@@ -508,7 +512,7 @@ class Event(models.Model, RevisionMixin):
def can_check_in(self):
earliest = self.earliest_time
if isinstance(self.earliest_time, datetime.date):
earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
earliest = datetime.datetime.combine(self.earliest_time, datetime.time(00, 00))
tz = pytz.timezone(settings.TIME_ZONE)
earliest = tz.localize(earliest)
return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now()

View File

@@ -87,6 +87,8 @@
<listStyle name="ul"
start="bulletchar"
leftIndent="0"
bulletDedent="10"
bulletFontSize="10"/>
</stylesheet>

View File

@@ -6,7 +6,36 @@
{% load total_invoices_todo from filters %}
{% block titleheader %}
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
<style>
.franken {
font-family: Fontdiner Swanky;
color: #00ff00;
animation: glow 1.5s infinite alternate;
}
@keyframes glow {
0% {
text-shadow: 0 0 5px #00ff00,
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 40px #00ff00;
}
50% {
text-shadow: 0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 30px #00ff00,
0 0 60px #00ff00;
}
100% {
text-shadow: 0 0 5px #00ff00,
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 40px #00ff00;
}
}
</style>
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/"><span class="franken">Franken</span>RIGS</a>
{% endblock %}
{% block titleelements %}

View File

@@ -3,6 +3,19 @@
{% load markdown_tags %}
{% load static %}
{% block js %}
{{ block.super }}
<script>
$(document).keydown(function(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode == 80) {
window.open("{% url 'event_print' object.pk %}", '_blank');
return false;
}
});
</script>
{% endblock %}
{% block content %}
<div class="row my-3 py-3">
{% if not request.is_ajax %}

View File

@@ -70,6 +70,16 @@
</blockTable>
<spacer length="15"/>
{% if object.notes %}
<hr/>
<spacer length="15"/>
<para><strong>Additional Notes:</strong></para>
<spacer length="15"/>
<para>{{ object.notes }}</para>
<spacer length="15"/>
{% endif %}
<hr/>
<spacer length="15"/>

View File

@@ -0,0 +1,147 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card h-100 border-3 {{ border_class }} event-row">
<div class="card-header {{ header_bg }} {{ header_text }} py-3">
<div class="d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center">
<h5 class="mb-0 mr-3">
<a href="{% url 'event_detail' event.pk %}"
class="{{ header_text }} text-decoration-underline fw-bold">
<strong>{{ event.display_id }}</strong> - {{ event.name }}
</a>
</h5>
{% if event.dry_hire %}
<span class="badge px-3 py-2 rounded-pill fs-6 text-dark bg-light">Dry Hire</span>
{% endif %}
</span>
<span class="badge fs-6 px-3 py-2 bg-light text-dark rounded-pill">{{ event.get_status_display }}</span>
</div>
</div>
<div class="card-body">
<div class="row align-items-start">
<div class="col-md-2 border-end event-dates">
<div class="mb-2">
<small class="text-muted">Meet at:</small>
{% if event.meet_at %}
<p class="mb-1">{{ event.meet_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Access from:</small>
{% if event.access_at %}
<p class="mb-1">{{ event.access_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Start:</small>
<p class="mb-1">
{% if event.start_date and event.start_time %}
{{ event.start_date|date:"D j M Y" }}, {{ event.start_time|date:"H:i" }}
{% elif event.start_date %}
{{ event.start_date|date:"D j M Y" }}
{% elif event.start_time %}
{{ event.start_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
<div class="mb-2">
<small class="text-muted">End:</small>
<p class="mb-1">
{% if event.end_date and event.end_time %}
{{ event.end_date|date:"D j M Y" }}, {{ event.end_time|date:"H:i" }}
{% elif event.end_date %}
{{ event.end_date|date:"D j M Y" }}
{% elif event.end_time %}
{{ event.end_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
</div>
<div class="col-md-10">
<div class="row">
<div class="col-md-6">
{% if event.venue %}
<div class="mb-3">
<small class="text-muted">Venue:</small>
<p class="mb-1">{{ event.venue|namewithnotes:'venue_detail' }}</p>
</div>
{% endif %}
{% if event.is_rig %}
<div class="mb-3">
<small class="text-muted">Client:</small>
<p class="mb-1">
{% if event.person %}
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% endif %}
{% elif event.organisation %}
<a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% else %}
No client specified
{% endif %}
</p>
</div>
{% endif %}
{% if event.mic or event.needs_mic %}
<div class="mb-3">
<small class="text-muted">Member in Charge (MIC):</small>
<div class="d-flex align-items-center mt-1">
{% if event.mic %}
<img src="{{ event.mic.profile_picture }}" alt="{{ event.mic.name }}"
class="rounded-circle mr-1" width="32" height="32">
<span>
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
{{ event.mic.name }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
</span>
{% else %}
<span class="text-danger">No MIC assigned</span>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="mb-3">
<small class="text-muted">Description:</small>
<p class="mb-1">{{ event.description|markdown }}</p>
</div>
<div class="mb-3">
<small class="text-muted">Status:</small>
<div class="mt-1">
{% include "partials/event_status.html" with status=event.status %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% if event.is_rig %}
{% if event.sum_total > 0 %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
<span class="badge badge-success">PO: Received</span>
{% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}

View File

@@ -1,195 +1,70 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
#event_table {
display: grid;
grid-template-columns: max-content min-content minmax(max-content, 1fr) max-content;
column-gap: 1em;
}
.eventgrid {
display: inherit;
grid-column: 1/5;
grid-template-columns: subgrid;
padding: 1em;
dt, dd { display: block; float: left; }
dt { clear: both; }
dd { float: right; }
}
.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;
}
.c-none {
display: none;
}
.c-inline {
display: inline;
}
@container (width <= 500px) {
#event_table {
grid-template-columns: 1fr !important;
.light-link {
color: #ebf5ff !important;
}
.eventgrid {
grid-column: 1/1 !important;
padding: 0.5em;
.dark-link {
color: #4495ff !important;
}
.grid-header {
display: none;
.link-on-green {
color: #ffffff !important;
}
#event_dates {
order: 2;
}
#event_status {
order: 3;
}
#event_mic {
grid-row-start: auto;
grid-column-start: 4;
}
}
@container (width <= 700px) {
#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, .c-md-none {
display: none;
}
}
@container (width > 700px) {
.c-lg-block {
display: block;
}
.c-lg-inline {
display: inline;
}
.c-lg-none, .c-md-none {
display: none;
}
}
</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 %}
<div class="eventgrid {% if event.cancelled %}
table-secondary
<div class="row">
{% for event in events %}
<div class="col-12 mb-4">
{% comment %} Determine card style based on event status {% endcomment %}
{% if event.cancelled %}
{% with border_class="border-secondary" header_bg="bg-secondary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.is_rig %}
table-info
{% with border_class="border-primary" header_bg="bg-primary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.mic %}
table-danger
{% with border_class="border-danger" header_bg="bg-danger" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% with border_class="border-success" header_bg="bg-success" header_text="link-on-green" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% else %}
table-warning
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<div class="font-weight-bold c-none c-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>
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
No events currently scheduled.
</div>
<!---Details-->
<div id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
<span class="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
<br class="c-none c-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="c-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>
{% endfor %}

View File

@@ -0,0 +1,195 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
#event_table {
display: grid;
grid-template-columns: max-content min-content minmax(max-content, 1fr) max-content;
column-gap: 1em;
}
.eventgrid {
display: inherit;
grid-column: 1/5;
grid-template-columns: subgrid;
padding: 1em;
dt, dd { display: block; float: left; }
dt { clear: both; }
dd { float: right; }
}
.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;
}
.c-none {
display: none;
}
.c-inline {
display: inline;
}
@container (width <= 500px) {
#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;
}
}
@container (width <= 700px) {
#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, .c-md-none {
display: none;
}
}
@container (width > 700px) {
.c-lg-block {
display: block;
}
.c-lg-inline {
display: inline;
}
.c-lg-none, .c-md-none {
display: none;
}
}
</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 %}
<div class="eventgrid {% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<div class="font-weight-bold c-none c-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="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
<br class="c-none c-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="c-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>

View File

@@ -4,15 +4,44 @@
{% block content %}
<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">
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 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>
</div>
{% if perms.RIGS.add_event %}
<div class="col text-right">
{% button 'new' 'event_create' %}
</div>
{% endif %}
{% if not request.GET.legacy %}
{% if not request.GET.hide_cancelled %}
<a href="?hide_cancelled=true" class="btn btn-primary mr-3">Hide cancelled</a>
{% else %}
<a href="." class="btn btn-primary mr-3">Show cancelled</a>
{% endif %}
<a href="?legacy=true" class="btn btn-secondary">Legacy rigboard</a>
{% else %}
<a href="." class="btn btn-secondary">New rigboard</a>
{% endif %}
</div>
{% if request.GET.legacy %}
<div class="alert alert-warning">
<strong>Warning:</strong> The legacy rigboard is being deprecated and will be removed in the future. Please use the
new rigboard.
</div>
{% endif %}
<div style="container-type: inline-size;">
{% include 'partials/event_table.html' %}
{% if request.GET.legacy %}
{% include 'partials/legacy_event_table.html' %}
{% else %}
{% include 'partials/event_table.html' %}
{% endif %}
</div>
{% endblock %}

View File

@@ -16,14 +16,14 @@ class Rigboard(BasePage):
URL_TEMPLATE = reverse('rigboard')
_add_item_selector = (By.XPATH, "//a[contains(@class,'btn-primary') and contains(., 'New')]")
_event_row_locator = (By.ID, 'event_row')
_event_row_locator = (By.CLASS_NAME, "event-row")
def add(self):
self.find_element(*self._add_item_selector).click()
class EventListRow(Region):
_event_number_locator = (By.ID, "event_number")
_event_dates_locator = (By.ID, "event_dates")
_event_dates_locator = (By.CLASS_NAME, "event-dates")
_event_details_locator = (By.ID, "event_details")
_event_mic_locator = (By.ID, "event_mic")

View File

@@ -91,8 +91,8 @@ class TestRigboard(BaseRigboardTest):
# self.live_server_url + '/event/create/', self.driver.current_url)
def test_event_order(self):
self.assertIn(self.testEvent.start_date.strftime('%a %d/%m/%Y'), self.page.events[0].dates)
self.assertIn(self.testEvent2.start_date.strftime('%a %d/%m/%Y'), self.page.events[1].dates)
self.assertIn(self.testEvent.start_date.strftime('%-d %b %Y'), self.page.events[0].dates)
self.assertIn(self.testEvent2.start_date.strftime('%-d %b %Y'), self.page.events[1].dates)
def test_add_button(self):
self.page.add()
@@ -530,10 +530,11 @@ class TestCalendar(BaseRigboardTest):
self.page.toggle_filter('cancelled')
self.page.toggle_filter('provisional')
self.page.toggle_filter('confirmed')
self.page.toggle_filter('only_mic')
# and then check the url is correct
self.assertIn(
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false",
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false&only_mic=true",
self.page.cal_url)
# Awesome - all seems to work

View File

@@ -24,6 +24,7 @@ class CalendarICS(ICalFeed):
# Rig = 'rig' = True
# Provisional = 'provisional' = True
# Confirmed/Booked = 'confirmed' = True
# Only MIC = 'mic' = False
def get_object(self, request, *args, **kwargs):
params = {}
@@ -35,6 +36,9 @@ class CalendarICS(ICalFeed):
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
params['confirmed'] = request.GET.get('confirmed', 'true') == 'true'
params['only_mic'] = request.GET.get('only_mic', 'false') == 'true'
params['user'] = kwargs['user']
return params
@@ -73,6 +77,9 @@ class CalendarICS(ICalFeed):
filter = filter & typeFilters & statusFilters
if params['only_mic']:
filter = filter & Q(mic=params['user'])
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
'venue', 'mic')

View File

@@ -41,8 +41,13 @@ class RigboardIndex(generic.TemplateView):
# get super context
context = super().get_context_data(**kwargs)
objects = models.Event.objects.current_events()
if self.request.GET.get('hide_cancelled', False):
objects = objects.exclude(status=models.Event.CANCELLED)
# call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['events'] = objects.select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['page_title'] = "Rigboard"
return context

View File

@@ -244,6 +244,7 @@ class TestSupplierList(AutoLoginTest):
self.page.set_query("")
self.page.search()
time.sleep(1)
self.assertTrue(len(self.page.suppliers) == 7)
self.page.set_query("This is not a supplier")

662
package-lock.json generated

File diff suppressed because it is too large Load Diff

99
pyproject.toml Normal file
View File

@@ -0,0 +1,99 @@
[project]
name = "pyrigs"
version = "0.1.0"
description = "A Django-based event booking system designed for use by TEC PA and Lighting"
readme = "README.md"
requires-python = "~=3.10.0"
dependencies = [
"ansicolors~=1.1.8",
"asgiref~=3.3.1",
"backports.tempfile~=1.0",
"backports.weakref~=1.0.post1",
"beautifulsoup4~=4.9.3",
"Brotli~=1.0.9",
"cachetools~=4.2.1",
"chardet~=4.0.0",
"configparser~=5.0.1",
"contextlib2~=0.6.0.post1",
"cssselect~=1.1.0",
"cssutils~=1.0.2",
"dj-database-url~=0.5.0",
"dj-static~=0.0.6",
"Django~=3.2",
"django-debug-toolbar~=4.0.0",
"django-filter~=2.4.0",
"django-ical~=1.7.1",
"django-recurrence~=1.10.3",
"django-registration-redux~=2.9",
"django-reversion~=3.0.9",
"django-widget-tweaks~=1.4.8",
"django-htmlmin~=0.11.0",
"envparse",
"gunicorn~=22.0.0",
"icalendar~=4.0.7",
"idna~=3.7",
"Markdown~=3.3.3",
"msgpack~=1.0.2",
"pep517~=0.9.1",
"Pillow~=10.0.1",
"premailer~=3.7.0",
"progress~=1.5",
"psutil~=5.8.0",
"psycopg2-binary",
"Pygments~=2.15.0",
"pyparsing~=2.4.7",
"PyPDF2~=1.27.5",
"PyPOM~=2.2.4",
"python-dateutil~=2.8.1",
"pytoml~=0.1.21",
"pytz~=2020.5",
"reportlab",
"requests~=2.32.3",
"retrying~=1.3.3",
"simplejson~=3.17.2",
"six~=1.15.0",
"soupsieve~=2.1",
"sqlparse~=0.5.0",
"static3~=0.7.0",
"svg2rlg~=0.3",
"tini~=3.0.1",
"tornado~=6.3",
"urllib3~=1.26.19",
"whitenoise~=5.2.0",
"yolk~=0.4.3",
"zipp~=3.4.0",
"zope.component~=4.6.2",
"zope.deferredimport~=4.3.1",
"zope.deprecation~=4.4.0",
"zope.event~=4.5.0",
"zope.hookable~=5.0.1",
"zope.proxy~=4.3.5",
"zope.schema~=6.0.1",
"sentry-sdk",
"diff-match-patch",
"python-barcode",
"django-hCaptcha",
"importlib-metadata",
"django-hcaptcha",
"z3c.rml",
"pikepdf",
"django-queryable-properties",
"django-mass-edit",
"selenium~=4.9.1",
"zope.interface",
]
[dependency-groups]
dev = [
"pycodestyle~=2.9.1",
"coveralls",
"django-coverage-plugin",
"pytest-cov",
"pytest-django",
"pluggy",
"pytest-splinter",
"pytest",
"pytest-reverse",
"pytest-xdist[psutil]",
"PyPOM[splinter]",
]

View File

@@ -1,2 +1,3 @@
[pycodestyle]
max-line-length = 320
exclude = .venv

View File

@@ -13,6 +13,10 @@
<meta name="theme-color" content="#3853a4">
<meta name="color-scheme" content="light dark">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fontdiner+Swanky&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" href="{% static 'imgs/pyrigs-avatar.png' %}">
<link rel="apple-touch-icon" href="{% static 'imgs/pyrigs-avatar.png' %}">

View File

@@ -1,6 +1,9 @@
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
{% if supervisor %}
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true"
data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required
data-noclear="true">
{% if supervisor %}
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
{% endif %}
{% endif %}
<option value="{{request.user.pk}}" selected>{{ request.user }}</option>
</select>

View File

@@ -17,8 +17,8 @@ def select_super(page, supervisor):
assert page.supervisor_selector.is_open
page.supervisor_selector.search(supervisor.name[:-6])
time.sleep(2) # Slow down for javascript
page.supervisor_selector.set_option(supervisor.name, True)
assert page.supervisor_selector.options[0].selected
page.supervisor_selector.toggle()
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
@@ -40,6 +40,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.item_selector.toggle()
select_super(page, supervisor)
page.supervisor_selector.toggle()
page.submit()
assert page.success

View File

@@ -126,6 +126,9 @@
<label class="checkbox-inline ml-lg-2">
<input type="checkbox" value="confirmed" data-default="true" checked> Confirmed/Booked
</label>
<label class="checkbox-inline ml-lg-2">
<input type="checkbox" value="only_mic" data-default="false" > Only MIC
</label>
</div>
</form>
</dd>

1651
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff