Mar 04 2026: Implement customer's view functionalities - Phase 1

This commit is contained in:
2026-03-04 12:41:37 -05:00
parent c955569f21
commit d8475ba9c7
6 changed files with 105 additions and 4 deletions
+1
View File
@@ -3,3 +3,4 @@ from app.models.facility import Facility, Area
from app.models.inspection import (InspectionTemplate, ChecklistItem,
Inspection, InspectionResult)
from app.models.issue import Issue
from app.models.project import Project, CustomerAssignment
+4
View File
@@ -11,6 +11,10 @@ class Facility(db.Model):
contact_phone = db.Column(db.String(20))
active = db.Column(db.Boolean, default=True)
# Phase 1: link facility to a project (nullable for backward compatibility)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete='SET NULL'),
nullable=True, index=True)
# Relationships
areas = db.relationship('Area', backref='facility', lazy='dynamic')
inspections = db.relationship('Inspection', backref='facility', lazy='dynamic')
+63
View File
@@ -0,0 +1,63 @@
from app import db
from app.utils.time_utils import now_eastern
class Project(db.Model):
"""Top-level grouping that contains one or more Facilities.
Each project may have an assigned project_manager (User) and one or more
customer users linked via CustomerAssignment.
"""
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
project_manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
# Relationships
project_manager = db.relationship('User', foreign_keys=[project_manager_id],
backref='managed_projects')
facilities = db.relationship('Facility', backref='project', lazy='dynamic')
customer_assignments = db.relationship('CustomerAssignment', back_populates='project',
cascade='all, delete-orphan', lazy='dynamic')
def __repr__(self):
return f'<Project {self.name}>'
class CustomerAssignment(db.Model):
"""Links a customer-role User to a Project and/or a specific Facility.
- project_id only → customer can view all facilities within that project
- project_id + facility_id → customer is scoped to that specific facility
"""
__tablename__ = 'customer_assignments'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False, index=True)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete='CASCADE'),
nullable=False, index=True)
facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id', ondelete='CASCADE'),
nullable=True, index=True)
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
__table_args__ = (
db.UniqueConstraint('user_id', 'project_id', 'facility_id',
name='uq_customer_assignment'),
)
# Relationships
user = db.relationship('User', foreign_keys=[user_id],
backref=db.backref('customer_assignments', lazy='dynamic'))
project = db.relationship('Project', foreign_keys=[project_id],
back_populates='customer_assignments')
facility = db.relationship('Facility', foreign_keys=[facility_id],
backref=db.backref('customer_assignments', lazy='dynamic'))
def __repr__(self):
return (f'<CustomerAssignment user={self.user_id} '
f'project={self.project_id} facility={self.facility_id}>')
+5 -2
View File
@@ -14,7 +14,10 @@ class User(UserMixin, db.Model):
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
role = db.Column(
db.Enum('admin', 'supervisor', 'inspector', 'project_manager', 'customer'),
nullable=False
)
created_at = db.Column(db.DateTime, default=now_eastern)
active = db.Column(db.Boolean, default=True, nullable=False)
@@ -35,4 +38,4 @@ class User(UserMixin, db.Model):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
return f'<User {self.username}>'
+27 -1
View File
@@ -18,4 +18,30 @@ def supervisor_required(f):
flash('Supervisor access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
return decorated_function
def project_manager_required(f):
"""Grants access to admin, supervisor, and project_manager roles."""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in [
'admin', 'supervisor', 'project_manager'
]:
flash('Project Manager access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
def customer_required(f):
"""Restricts access to customer-role users only.
Internal staff (admin, supervisor, inspector, project_manager) should
never be routed through customer-scoped views — use their own routes.
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'customer':
flash('Customer portal access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
+5 -1
View File
@@ -49,7 +49,11 @@ class UserForm(FlaskForm):
password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
role = SelectField('Role', choices=[
('admin', 'Administrator'), ('supervisor', 'Supervisor'), ('inspector', 'Inspector')
('admin', 'Administrator'),
('supervisor', 'Supervisor'),
('inspector', 'Inspector'),
('project_manager', 'Project Manager'),
('customer', 'Customer'),
], validators=[DataRequired()])
def __init__(self, user=None, *args, **kwargs):