mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-09 00:09:44 +00:00
Compare commits
11 Commits
dependabot
...
90ae87b1b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
90ae87b1b2
|
|||
|
0b22669a29
|
|||
|
020d43d5f8
|
|||
|
b4c1ada05c
|
|||
|
aff911493f
|
|||
|
020b08f9b0
|
|||
|
aae9a45d82
|
|||
| 773c07af81 | |||
| 0cbba9d436 | |||
| d47322a959 | |||
| 74b8948daa |
11
.github/workflows/django.yml
vendored
11
.github/workflows/django.yml
vendored
@@ -12,11 +12,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: 'pipenv'
|
||||
@@ -27,7 +26,7 @@ jobs:
|
||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||
- name: Cache Static Files
|
||||
id: static-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'pipeline/built_assets'
|
||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||
@@ -42,8 +41,8 @@ jobs:
|
||||
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||
pipenv run python3 manage.py collectstatic --noinput
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest -n auto --cov
|
||||
- uses: actions/upload-artifact@v4
|
||||
run: pipenv run pytest -n auto -vv --cov
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: failure-screenshots ${{ matrix.test-group }}
|
||||
|
||||
18
Pipfile
18
Pipfile
@@ -28,18 +28,18 @@ django-reversion = "~=3.0.9"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
django-htmlmin = "~=0.11.0"
|
||||
envparse = "*"
|
||||
gunicorn = "~=22.0.0"
|
||||
gunicorn = "~=20.0.4"
|
||||
icalendar = "~=4.0.7"
|
||||
idna = "~=3.7"
|
||||
idna = "~=2.10"
|
||||
Markdown = "~=3.3.3"
|
||||
msgpack = "~=1.0.2"
|
||||
pep517 = "~=0.9.1"
|
||||
Pillow = "~=10.0.1"
|
||||
Pillow = "~=9.3.0"
|
||||
premailer = "~=3.7.0"
|
||||
progress = "~=1.5"
|
||||
psutil = "~=5.8.0"
|
||||
psycopg2 = "~=2.8.6"
|
||||
Pygments = "~=2.15.0"
|
||||
Pygments = "~=2.7.4"
|
||||
pyparsing = "~=2.4.7"
|
||||
PyPDF2 = "~=1.27.5"
|
||||
PyPOM = "~=2.2.4"
|
||||
@@ -47,20 +47,20 @@ python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
pytz = "~=2020.5"
|
||||
reportlab = "*"
|
||||
requests = "~=2.32.0"
|
||||
requests = "~=2.25.1"
|
||||
retrying = "~=1.3.3"
|
||||
simplejson = "~=3.17.2"
|
||||
six = "~=1.15.0"
|
||||
soupsieve = "~=2.1"
|
||||
sqlparse = "~=0.5.0"
|
||||
sqlparse = "~=0.4.2"
|
||||
static3 = "~=0.7.0"
|
||||
svg2rlg = "~=0.3"
|
||||
tini = "~=3.0.1"
|
||||
tornado = "~=6.3"
|
||||
urllib3 = "~=1.26.19"
|
||||
tornado = "~=6.1"
|
||||
urllib3 = "~=1.26.5"
|
||||
whitenoise = "~=5.2.0"
|
||||
yolk = "~=0.4.3"
|
||||
zipp = "~=3.19.1"
|
||||
zipp = "~=3.4.0"
|
||||
"zope.component" = "~=4.6.2"
|
||||
"zope.deferredimport" = "~=4.3.1"
|
||||
"zope.deprecation" = "~=4.4.0"
|
||||
|
||||
814
Pipfile.lock
generated
814
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,6 @@ if DEBUG:
|
||||
ALLOWED_HOSTS.append('localhost')
|
||||
ALLOWED_HOSTS.append('example.com')
|
||||
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')
|
||||
if not DEBUG:
|
||||
|
||||
@@ -48,7 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
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())
|
||||
return context
|
||||
|
||||
|
||||
@@ -134,15 +134,11 @@ class SecureAPIRequest(generic.View):
|
||||
results = []
|
||||
query = reduce(operator.and_, queries)
|
||||
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:
|
||||
name = o.display_name if hasattr(o, 'display_name') else o.name
|
||||
data = {
|
||||
'pk': o.pk,
|
||||
'value': o.pk,
|
||||
'text': name,
|
||||
'text': o.name,
|
||||
}
|
||||
try: # See if there is a valid update URL
|
||||
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
||||
@@ -187,7 +183,7 @@ class ModalURLMixin:
|
||||
url = reverse_lazy('closemodal')
|
||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
||||
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
|
||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
||||
else:
|
||||
url = reverse_lazy(detail, kwargs={
|
||||
'pk': self.object.pk,
|
||||
|
||||
@@ -11,9 +11,8 @@ For setup information and other such helpful stuff check the [Wiki](https://gith
|
||||
- PyRIGS: Base app, stores 'global' information
|
||||
- RIGS: Rigboard stuff - event calendar etc
|
||||
- assets: Database of our kit, testing data etc
|
||||
- training: Logs in-house training within various "departments" (sound, lighting etc).
|
||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||
|
||||
- training: SoonTM
|
||||
|
||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||
|
||||
@@ -154,9 +154,8 @@ class AssociateAdmin(VersionAdmin):
|
||||
|
||||
@admin.register(models.Profile)
|
||||
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_filter = UserAdmin.list_filter + ('is_approved',)
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {
|
||||
|
||||
@@ -121,7 +121,7 @@ class EventForm(forms.ModelForm):
|
||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||
'purchase_order', 'collector', 'forum_url']
|
||||
'purchase_order', 'collector']
|
||||
|
||||
|
||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-29 10:23
|
||||
# Generated by Django 3.2.19 on 2023-05-20 10:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
@@ -43,7 +43,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
33
RIGS/migrations/0050_auto_20230520_1520.py
Normal file
33
RIGS/migrations/0050_auto_20230520_1520.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-20 14:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0049_auto_20230520_1136'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-06-27 11:28
|
||||
|
||||
import RIGS.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0049_auto_20230529_1123'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='forum_url',
|
||||
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -76,8 +76,7 @@ class Profile(AbstractUser):
|
||||
|
||||
@classmethod
|
||||
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(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count()
|
||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -309,14 +308,6 @@ class EventManager(models.Manager):
|
||||
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'])
|
||||
class Event(models.Model, RevisionMixin):
|
||||
# Done to make it much nicer on the database
|
||||
@@ -366,8 +357,6 @@ class Event(models.Model, RevisionMixin):
|
||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||
auth_request_to = models.EmailField(blank=True, default='')
|
||||
|
||||
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
if self.pk:
|
||||
@@ -508,7 +497,7 @@ class Event(models.Model, RevisionMixin):
|
||||
earliest = datetime.datetime.combine(self.start_date, 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()
|
||||
return not self.dry_hire and earliest <= timezone.now()
|
||||
|
||||
objects = EventManager()
|
||||
|
||||
@@ -516,7 +505,7 @@ class Event(models.Model, RevisionMixin):
|
||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_id} | {self.name}"
|
||||
return f"{self.display_id}: {self.name}"
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
@@ -688,11 +677,13 @@ class Payment(models.Model, RevisionMixin):
|
||||
CASH = 'C'
|
||||
INTERNAL = 'I'
|
||||
EXTERNAL = 'E'
|
||||
SUCORE = 'SU'
|
||||
ADJUSTMENT = 'T'
|
||||
METHODS = (
|
||||
(CASH, 'Cash'),
|
||||
(INTERNAL, 'Internal'),
|
||||
(EXTERNAL, 'External'),
|
||||
(SUCORE, 'SU Core'),
|
||||
(ADJUSTMENT, 'TEC Adjustment'),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
import datetime
|
||||
|
||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||
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():
|
||||
context = {
|
||||
'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(),
|
||||
'to_name': admin.first_name
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
firstDay: 1,
|
||||
themeSystem: 'bootstrap',
|
||||
aspectRatio: 1.5,
|
||||
eventTimeFormat: {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12" style="container-type: inline-size;">
|
||||
<div class="col-sm-12">
|
||||
{% with object_list as events %}
|
||||
{% include 'partials/event_table.html' %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
<div class="col-sm-9 col-md-7 col-lg-8">
|
||||
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
||||
{% if venue %}
|
||||
<option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option>
|
||||
<option value="{{venue.value}}" selected="selected" data-update_url="{% url 'venue_update' venue.value %}">{{ venue }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -231,7 +231,7 @@
|
||||
<label for="{{ form.start_date.id_for_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="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
||||
{% render_field form.start_date class+="form-control" %}
|
||||
@@ -246,7 +246,7 @@
|
||||
<label for="{{ form.end_date.id_for_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="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" %}
|
||||
@@ -334,26 +334,12 @@
|
||||
|
||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||
<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">
|
||||
{% render_field form.purchase_order class+="form-control" %}
|
||||
</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>
|
||||
|
||||
@@ -165,11 +165,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-right">
|
||||
{% button 'edit' url='pt_edit' pk=object.pk %}
|
||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
|
||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
|
||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||
</div>
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
<div class="row collapse" id="values">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -33,20 +33,17 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="row">Distro</th>
|
||||
<th scope="row">Max PSCC with Single Phase Supply (kA)</th>
|
||||
<th scope="row">Max PSCC with Three Phase Supply (kA)</th>
|
||||
<th scope="row">Max PSSC (kA)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Intel & Toblerone distros</td>
|
||||
<td>6</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>All other distros</td>
|
||||
<td>10</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -77,15 +77,6 @@
|
||||
<dt class="col-sm-6">PO</dt>
|
||||
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-6">Forum Thread</dt>
|
||||
{% if object.forum_url %}
|
||||
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
|
||||
{% else %}
|
||||
<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 %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
{% if event.is_rig %}
|
||||
{% if event.sum_total > 0 %}
|
||||
|
||||
@@ -1,195 +1,105 @@
|
||||
{% 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 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">MIC</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr class="{% 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-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--Dates & Times-->
|
||||
<td id="event_dates" style="text-align: justify;">
|
||||
{% if not event.cancelled %}
|
||||
{% if event.meet_at %}
|
||||
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||
{% endif %}
|
||||
{% if event.access_at %}
|
||||
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<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>
|
||||
<a href="{% url 'event_detail' event.pk %}">
|
||||
{{ event.name }}
|
||||
</a>
|
||||
{% if event.venue %}
|
||||
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
|
||||
{% endif %}
|
||||
{% if event.dry_hire %}
|
||||
<span class="badge badge-secondary">Dry Hire</span>
|
||||
{% 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 %}
|
||||
{% include 'partials/event_status.html' %}
|
||||
</td>
|
||||
<!---MIC-->
|
||||
<td id="event_mic" class="text-nowrap">
|
||||
{% 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-user-slash"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="bg-warning">
|
||||
<td colspan="4">No events found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -29,15 +29,7 @@
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<label class="col-sm-4 col-form-label"
|
||||
for="{{ form.method.id_for_label }}">{{ form.method.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>
|
||||
for="{{ form.method.id_for_label }}">{{ form.method.label }}</label>
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.method class+="form-control" %}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
{% 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>
|
||||
<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">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>
|
||||
{% if perms.RIGS.add_event %}
|
||||
<div class="col text-right">
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="container-type: inline-size;">
|
||||
|
||||
{% include 'partials/event_table.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -372,7 +372,7 @@ def test_ra_redirect(admin_client, admin_user, ra):
|
||||
|
||||
|
||||
class TestMarkdownTemplateTags(TestCase):
|
||||
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f:
|
||||
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")) as f:
|
||||
markdown = f.read()
|
||||
|
||||
def test_html_safe(self):
|
||||
|
||||
@@ -110,9 +110,6 @@ urlpatterns = [
|
||||
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
|
||||
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/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
|
||||
|
||||
# Finance
|
||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
||||
name='invoice_list'),
|
||||
|
||||
@@ -53,6 +53,12 @@ class EventRiskAssessmentCreate(HSCreateView):
|
||||
def get_success_url(self):
|
||||
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if context['event'].mic:
|
||||
context['power_mic'] = context['event'].mic
|
||||
return context
|
||||
|
||||
|
||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
model = models.RiskAssessment
|
||||
@@ -166,7 +172,7 @@ class PowerTestEdit(generic.UpdateView):
|
||||
ec.reviewed_by = None
|
||||
ec.reviewed_at = None
|
||||
ec.save()
|
||||
return reverse('pt_detail', kwargs={'pk': self.object.pk})
|
||||
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -214,7 +220,7 @@ class HSList(generic.ListView):
|
||||
template_name = 'hs/hs_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
@@ -3,12 +3,6 @@ import datetime
|
||||
import re
|
||||
import premailer
|
||||
import simplejson
|
||||
import urllib
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
from envparse import env
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -25,7 +19,6 @@ from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from PyRIGS import decorators
|
||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
||||
@@ -384,41 +377,3 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
context['to_name'] = self.request.GET.get('to_name', None)
|
||||
context['target'] = 'event_authorise_form_preview'
|
||||
return context
|
||||
|
||||
|
||||
class CreateForumThread(generic.base.RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
event = get_object_or_404(models.Event, pk=kwargs['pk'])
|
||||
|
||||
if event.forum_url:
|
||||
return event.forum_url
|
||||
|
||||
params = {
|
||||
'title': str(event),
|
||||
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
|
||||
'category': 'rig-info'
|
||||
}
|
||||
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
|
||||
|
||||
|
||||
class RecieveForumWebhook(generic.View):
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
|
||||
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
|
||||
return HttpResponseForbidden('Invalid signature header')
|
||||
# Check if this is the right kind of event. The webhook filters by category on the forum side
|
||||
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()
|
||||
if event:
|
||||
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
|
||||
event.save()
|
||||
return HttpResponse(status=202)
|
||||
return HttpResponse(status=204)
|
||||
|
||||
4
app.json
4
app.json
@@ -4,7 +4,7 @@
|
||||
"scripts": {
|
||||
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
|
||||
},
|
||||
"stack": "heroku-22",
|
||||
"stack": "heroku-20",
|
||||
"env": {
|
||||
"DEBUG": {
|
||||
"required": true
|
||||
@@ -51,7 +51,7 @@
|
||||
"url": "heroku/nodejs"
|
||||
},
|
||||
{
|
||||
"url": "heroku/python"
|
||||
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-24 22:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0027_asset_nickname'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='m', max_digits=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -135,7 +135,7 @@ class Asset(models.Model, RevisionMixin):
|
||||
# Cable assets
|
||||
is_cable = models.BooleanField(default=False)
|
||||
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
|
||||
length = models.DecimalField(decimal_places=2, max_digits=10,
|
||||
length = models.DecimalField(decimal_places=1, max_digits=10,
|
||||
blank=True, null=True, help_text='m')
|
||||
csa = models.DecimalField(decimal_places=2, max_digits=10,
|
||||
blank=True, null=True, help_text='mm²')
|
||||
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
|
||||
return str(self.asset_id)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
def name(self):
|
||||
return f"{self.display_id} | {self.description}"
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
function onAuditClick(assetID) {
|
||||
$('#' + assetID).remove();
|
||||
}
|
||||
$('#modal').on('hidden.bs.modal', function (e) {
|
||||
searchbar = document.getElementById('id_q');
|
||||
searchbar.value = "";
|
||||
setTimeout(searchbar.focus(), 2000);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
<script src="{% static 'js/easymde.min.js' %}"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
@@ -34,10 +35,9 @@
|
||||
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_comments');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% for item in object_list %}
|
||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow" id="{{ item.asset_id }}">
|
||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
|
||||
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
|
||||
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
|
||||
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.purchase_price|add_class:'form-control'|set_data:"toggle:tooltip" value=object.purchase_price title="Ex. VAT" %}
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.replacement_cost|add_class:'form-control'|set_data:"toggle:tooltip" value=object.replacement_cost title="Ex. VAT" %}
|
||||
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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)
|
||||
yield asset
|
||||
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()
|
||||
|
||||
@@ -77,7 +77,7 @@ class AssetForm(FormPage):
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.TextBox, (By.ID, 'id_comments')),
|
||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
@@ -54,45 +53,45 @@ class TestAssetList(AutoLoginTest):
|
||||
self.assertEqual("10", asset_ids[2])
|
||||
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)
|
||||
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type):
|
||||
page = pages.AssetList(logged_in_browser.driver, live_server.url).open()
|
||||
page.set_query(test_asset.asset_id)
|
||||
page.search()
|
||||
assert len(page.assets) == 1
|
||||
assert page.assets[0].description == test_asset.description
|
||||
assert page.assets[0].id == test_asset.asset_id
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
page.set_query(test_asset.description)
|
||||
page.search()
|
||||
assert len(page.assets) == 1
|
||||
assert page.assets[0].description == test_asset.description
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
page.set_query("Random string")
|
||||
page.search()
|
||||
assert len(page.assets) == 0
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
page.set_query("")
|
||||
page.search()
|
||||
# Only working stuff shown by default
|
||||
assert len(page.assets) == 1
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
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()
|
||||
assert page.status_selector.is_open
|
||||
page.status_selector.select_all()
|
||||
page.status_selector.toggle()
|
||||
assert not page.status_selector.is_open
|
||||
page.filter()
|
||||
assert len(page.assets) == 2
|
||||
|
||||
page.category_selector.toggle()
|
||||
assert page.category_selector.is_open
|
||||
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
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.filter()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("10", asset_ids[1])
|
||||
|
||||
|
||||
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
|
||||
|
||||
@@ -16,7 +16,7 @@ const con = require('gulp-concat');
|
||||
const gulpif = require('gulp-if');
|
||||
|
||||
function fonts(done) {
|
||||
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*', { encoding: false })
|
||||
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*')
|
||||
.pipe(gulp.dest('pipeline/built_assets/fonts'))
|
||||
.pipe(browsersync.stream());
|
||||
}
|
||||
@@ -70,16 +70,16 @@ function scripts() {
|
||||
.pipe(gulpif(function(file) { return interaction.includes(file.relative);}, con('interaction.js')))
|
||||
.pipe(gulpif(function(file) { return jpop.includes(file.relative);}, con('jpop.js')))
|
||||
.pipe(flatten())
|
||||
// Only minify if filename does not already denote it as minified
|
||||
.pipe(gulpif(function(file) { return file.path.indexOf("min") == -1;},terser()))
|
||||
.pipe(terser())
|
||||
.pipe(gulp.dest(dest))
|
||||
.pipe(browsersync.stream());
|
||||
}
|
||||
|
||||
function browserSync(done) {
|
||||
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
|
||||
// TODO Wait for Django server to come up before browsersync, it seems inconsistent
|
||||
browsersync.init({
|
||||
notify: true,
|
||||
notify: false,
|
||||
open: false,
|
||||
port: 8001,
|
||||
proxy: '127.0.0.1:8000'
|
||||
|
||||
9564
package-lock.json
generated
9564
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"cssnano": "^5.0.13",
|
||||
"easymde": "^2.16.1",
|
||||
"fullcalendar": "^5.10.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-flatten": "^0.4.0",
|
||||
"gulp-if": "^3.0.0",
|
||||
@@ -27,14 +28,13 @@
|
||||
"jquery": "^3.6.0",
|
||||
"konami": "^1.6.3",
|
||||
"moment": "^2.29.4",
|
||||
"node-sass": "^9.0.0",
|
||||
"node-sass": "^7.0.3",
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.5",
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^3.0.2",
|
||||
"gulp": "^5.0.0"
|
||||
"browser-sync": "^2.27.11"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
|
||||
@@ -6,6 +6,11 @@ function setupItemTable(items_json) {
|
||||
newitem = -1;
|
||||
}
|
||||
|
||||
function nl2br(str, is_xhtml) {
|
||||
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
|
||||
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return $('<div/>').text(str).html();
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ jQuery(document).ready(function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
var easter_egg = new Konami(function () {
|
||||
var easter_egg = new Konami();
|
||||
easter_egg.code = function () {
|
||||
var s = document.createElement('script');
|
||||
s.type = 'text/javascript';
|
||||
document.body.appendChild(s);
|
||||
s.src = '/static/js/asteroids.min.js';
|
||||
});
|
||||
s.src = '{% static "js/asteroids.min.js"%}';
|
||||
ga('send', 'event', 'easter_egg', 'activated');
|
||||
}
|
||||
easter_egg.load();
|
||||
});
|
||||
//CTRL-Enter form submission
|
||||
|
||||
@@ -77,8 +77,17 @@
|
||||
border-collapse: separate !important;
|
||||
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 {
|
||||
table.table-#{$color} {
|
||||
.table-#{$color} {
|
||||
> td,th {
|
||||
border: 0.3em solid theme-color-level($color, -6) !important;
|
||||
}
|
||||
@@ -87,11 +96,6 @@
|
||||
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 {
|
||||
color: black;
|
||||
@@ -152,7 +156,4 @@
|
||||
.modal {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +281,3 @@ html.embedded {
|
||||
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
// New implementation of class dropped in Bootstrap 3
|
||||
.dl-horizontal {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.7rem 0;
|
||||
}
|
||||
.dl-horizontal > dd, .dl-horizontal .markdown > p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
e.preventDefault();
|
||||
data = $(this).serialize();
|
||||
action = $(this).attr('action');
|
||||
console.log(action)
|
||||
$.post(action, data, function(resp) {
|
||||
$('#modal').html(resp);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% if now %}
|
||||
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -105,10 +105,6 @@ class TrainingItem(models.Model):
|
||||
def display_id(self):
|
||||
return f"{self.category.reference_number}.{self.reference_number}"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f"{self.display_id} | {self.name}"
|
||||
|
||||
@display_id.filter
|
||||
@classmethod
|
||||
def display_id(cls, lookup, value):
|
||||
|
||||
@@ -78,10 +78,10 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<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>
|
||||
<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 %} {% 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 %} {% 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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<p>Please Note:</p>
|
||||
<ul>
|
||||
<li>Technical Assistant status is automatically valid when the item requirements are met.</li>
|
||||
<li>Technician status is also automatic. Notification of completion should be made at the next general meeting.</li>
|
||||
<li>Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system.</li>
|
||||
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
|
||||
</ul>
|
||||
<sup>Correct as of 24th May 2023, check the Training Policy.</sup>
|
||||
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for level in object_list %}
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
page.submit()
|
||||
assert page.success
|
||||
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
|
||||
assert qualification.supervisor_id == supervisor.pk
|
||||
assert qualification.supervisor.pk == supervisor.pk
|
||||
assert qualification.date == date
|
||||
assert qualification.notes == "A note"
|
||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
|
||||
assert response.status_code == 302
|
||||
qual = models.TrainingItemQualification.objects.last()
|
||||
assert qual is not None
|
||||
assert training_item.pk == qual.item_id
|
||||
assert training_item.pk == qual.item.pk
|
||||
# Ensure only one revision has been created
|
||||
assert Revision.objects.count() == 1
|
||||
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
|
||||
|
||||
@@ -265,5 +265,5 @@ class ItemQualifications(generic.ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"People Qualified In {models.TrainingItem.objects.get(pk=self.kwargs['pk'])}"
|
||||
context["page_title"] = f"People Qualified In {self.object_list[0].item}"
|
||||
return context
|
||||
|
||||
@@ -12,8 +12,8 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for person in Profile.objects.all():
|
||||
# Inactivate users that have not logged in for a year
|
||||
if person.last_login is not None and (timezone.now() - person.last_login).days > 365:
|
||||
# Inactivate users that have not logged in for a year (or have never logged in)
|
||||
if person.last_login is None or (timezone.now() - person.last_login).days > 365:
|
||||
person.is_active = False
|
||||
person.is_approved = False
|
||||
person.save()
|
||||
|
||||
@@ -167,11 +167,9 @@
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Events</div>
|
||||
<div style="container-type: size; height: 30vh; overflow-y: scroll;">
|
||||
{% with object.latest_events as events %}
|
||||
{% include 'partials/event_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ for app in [apps.get_app_config(label) for label in ("RIGS", "assets", "training
|
||||
else:
|
||||
urlpatterns += [
|
||||
path(f'{appname}/{modelname}/<str:pk>/history/',
|
||||
permission_required_with_403(f'{app.label}.change_{modelname}')(
|
||||
permission_required_with_403('{app.label}.change_{modelname}')(
|
||||
views.VersionHistory.as_view()),
|
||||
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
|
||||
]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from diff_match_patch import diff_match_patch
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -148,9 +147,9 @@ class ModelComparison:
|
||||
|
||||
@cached_property
|
||||
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:
|
||||
from RIGS.models import EventAuthorisation
|
||||
from training.models import TrainingLevelQualification, TrainingItemQualification
|
||||
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)) \
|
||||
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
|
||||
@@ -161,14 +160,10 @@ class ModelComparison:
|
||||
# Build some dicts of what we have
|
||||
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
|
||||
if version._model is None:
|
||||
continue
|
||||
compare = ModelComparison(old=version._object_version.object, **comparisonParams)
|
||||
item_dict[version.object_id] = compare
|
||||
|
||||
for version in new_item_versions: # go through the new versions
|
||||
if version._model is None:
|
||||
continue
|
||||
try:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user