Mar 04 2026: Implement customer's view functionalities - Phase 1
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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}>')
|
||||||
+4
-1
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,29 @@ def supervisor_required(f):
|
|||||||
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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user