Board row ordering, type-derived row colours, and clearer upcoming events
This commit is contained in:
@@ -140,6 +140,10 @@ def _apply_expiry_fields(t, data):
|
||||
|
||||
_DISPLAY_STYLES = ('standard', 'carousel', 'grid', 'banner')
|
||||
|
||||
# Ceiling on a type's board position. Wide enough to leave gaps between rows
|
||||
# (10, 20, 30 ...) so inserting one later needs no renumbering.
|
||||
_MAX_BOARD_ORDER = 999
|
||||
|
||||
# Ceiling on the per-type post-expiry tail. A day is already far longer than
|
||||
# "recently ended"; anything more is an end time that should have been later.
|
||||
_MAX_GRACE_MINUTES = 1440
|
||||
@@ -164,6 +168,16 @@ def _apply_display_fields(t, data):
|
||||
# Blank stores as NULL: "no category" is the absence of one, not the
|
||||
# empty-string category that every uncategorised type would share.
|
||||
t.boardcategory = category or None
|
||||
if 'boardorder' in data:
|
||||
raw = data.get('boardorder')
|
||||
raw = 100 if raw in (None, '') else raw
|
||||
try:
|
||||
order = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return "boardorder must be a whole number"
|
||||
if order < 0 or order > _MAX_BOARD_ORDER:
|
||||
return "boardorder must be between 0 and %d" % _MAX_BOARD_ORDER
|
||||
t.boardorder = order
|
||||
if 'gracewindowminutes' in data:
|
||||
raw = data.get('gracewindowminutes')
|
||||
raw = 0 if raw in (None, '') else raw
|
||||
@@ -185,11 +199,12 @@ def _config_version():
|
||||
reach pages that are already open."""
|
||||
types = NotificationType.query.order_by(NotificationType.notificationtypeid).all()
|
||||
parts = [
|
||||
"%s|%s|%s|%d|%d|%s|%s|%s|%d|%d|%s" % (
|
||||
"%s|%s|%s|%d|%d|%s|%s|%s|%d|%d|%s|%d" % (
|
||||
t.notificationtypeid, t.typecolor, t.displaystyle,
|
||||
int(bool(t.splitperemployee)), int(bool(t.showemployeephoto)),
|
||||
t.expirymode, t.expirydays, t.expiryhour, int(bool(t.isactive)),
|
||||
int(t.gracewindowminutes or 0), t.boardcategory or '',
|
||||
int(t.boardorder if t.boardorder is not None else 100),
|
||||
)
|
||||
for t in types
|
||||
]
|
||||
@@ -791,6 +806,7 @@ def get_shopfloor_notifications():
|
||||
# blank gives the type a row of its own.
|
||||
'displaystyle': (ntype.displaystyle or 'standard') if ntype else 'standard',
|
||||
'boardcategory': (ntype.boardcategory or '') if ntype else '',
|
||||
'boardorder': int(ntype.boardorder if ntype and ntype.boardorder is not None else 100),
|
||||
}
|
||||
|
||||
# Employee info (photo only when the type wants it)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<span class="mono">{{ t.typecolor }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge">#{{ t.boardorder ?? 100 }}</span>
|
||||
<span class="badge">{{ t.displaystyle || 'standard' }}</span>
|
||||
<span v-if="t.boardcategory" class="badge badge-secondary">{{ t.boardcategory }}</span>
|
||||
</td>
|
||||
@@ -90,6 +91,17 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Board order</span>
|
||||
<input v-model.number="form.boardorder" type="number" min="0" max="999" step="10" />
|
||||
<small class="muted">
|
||||
Where this type's row sits on the shopfloor board, lowest first
|
||||
(Recognition 10, Recertification 20, everything else 100). Leave
|
||||
gaps so a new row can be slotted in without renumbering. Types
|
||||
sharing a category take the lowest order among them.
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Board category (optional)</span>
|
||||
<input v-model="form.boardcategory" type="text" maxlength="50"
|
||||
@@ -230,6 +242,7 @@ function openNew() {
|
||||
expiryhour: null,
|
||||
gracewindowminutes: 0,
|
||||
boardcategory: '',
|
||||
boardorder: 100,
|
||||
isactive: true
|
||||
}
|
||||
editing.value = true
|
||||
@@ -250,6 +263,7 @@ function openEdit(t) {
|
||||
expiryhour: t.expiryhour ?? null,
|
||||
gracewindowminutes: t.gracewindowminutes ?? 0,
|
||||
boardcategory: t.boardcategory || '',
|
||||
boardorder: t.boardorder ?? 100,
|
||||
isactive: t.isactive !== false
|
||||
}
|
||||
editing.value = true
|
||||
|
||||
61
plugins/notifications/migrations/versions/0005_boardorder.py
Normal file
61
plugins/notifications/migrations/versions/0005_boardorder.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Add notificationtypes.boardorder (row order on the shopfloor board).
|
||||
|
||||
Rows were ordered by display style and then type name, so what led the screen
|
||||
was an accident of styling and the alphabet. A site decides its own running
|
||||
order now: low first, and rows sharing a category sort by the lowest order
|
||||
among their types.
|
||||
|
||||
Existing rows default to 100. Recognition and Recertification are seeded ahead
|
||||
of that (10 and 20) so the out-of-the-box board keeps the order sites already
|
||||
expect, without pinning anything a site has since renamed.
|
||||
|
||||
Idempotent; downgrade drops the column.
|
||||
|
||||
Revision ID: notifications0005boardorder
|
||||
Revises: notifications0004category
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = 'notifications0005boardorder'
|
||||
down_revision = 'notifications0004category'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_TABLE = 'notificationtypes'
|
||||
_COLUMN = 'boardorder'
|
||||
|
||||
# Only the two types every site starts with; anything else keeps the default.
|
||||
_SEEDED_ORDER = (('Recognition', 10), ('Recertification', 20))
|
||||
|
||||
|
||||
def _column_names(insp, table):
|
||||
return {c['name'] for c in insp.get_columns(table)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
if _TABLE not in insp.get_table_names():
|
||||
return
|
||||
if _COLUMN in _column_names(insp, _TABLE):
|
||||
return
|
||||
op.add_column(_TABLE, sa.Column(_COLUMN, sa.Integer(), nullable=False,
|
||||
server_default='100'))
|
||||
# Seed the shipped types' order. Guarded on the default so a site that has
|
||||
# already set an order (re-running after a manual add) is left alone.
|
||||
for typename, order in _SEEDED_ORDER:
|
||||
bind.execute(
|
||||
sa.text('UPDATE notificationtypes SET boardorder = :order '
|
||||
'WHERE typename = :typename AND boardorder = 100'),
|
||||
{'order': order, 'typename': typename})
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
if _TABLE not in insp.get_table_names():
|
||||
return
|
||||
if _COLUMN in _column_names(insp, _TABLE):
|
||||
op.drop_column(_TABLE, _COLUMN)
|
||||
@@ -89,6 +89,13 @@ class NotificationType(db.Model):
|
||||
# displaystyle - a banner and a tile row cannot be the same row.
|
||||
boardcategory = db.Column(db.String(50), nullable=True)
|
||||
|
||||
# Where this type's row sits on the shopfloor board, low first. A site
|
||||
# decides what leads the screen - recognition above recertification above
|
||||
# the rest - rather than inheriting an alphabetical or by-style accident.
|
||||
# Rows sharing a category sort by the lowest order among their types.
|
||||
boardorder = db.Column(db.Integer, nullable=False,
|
||||
server_default='100', default=100)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NotificationType {self.typename}>"
|
||||
|
||||
@@ -107,7 +114,8 @@ class NotificationType(db.Model):
|
||||
'showemployeephoto': bool(self.showemployeephoto),
|
||||
'displaystyle': self.displaystyle or 'standard',
|
||||
'gracewindowminutes': int(self.gracewindowminutes or 0),
|
||||
'boardcategory': self.boardcategory or ''
|
||||
'boardcategory': self.boardcategory or '',
|
||||
'boardorder': int(self.boardorder if self.boardorder is not None else 100)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user