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, from app.models.inspection import (InspectionTemplate, ChecklistItem,
Inspection, InspectionResult) Inspection, InspectionResult)
from app.models.issue import Issue 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)) contact_phone = db.Column(db.String(20))
active = db.Column(db.Boolean, default=True) 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 # Relationships
areas = db.relationship('Area', backref='facility', lazy='dynamic') areas = db.relationship('Area', backref='facility', lazy='dynamic')
inspections = db.relationship('Inspection', 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) username = db.Column(db.String(100), unique=True, nullable=False, index=True)
email = db.Column(db.String(255), 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) 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) created_at = db.Column(db.DateTime, default=now_eastern)
active = db.Column(db.Boolean, default=True, nullable=False) 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) return check_password_hash(self.password_hash, password)
def __repr__(self): 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') flash('Supervisor access required.', 'danger')
return redirect(url_for('dashboard.index')) return redirect(url_for('dashboard.index'))
return f(*args, **kwargs) 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)]) password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')]) confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
role = SelectField('Role', choices=[ role = SelectField('Role', choices=[
('admin', 'Administrator'), ('supervisor', 'Supervisor'), ('inspector', 'Inspector') ('admin', 'Administrator'),
('supervisor', 'Supervisor'),
('inspector', 'Inspector'),
('project_manager', 'Project Manager'),
('customer', 'Customer'),
], validators=[DataRequired()]) ], validators=[DataRequired()])
def __init__(self, user=None, *args, **kwargs): def __init__(self, user=None, *args, **kwargs):