diff --git a/apps/volunteer/__init__.py b/apps/volunteer/__init__.py index 216258883..1ed1a01b9 100644 --- a/apps/volunteer/__init__.py +++ b/apps/volunteer/__init__.py @@ -41,3 +41,4 @@ def volunteer_variables(): from . import training # noqa: F401 from . import bar_training # noqa: F401 from . import stats # noqa: F401 +from . import cards # noqa: F401 diff --git a/apps/volunteer/cards.py b/apps/volunteer/cards.py new file mode 100644 index 000000000..79b153e0f --- /dev/null +++ b/apps/volunteer/cards.py @@ -0,0 +1,132 @@ +import json +from decorator import decorator +from flask import Response, abort, flash, redirect, render_template, request, url_for, current_app as app +from flask_login import current_user +from wtforms import SelectField, StringField, SubmitField +from wtforms.validators import InputRequired + +from main import db +from apps.common.forms import Form +from apps.common.fields import HiddenIntegerField +from models.user import User +from models.volunteer.card import Card +from . import v_admin_required, volunteer + + +class CardForm(Form): + printer = SelectField("Printer", [InputRequired()], choices=[["volunteer", "Volunteering"], ["hq", "HQ"]]) + type = SelectField( + "Card Type", + [InputRequired()], + choices=[ + ["volunteer", "Volunteer"], + ["orga", "Orga Member"], + ["orga_lead", "Orga Lead"], + ["brown", "Brown Role"], + ["orange", "Orange Role"], + ], + ) + user_id = HiddenIntegerField("User ID", [InputRequired()]) + name = StringField("Preferred Name", [InputRequired()]) + alias = StringField("Alias") + pronouns = StringField("Pronouns", [InputRequired()]) + line_one = StringField("Line One") + line_two = StringField("Line Two") + submit = SubmitField("Print ID") + +def build_card_for(user: User) -> Card: + return Card(volunteer_number=f"2024-{user.id}", name=user.name) + + +def card_for(user: User) -> tuple[CardForm, Card]: + return Card(volunteer_number=f"2024-{user.id}", name=user.name, type="volunteer", printer="volunteer") + + +@volunteer.route("/cards", methods=["GET", "POST"]) +@v_admin_required +def cards(): + if request.method == "POST": + user = User.get_by_email(request.form.get("email")) + if user is None: + flash("No user was found with that email address.") + return redirect(url_for(".cards")) + else: + card = card_for(user) + form = CardForm(obj=card) + form.user_id.data = user.id + return render_template("volunteer/cards/index.html", user=user, form=form) + + return render_template("volunteer/cards/index.html") + + +@volunteer.route("/cards/issue", methods=["POST"]) +@v_admin_required +def issue_card(): + user = User.query.get_or_404(request.form["user_id"]) + + form = CardForm(data=request.form) + form.user_id.data = user.id + if not current_user.has_permission("admin"): + # Force values for non-admins + form.data.type = "volunteer" + form.data.printer = "volunteer" + + if not form.validate(): + return render_template("volunteer/cards/index.html", user=user, form=form) + + card = card_for(user) + form.populate_obj(card) + db.session.add(card) + + db.session.commit() + + flash("Card created") + return redirect(url_for(".cards")) + + +@decorator +def print_key_required(f, *args, **kwargs): + if ( + "X_PRINTER_KEY" not in request.headers + or "PRINTER_KEY" not in app.config + or request.headers["X_PRINTER_KEY"] != app.config["PRINTER_KEY"] + ): + return abort(403) + + return f(*args, **kwargs) + + +@volunteer.route("/cards/queue.json") +@print_key_required +def queue(): + queued_jobs = Card.query.filter_by(state="queued").all() + serialised = [ + { + "job_id": card.id, + "state": card.state, + "volunteer_number": card.volunteer_number, + "printer": card.printer, + "type": card.type, + "name": card.name, + "alias": card.alias, + "pronouns": card.pronouns, + "line_one": card.line_one, + "line_two": card.line_two, + } + for card in queued_jobs + ] + + return Response(json.dumps(serialised), mimetype="application/json") + + +@volunteer.route("/cards//set_state", methods=["POST"]) +@print_key_required +def set_print_job_state(card_id: int): + state = request.json["state"] # type: ignore + card = Card.query.get_or_404(card_id) + card.state = state + + db.session.add(card) + db.session.commit() + + return Response("ok", mimetype="application/json") diff --git a/config/development-example.cfg b/config/development-example.cfg index 69bf3301c..5a675c191 100644 --- a/config/development-example.cfg +++ b/config/development-example.cfg @@ -112,3 +112,6 @@ ETHNICITY_MATCHERS = { ), "other": r"^other$", } + +# Protects the ID printer endpoints. +PRINTER_KEY = "abc123" diff --git a/migrations/versions/35066af0d451_create_volunteer_cards.py b/migrations/versions/35066af0d451_create_volunteer_cards.py new file mode 100644 index 000000000..d3e5d8012 --- /dev/null +++ b/migrations/versions/35066af0d451_create_volunteer_cards.py @@ -0,0 +1,46 @@ +"""create_volunteer_cards + +Revision ID: 35066af0d451 +Revises: fa176a23edd6 +Create Date: 2024-05-26 21:06:16.328165 + +""" + +# revision identifiers, used by Alembic. +revision = '35066af0d451' +down_revision = 'fa176a23edd6' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('volunteer_card', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(), nullable=False), + sa.Column('state', sa.String(), nullable=False), + sa.Column('printer', sa.String(), nullable=False), + sa.Column('volunteer_number', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('alias', sa.String(), nullable=True), + sa.Column('pronouns', sa.String(), nullable=False), + sa.Column('line_one', sa.String(), nullable=False), + sa.Column('line_two', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_volunteer_card')) + ) + op.alter_column('feature_flag_version', 'enabled', + existing_type=sa.BOOLEAN(), + nullable=True, + autoincrement=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('feature_flag_version', 'enabled', + existing_type=sa.BOOLEAN(), + nullable=False, + autoincrement=False) + op.drop_table('volunteer_card') + # ### end Alembic commands ### diff --git a/models/volunteer/__init__.py b/models/volunteer/__init__.py index 9bb31a19c..8a47f154c 100644 --- a/models/volunteer/__init__.py +++ b/models/volunteer/__init__.py @@ -2,3 +2,4 @@ from .role import * # noqa: F401,F403 from .shift import * # noqa: F401,F403 from .volunteer import * # noqa: F401,F403 +from .card import * # noqa: F401,F403 diff --git a/models/volunteer/card.py b/models/volunteer/card.py new file mode 100644 index 000000000..c11c328ab --- /dev/null +++ b/models/volunteer/card.py @@ -0,0 +1,16 @@ +from models import BaseModel +from main import db + + +class Card(BaseModel): + __tablename__ = "volunteer_card" + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String, nullable=False, default="volunteer") + state = db.Column(db.String, nullable=False, default="queued") + printer = db.Column(db.String, nullable=False, default="volunteer") + volunteer_number = db.Column(db.String, nullable=False) + name = db.Column(db.String, nullable=False) + alias = db.Column(db.String, nullable=True) + pronouns = db.Column(db.String, nullable=False) + line_one = db.Column(db.String, nullable=False) + line_two = db.Column(db.String, nullable=False) diff --git a/templates/volunteer/_nav.html b/templates/volunteer/_nav.html index 4c3ca288b..13897fde0 100644 --- a/templates/volunteer/_nav.html +++ b/templates/volunteer/_nav.html @@ -22,6 +22,7 @@ {% endif %} {% if current_user.has_permission('volunteer:admin') %} {{ menuitem('Admin', 'volunteer_admin.index') }} + {{ menuitem('Cards', '.cards')}} {% endif %} {{ menuitem("Log Out", "users.logout") }} diff --git a/templates/volunteer/cards/index.html b/templates/volunteer/cards/index.html new file mode 100644 index 000000000..edbd8cc78 --- /dev/null +++ b/templates/volunteer/cards/index.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% from "_formhelpers.html" import render_field %} + +{% macro search_form() %} +
+
+ + +
+ +
+{% endmacro %} + +{% block title %} + Volunteer Cards +{% endblock %} +{% block body %} +{% include "volunteer/_nav.html" %} +

Card Issuing

+{% if user %} +
+ {{ form.hidden_tag() }} + {% if current_user.has_permission("admin") %} + {{ render_field(form.type) }} + {{ render_field(form.printer) }} + {% endif %} + {{ render_field(form.name) }} + {{ render_field(form.alias) }} + {{ render_field(form.pronouns) }} + {% if current_user.has_permission("admin") %} +

The following fields are only used on role cards.

+ {{ render_field(form.line_one) }} + {{ render_field(form.line_two) }} + {% endif %} + +
+

Search Again

+ {{ search_form() }} +{% else %} +

To issue a volunteer card enter the email address of the person you're + printing for.

+ {{ search_form() }} +{% endif %} +{% endblock %} +