Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
".venv/**": true,
"**/__pycache__/**": true
},
"python.formatting.provider": "none",
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter"
Expand Down
112 changes: 112 additions & 0 deletions migrations/versions/0f07a3edd643_role_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Role System

Revision ID: 0f07a3edd643
Revises: 9b0e597da71c
Create Date: 2023-09-16 12:46:50.000739

"""
import sqlalchemy as sa
import sqlalchemy.orm as orm
from alembic import op
from sqlalchemy.future import select

# revision identifiers, used by Alembic.
revision = "0f07a3edd643"
down_revision = "9b0e597da71c"
branch_labels = None
depends_on = None

users_table = sa.table(
"users", sa.column("name", sa.String), sa.column("role_id", sa.Integer)
)
roles_table = sa.table(
"roles", sa.column("name", sa.String), sa.column("id", sa.Integer)
)
account_types_table = sa.table(
"account_types", sa.column("name", sa.String), sa.column("id", sa.Integer)
)
user_role_association_table = sa.table(
"user_role_association",
sa.column("user_id", sa.ForeignKey("users.id")),
sa.column("role_id", sa.ForeignKey("roles.id")),
)


def role_named(name):
return (
roles_table.select()
.where(roles_table.c.name == op.inline_literal(name))
.scalar_subquery()
)


def account_type_named(name):
return (
account_types_table.select()
.where(account_types_table.c.name == op.inline_literal(name))
.scalar_subquery()
)


def upgrade_users(session, oldrole, *newroles):
for r in newroles:
stmt = user_role_association_table.insert().values(role_id=role_named(r))
print(stmt)
print("---")
# session.execute(stmt)


def downgrade_users(session: orm.Session, newrole: str, oldrole: str):
stmt = (
users_table.update()
.where(user_role_association_table.c.role_id == role_named(newrole))
.values(role_id=account_type_named(oldrole).c.id)
)
print(stmt)
# session.execute(stmt)


def upgrade():
bind = op.get_bind()
session = orm.Session(bind=bind)

upgrade_users(session, "admin", "admin", "mentor", "display", "visible")
upgrade_users(session, "mentor", "mentor", "display", "visible")
upgrade_users(session, "display", "display", "autoload")
upgrade_users(session, "lead", "lead", "student", "funds", "visible")
upgrade_users(session, "student", "student", "funds", "visible")
upgrade_users(session, "guardian_limited", "guardian")
upgrade_users(session, "guardian", "guardian", "visible")

session.commit()
return

# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_constraint(None, type_="foreignkey")
batch_op.drop_column("role_id")

# ### end Alembic commands ###


def downgrade():
bind = op.get_bind()
session = orm.Session(bind=bind)

downgrade_users(session, "admin", "admin")
downgrade_users(session, "guardian", "guardian")
downgrade_users(session, "student", "student")
downgrade_users(session, "lead", "lead")
downgrade_users(session, "autoload", "display")
downgrade_users(session, "mentor", "mentor")
downgrade_users(session, "admin", "admin")
return

session.flush()

# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(sa.Column("role_id", sa.INTEGER(), nullable=False))
batch_op.create_foreign_key(None, "account_types", ["role_id"], ["id"])

# ### end Alembic commands ###
32 changes: 22 additions & 10 deletions signinapp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,15 @@ def create_if_not_exists(cls, name, **kwargs):


def init_default_db():
create_if_not_exists(Role, name="admin", mentor=True, can_display=True, admin=True)
create_if_not_exists(Role, name="mentor", mentor=True, can_display=True)
create_if_not_exists(Role, name="display", can_display=True, autoload=True)
create_if_not_exists(Role, name="lead", can_see_subteam=True, receives_funds=True)
create_if_not_exists(Role, name="student", default_role=True, receives_funds=True)
create_if_not_exists(Role, name="guardian_limited", guardian=True, visible=False)
create_if_not_exists(Role, name="guardian", guardian=True)
create_if_not_exists(Role, name="admin")
create_if_not_exists(Role, name="mentor")
create_if_not_exists(Role, name="display")
create_if_not_exists(Role, name="lead")
create_if_not_exists(Role, name="student")
create_if_not_exists(Role, name="guardian")
create_if_not_exists(Role, name="funds")
create_if_not_exists(Role, name="autoload")
create_if_not_exists(Role, name="visible")

create_if_not_exists(
EventType, name="Training", description="Training Session", autoload=True
Expand All @@ -214,10 +216,20 @@ def init_default_db():
db.session.commit()

if not User.from_email("admin@signin"):
User.make("admin@signin", "admin", password="1234", role="admin", approved=True)
User.make(
"admin@signin",
"admin",
password="1234",
roles=["admin", "mentor", "visible", "display"],
approved=True,
)
if not User.from_email("display@signin"):
User.make(
"display@signin", "display", password="1234", role="display", approved=True
"display@signin",
"display",
password="1234",
roles=["display", "autoload"],
approved=True,
)
db.session.commit()

Expand Down Expand Up @@ -268,7 +280,7 @@ def init_default_db():
address="123 First Street",
tshirt_size="Large",
password="1234",
role="mentor",
roles=["mentor", "display"],
approved=True,
)
student_user = Student.make(
Expand Down
2 changes: 1 addition & 1 deletion signinapp/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy import delete

from ..util import admin_required
from . import role, subteam, users # noqa
from . import subteam, users # noqa
from .util import admin


Expand Down
64 changes: 0 additions & 64 deletions signinapp/admin/role.py

This file was deleted.

10 changes: 6 additions & 4 deletions signinapp/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ def edit_user():
user.name = form.name.data
if form.password.data:
user.password = generate_password_hash(form.password.data)
user.role_id = form.admin_data.role.data

user.roles = [
Role.from_name(r) for r in form.admin_data.data["roles"] if r and r != "0"
]
user.subteam_id = form.subteam.data or None
user.approved = form.admin_data.approved.data
user.preferred_name = form.preferred_name.data
Expand All @@ -118,8 +121,7 @@ def edit_user():
return redirect(url_for("team.users"))

form.phone_number.process_data(user.formatted_phone_number)

form.admin_data.role.process_data(user.role_id)
form.admin_data.roles.process_data([role.id for role in user.roles])
form.admin_data.approved.process_data(user.approved)
form.subteam.process_data(user.subteam_id)
form.tshirt_size.process_data(
Expand Down Expand Up @@ -167,7 +169,7 @@ def edit_student_data():
@admin_required
def edit_guardian_data():
user: User = db.session.get(User, request.args["user_id"])
if not user or not user.role.guardian:
if not user or not user.has_role("guardian"):
flash("Invalid guardian user ID")
return redirect(url_for("team.list_guardians"))

Expand Down
2 changes: 1 addition & 1 deletion signinapp/badge.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def view():
bid = int(bid)
badge: Badge = db.session.get(Badge, bid)
awards: list[BadgeAward] = sorted(
[a for a in badge.awards], key=lambda u: u.owner.name
[u for u in badge.awards], key=lambda u: u.owner.name
)
return render_template("badge.html.jinja2", badge=badge, awards=awards)
return redirect(url_for("mentor.all_badges", badge_id=badge.id))
Expand Down
6 changes: 3 additions & 3 deletions signinapp/dbadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

class AuthModelView(ModelView):
def is_accessible(self):
return current_user.is_authenticated and current_user.role.admin
return current_user.is_authenticated and current_user.has_role("admin")

def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
Expand All @@ -28,13 +28,13 @@ def inaccessible_callback(self, name, **kwargs):

class AdminView(AdminIndexView):
def is_accessible(self):
return current_user.is_authenticated and current_user.role.admin
return current_user.is_authenticated and current_user.has_role("admin")

def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
if not current_user.is_authenticated:
return redirect(url_for("auth.login", next=request.url))
elif not current_user.role.admin:
elif not current_user.has_role("admin"):
return abort(401)


Expand Down
15 changes: 9 additions & 6 deletions signinapp/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@eventbp.route("/event")
@login_required
def event():
if not current_user.role.can_display:
if not current_user.has_role("display"):
flash("You don't have permissions to view the event scan page")
return redirect(url_for("index"))
event_code = request.values.get("event_code")
Expand Down Expand Up @@ -84,7 +84,7 @@ def selfout():
def scan():
"""This function returns a JSON object, not a web page."""

if not current_user.role.can_display:
if not current_user.has_role("display"):
return Response(
"Error: User does not have permission to view active stamps",
HTTPStatus.FORBIDDEN,
Expand Down Expand Up @@ -123,7 +123,7 @@ def scan():
def autoevent():
"""This function returns a JSON object, not a web page."""

if not current_user.role.can_display:
if not current_user.has_role("display"):
return Response(
"Error: User does not have permission to view active stamps",
HTTPStatus.FORBIDDEN,
Expand All @@ -145,7 +145,7 @@ def active():
if not current_user.approved:
return Response("Error: User is not approved", HTTPStatus.FORBIDDEN)

if not current_user.role.can_display:
if not current_user.has_role("display"):
return Response(
"Error: User does not have permission to view active stamps",
HTTPStatus.FORBIDDEN,
Expand Down Expand Up @@ -205,7 +205,7 @@ def export_stamps(
@eventbp.route("/export")
@login_required
def export():
if current_user.role.admin:
if current_user.has_role("admin"):
name = request.values.get("name", None)
else:
name = current_user.name
Expand All @@ -221,7 +221,10 @@ def export():
@eventbp.route("/export/subteam")
@login_required
def export_subteam():
if not current_user.can_see_subteam or not current_user.subteam_id:
if (
not (current_user.has_role("lead") or current_user.has_role("mentor"))
or not current_user.subteam_id
):
return current_app.login_manager.unauthorized()

subteam = current_user.subteam
Expand Down
3 changes: 2 additions & 1 deletion signinapp/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
generate_grade_choices,
get_form_ids,
)
from .util import MultiCheckboxField

NAME_RE = regex.compile(r"^(\p{L}+(['\-]\p{L}+)*)( \p{L}+(['\-]\p{L}+)*)*$")
ADDRESS_RE = regex.compile(r"[A-Za-z0-9'\.\-\s\,]+")
Expand Down Expand Up @@ -75,7 +76,7 @@ class StudentDataForm(Form):


class AdminUserForm(Form):
role = SelectField(choices=lambda: get_form_ids(Role))
roles = MultiCheckboxField(choices=lambda: get_form_ids(Role))
approved = BooleanField()


Expand Down
Loading