Skip to content

Commit 4d2999b

Browse files
Merge pull request #817 from softwaresaved/milestone_2025
Merge milestone_2025 into dev
2 parents e95b349 + 33c8d44 commit 4d2999b

22 files changed

Lines changed: 384 additions & 83 deletions

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.8-slim
22

3-
LABEL maintainer="Philippa Broadbent <p.k.broadbent@soton.ac.uk>"
3+
LABEL maintainer="Mehtap Ozbey Arabaci <m.ozbey-arabaci@soton.ac.uk>"
44

55
ENV PYTHONUNBUFFERED=1
66

lowfat/forms.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from crispy_forms.helper import FormHelper
2525
from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit, HTML, Div
2626
from . import models
27-
from lowfat.models import FUND_STATUS, EXPENSE_STATUS
27+
from lowfat.models import FUND_STATUS, EXPENSE_STATUS, BLOG_POST_STATUS
2828

2929
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
3030

@@ -493,7 +493,7 @@ def __init__(self, *args, **kwargs):
493493
disabled=True,
494494
value=0.00
495495
),
496-
HTML('<h2>Is a Purchase Order Required?</h2><p>If the payment is for a third-party organisation or individual, a purchase order (PO) may be required. If a PO is required, this must be raised <b>before</b> the event takes place.</p><p>For us to raise a PO, we need to know who we are paying (name/address etc.), what we are buying and how much it is going to cost e.g. Catering for an event on 28th May for 20 people, costing £160 plus VAT, provided by XYZ Catering.</p><p>If the supplier already exists on our system, there should be minimal delay in getting the PO issued. If the supplier is a new supplier, the accounts payable team will contact the supplier for additional information, and it can take up to 3 months for the PO to be issued.</p><p>If you are paying an individual to reimburse their out-of-pocket expenses, this is done after the event using a simple claim form, but <b>you</b> need to upload the claim form onto lowFAT as it will be offset against your funding request. It can take up to six weeks for these expenses to be reimbursed.</p><p>If you are paying an individual for their time e.g. 3 hours at £30 per hour to provide training or a workshop, this is treated by the University of Edinburgh as an appointment or engagement and requires an Employment Status Check to be completed <b>before</b> any work is started. It can take up to 3 months for the Employment Status Check to be completed. Any work undertaken before these checks are completed will not be paid. The way the payment is made depends on the outcome of these checks. The person undertaking this work will be required to have registered with HMRC for self assessment tax and must be able to provide a Unique Tax Reference No (UTR). Again, <b>you</b> should upload the claim form to lowFAT as it will be offset against your funding request.</p><p><b>Please make sure you select an option for each of the following questions.</b></p>'),
496+
HTML('<h2>Is a Purchase Order Required?</h2><p>If the payment is for a third-party organisation or individual, a purchase order (PO) may be required. If a PO is required, this must be raised <b>before</b> the event takes place.</p><p>For us to raise a PO, we need to know who we are paying (name/address etc.), what we are buying and how much it is going to cost e.g. Catering for an event on 28th May for 20 people, costing £160 plus VAT, provided by XYZ Catering.</p><p>If the supplier already exists on our system, there should be minimal delay in getting the PO issued. If the supplier is a new supplier, the accounts payable team will contact the supplier for additional information, and it can take up to 2 months for the PO to be issued.</p><p>If you are paying an individual to reimburse their out-of-pocket expenses, this is done after the event using a simple claim form, but <b>you</b> need to upload the claim form onto lowFAT as it will be offset against your funding request. It can take up to six weeks for these expenses to be reimbursed.</p><p>If you are paying an individual for their time e.g. 3 hours at £30 per hour to provide training or a workshop, this is treated by the University of Edinburgh as an appointment or engagement and requires an Employment Status Check to be completed <b>before</b> any work is started. It can take up to 3 months for the Employment Status Check to be completed. Any work undertaken before these checks are completed will not be paid. The way the payment is made depends on the outcome of these checks. The person undertaking this work will be required to have registered with HMRC for self assessment tax and must be able to provide a Unique Tax Reference No (UTR). Again, <b>you</b> should upload the claim form to lowFAT as it will be offset against your funding request.</p><p><b>Please make sure you select an option for each of the following questions.</b></p>'),
497497
# 'direct_invoice',
498498
'fund_payment_receiver',
499499
'fund_claim_method',
@@ -1265,8 +1265,26 @@ class Meta:
12651265
required_css_class = 'form-field-required'
12661266
email = CharField(widget=Textarea, required=False)
12671267

1268-
def __init__(self, *args, **kwargs):
1268+
def __init__(self, *args, user=None, is_staff=False, **kwargs):
12691269
super().__init__(*args, **kwargs)
1270+
self.user = user
1271+
self.is_staff = is_staff
1272+
1273+
if self.is_staff and not self.user.is_superuser:
1274+
self.fields['status'].choices = [(key, label) for key, label in BLOG_POST_STATUS if key != "X"
1275+
]
1276+
if getattr(self.instance, "status", None) == "X":
1277+
self.fields['status'].help_text = (
1278+
"<strong>Warning:</strong> This blog post is currently <em>Removed</em>. "
1279+
"If you submit changes, the status will be updated to the selected value above. "
1280+
"Only admins can re-remove posts."
1281+
)
1282+
else:
1283+
self.fields['status'].help_text = (
1284+
"Setting status to 'Removed' will hide this request from the main dashboards, "
1285+
"but it will still appear under the 'All' tab and in the admin panel. "
1286+
"Use only if this request should be excluded from normal workflows."
1287+
)
12701288

12711289
self.helper.layout = Layout(
12721290
Fieldset(
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Generated by Django 4.2 on 2025-12-18 20:09
2+
3+
from django.db import migrations, models
4+
5+
6+
def normalise_legacy_blog_statuses(apps, schema_editor):
7+
Blog = apps.get_model("lowfat", "Blog")
8+
HistoricalBlog = apps.get_model("lowfat", "HistoricalBlog")
9+
10+
# Legacy -> current mapping
11+
# M, O were old "cancelled-ish" states -> K (Cancelled)
12+
# L was old "waiting to be published" -> map to G (Waiting to be proofread)
13+
legacy_to_current = {
14+
"M": "K",
15+
"O": "K",
16+
"L": "G",
17+
}
18+
19+
for old, new in legacy_to_current.items():
20+
Blog.objects.filter(status=old).update(status=new)
21+
HistoricalBlog.objects.filter(status=old).update(status=new)
22+
23+
24+
def noop_reverse(apps, schema_editor):
25+
# We don't reverse this automatically (would lose information about original legacy code).
26+
pass
27+
28+
29+
class Migration(migrations.Migration):
30+
dependencies = [
31+
("lowfat", "0168_alter_fund_fund_claim_method_and_more"),
32+
]
33+
34+
operations = [
35+
migrations.RunPython(normalise_legacy_blog_statuses, noop_reverse),
36+
37+
migrations.AlterField(
38+
model_name="blog",
39+
name="status",
40+
field=models.CharField(
41+
choices=[
42+
("U", "Waiting for triage"),
43+
("R", "Waiting to be reviewed"),
44+
("C", "Reviewing loop"),
45+
("G", "Waiting to be proofread"),
46+
("P", "Published"),
47+
("K", "Cancelled"),
48+
("D", "Rejected"),
49+
("X", "Removed"),
50+
],
51+
default="U",
52+
max_length=1,
53+
),
54+
),
55+
migrations.AlterField(
56+
model_name="expense",
57+
name="status",
58+
field=models.CharField(
59+
choices=[
60+
("S", "Submitted"),
61+
("P", "Processing"),
62+
("E", "Returned to Claimant for Review/Action"),
63+
("N", "Pending for Approval"),
64+
("A", "Approved"),
65+
("B", "Waiting for Blog Post"),
66+
("R", "Rejected"),
67+
("C", "Cancelled"),
68+
("X", "Removed"),
69+
],
70+
default="S",
71+
max_length=1,
72+
),
73+
),
74+
migrations.AlterField(
75+
model_name="historicalblog",
76+
name="status",
77+
field=models.CharField(
78+
choices=[
79+
("U", "Waiting for triage"),
80+
("R", "Waiting to be reviewed"),
81+
("C", "Reviewing loop"),
82+
("G", "Waiting to be proofread"),
83+
("P", "Published"),
84+
("K", "Cancelled"),
85+
("D", "Rejected"),
86+
("X", "Removed"),
87+
],
88+
default="U",
89+
max_length=1,
90+
),
91+
),
92+
migrations.AlterField(
93+
model_name="historicalexpense",
94+
name="status",
95+
field=models.CharField(
96+
choices=[
97+
("S", "Submitted"),
98+
("P", "Processing"),
99+
("E", "Returned to Claimant for Review/Action"),
100+
("N", "Pending for Approval"),
101+
("A", "Approved"),
102+
("B", "Waiting for Blog Post"),
103+
("R", "Rejected"),
104+
("C", "Cancelled"),
105+
("X", "Removed"),
106+
],
107+
default="S",
108+
max_length=1,
109+
),
110+
),
111+
]

lowfat/models/blog.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
('R', 'Waiting to be reviewed'), # Blog post is assigned to one staff to be reviewed.
1717
('C', 'Reviewing loop'), # Blog post is waiting for another reviewing interaction.
1818
('G', 'Waiting to be proofread'), # Blog post is assigned to be proofread by the community officer.
19-
('L', 'Waiting to be published'), # Blog post will be publish by the community officer.
19+
# ('L', 'Waiting to be published'), # Blog post will be published by the community officer.
2020
('P', 'Published'), # Blog post is published and have a URL at the website.
21-
('M', 'Mistaked'), # Blog post submitted by mistake.
21+
# ('M', 'Mistaked'), # Blog post submitted by mistake.
22+
('K', 'Cancelled'), # New blog post status inline with fund and expense claim model status.
2223
('D', 'Rejected'), # Blog post is rejected for any reason.
23-
('O', 'Out of date'), # Blog post that wait too long to be publish for any reason.
24-
('X', 'Remove'), # When the fellow decided to remove their request.
24+
# ('O', 'Out of date'), # Blog post that wait too long to be published for any reason.
25+
('X', 'Removed'), # When the fellow decided to remove their request...Update: Changed Remove--->Removed to be consistent with fund & expense statuses
2526
)
2627

2728
MAX_CHAR_LENGTH = 120

lowfat/models/expense.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
EXPENSE_STATUS = (
1313
('S', 'Submitted'),
1414
('P', 'Processing'), # Renamed C -> P
15+
('E', 'Returned to Claimant for Review/Action'), # NEW #813, Edit required / Needs action
16+
('N', 'Pending for Approval'), # NEW #813, Next approval step
1517
('A', 'Approved'),
18+
('B', 'Waiting for Blog Post'), # NEW #813
1619
('R', 'Rejected'), # When expense was rejected.
17-
('C', 'Cancelled'), # NEW: Staff can cancel instead of removing
20+
('C', 'Cancelled'), # NEW: Staff can cancel instead of removing, 2025 Release
1821
('X', 'Removed'), # LEGACY: Previously used when a fellow or staff removed an expense. No longer triggered via UI. Currently reserved for superuser use only (e.g. via admin panel).
1922
)
2023

lowfat/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

2121
URL_SRC = "https://github.com/softwaresaved/lowfat"
22-
VERSION = "1.21.0"
22+
VERSION = "1.21.1"
2323

2424
SETTINGS_EXPORT = [
2525
'URL_SRC',

lowfat/templates/lowfat/blog_detail.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
{% extends "lowfat/base.html" %}
2+
{% load blog_edit_permissions %}
3+
{% load blog_extras %}
24

35
{% block content %}
46
<h1>Blog Detail (Funding Request ID: {{ blog.fund.id }})
57
<span
6-
{% if blog.status in 'PDO' %}
8+
{% if blog.status in 'P' %}
79
class="label label-success"
810
{% elif blog.status in 'U' %}
911
class="label label-danger"
12+
{% elif blog.status in "KMO" %}
13+
class="table-light"
14+
{% elif blog.status in "DX" %}
15+
class="table-dark"
1016
{% else %}
1117
class="label label-warning"
1218
{% endif %}
13-
>{{ blog.get_status_display }}</span>
19+
>{{ blog.status|blog_status_label }}</span>
1420
{% if user.is_authenticated %}
15-
{% if user.is_staff %}
21+
{# Staff can only review if the blog is NOT Removed #}
22+
{% if user.is_staff and blog.status != "X" %}
1623
<a title="Review" class="btn btn-outline-dark" href="{% url 'blog_review' blog.id %}" role="button"><span class="fa-solid fa-check" aria-hidden="true"></span> Review</a>
1724
{% endif %}
18-
{% if user.is_staff or blog.status == "U" %}
25+
{% comment %}{% if user.is_staff or blog.status == "U" %}{% endcomment %}
26+
{% if user|can_edit_blog:blog %}
1927
<a title="Edit" class="btn btn-outline-dark" href="{% url 'blog_edit' blog.id %}" role="button"><span class="fa-solid fa-edit" aria-hidden="true"></span> Edit</a>
2028
{% comment %} <a title="Remove" class="btn btn-outline-dark" href="{% url 'blog_remove' blog.id %}?next={% url 'dashboard' %}" role="button"><span class="fa-solid fa-remove" aria-hidden="true"></span> Remove</a> {% endcomment %}
2129
{% endif %}

lowfat/templates/lowfat/blog_review.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{% extends "lowfat/base.html" %}
22
{% load crispy_forms_tags %}
3+
{% load blog_edit_permissions %}
4+
{% load blog_extras %}
35

46
{% block content %}
57
<h1>
68
Blog Review (Funding Request ID: {{ blog.fund.id }})
79
{% if user.is_staff %}
810
<a title="View" class="btn btn-outline-dark" href="{% url 'blog_detail' blog.id %}" role="button"><span class="fa-solid fa-eye" aria-hidden="true"></span> View</a>
911
{% endif %}
10-
{% if user.is_staff %}
12+
{% if user|can_edit_blog:blog %}
1113
<a title="Edit" class="btn btn-outline-dark" href="{% url 'blog_edit' blog.id %}" role="button"><span class="fa-solid fa-edit" aria-hidden="true"></span> Edit</a>
1214
{% comment %} <a title="Remove" class="btn btn-outline-dark" href="{% url 'blog_remove' blog.id %}" role="button"><span class="fa-solid fa-remove" aria-hidden="true"></span> Remove</a> {% endcomment %}
1315
{% endif %}

lowfat/templates/lowfat/blogs.html

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{% load blog_edit_permissions %}
2+
{% load blog_extras %}
13
<h2>
24
Blog Posts
35
{% if not user.is_authenticated and fund.access_token_is_valid %}
@@ -19,18 +21,26 @@ <h2>
1921
{% endif %}
2022
{% if user.is_staff %}
2123
<ul class="nav">
22-
<li {% if blogs_status == 'URCGLPMDOX' %}class="nav-item"{% endif %}>
23-
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=URCGLPMDOX">All</a>
24+
{# === All blogs (everything, including legacy M/O) === #}
25+
<li {% if blogs_status == 'URCGPKLMDOX' %}class="nav-item"{% endif %}>
26+
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=URCGPKLMDOX">All</a>
2427
</li>
28+
{# === Pending (waiting or in-progress) === #}
2529
<li {% if blogs_status == 'URCGL' %}class="nav-item"{% endif %}>
2630
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=URCGL">Pending</a>
2731
</li>
32+
{# === Published only === #}
2833
<li {% if blogs_status == 'P' %}class="nav-item"{% endif %}>
2934
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=P">Published</a>
3035
</li>
31-
<li {% if blogs_status == 'MDOX' %}class="nav-item"{% endif %}>
32-
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=MDOX">Other</a>
36+
{# === Cancelled (new K + legacy M/O) === #}
37+
<li {% if blogs_status == 'KMO' %}class="nav-item"{% endif %}>
38+
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=KMO">Cancelled</a>
3339
</li>
40+
{# === Other (Rejected + Remove) === #}
41+
<li {% if blogs_status == 'DX' %}class="nav-item"{% endif %}>
42+
<a class="nav-link active" href="?funding_requests={{ funding_requests_status }}&expenses={{ expenses_status }}&blogs=DX">Other</a>
43+
</li>
3444
</ul>
3545
{% endif %}
3646
<table class="table table-bordered sortable">
@@ -46,26 +56,22 @@ <h2>
4656
<th>
4757
Fellow
4858
</th>
59+
{% endif %}
4960
<th>
5061
Submitted date
5162
</th>
52-
{% endif %}
5363
<th>
5464
Blog
5565
</th>
5666
<th>
5767
Funding Request
5868
</th>
59-
{% endif %}
60-
{% if user.is_staff or claimant and user == claimant.user or fund.claimant and user == fund.claimant.user %}
6169
<th>
6270
Status
6371
</th>
64-
{% endif %}
6572
<th>
6673
Publish date
6774
</th>
68-
{% if user.is_staff or claimant and user == claimant.user or fund.claimant and user == fund.claimant.user %}
6975
<th>
7076
Actions
7177
</th>
@@ -75,10 +81,14 @@ <h2>
7581
{% for blog in blogs %}
7682
<tr
7783
{% if user.is_staff or claimant and user == claimant.user %}
78-
{% if blog.status in 'PDO' %}
84+
{% if blog.status in 'P' %}
7985
class="table-success"
8086
{% elif blog.status in 'U' %}
8187
class="table-danger"
88+
{% elif blog.status in "KMO" %}
89+
class="table-light"
90+
{% elif blog.status in "DX" %}
91+
class="table-secondary"
8292
{% else %}
8393
class="table-warning"
8494
{% endif %}
@@ -106,23 +116,28 @@ <h2>
106116
</a>
107117
{% endif %}
108118
{% else %}
109-
{% if user.is_staff %}
119+
{% if user.is_staff and blog.status != "X" %}
110120
<a title="Review" href="{% url 'blog_review' blog.id %}">{{ blog.draft_url }}</a>
111121
{% else %}
112122
<a href="{{ blog.draft_url }}">{{ blog.draft_url }}</a>
113123
{% endif %}
114124
{% endif %}
115125
</td>
116-
{% if user.is_staff %}
126+
{% if user.is_staff or claimant and user == claimant.user or fund.claimant and user == fund.claimant.user %}
117127
<td>
118128
{% if blog.fund %}
129+
{% if user.is_staff or blog.fund.claimant.user == user %}
119130
<a href="{% url 'fund_detail' blog.fund.id %}">{{ blog.fund.title }}</a>
131+
{% else %}
132+
{{ blog.fund.title }}
120133
{% endif %}
121-
</td>
122134
{% endif %}
135+
</td>
136+
{% endif %}
123137
{% if user.is_staff or claimant and user == claimant.user or fund.claimant and user == fund.claimant.user %}
124138
<td>
125-
{{ blog.get_status_display }}
139+
{% comment %} {{ blog.get_status_display }}{% endcomment %}
140+
{{ blog.status|blog_status_label }}
126141
{% if blog.status in "RCGL" and blog.reviewer %}
127142
(Please contact <a href="mailto:{{ blog.reviewer.email }}">{{ blog.reviewer.get_full_name }}</a>.)
128143
{% endif %}
@@ -143,10 +158,10 @@ <h2>
143158
<a class="icon" href="mailto:{{ blog.author.email }}"><i class="fa-solid fa-envelope" aria-hidden="true"></i></a>
144159
{% endif %}
145160
<a title="View" href="{% url 'blog_detail' blog.id %}"><span class="fa-solid fa-eye" aria-hidden="true"></span></a>
146-
{% if user.is_staff %}
161+
{% if user.is_staff and blog.status != "X" %}
147162
<a title="Review" href="{% url 'blog_review' blog.id %}"><span class="fa-solid fa-check" aria-hidden="true"></span></a>
148163
{% endif %}
149-
{% if user.is_staff or blog.status == "U" %}
164+
{% if user|can_edit_blog:blog %}
150165
<a title="Edit" href="{% url 'blog_edit' blog.id %}"><span class="fa-solid fa-edit" aria-hidden="true"></span></a>
151166
{% comment %} <a title="Remove" href="{% url 'blog_remove' blog.id %}?next={{ request.path }}"><span class="fa-solid fa-remove" aria-hidden="true"></span></a> {% endcomment %}
152167
{% endif %}

0 commit comments

Comments
 (0)