Apr 2 2026: update user's info, adding Full Name record
This commit is contained in:
@@ -12,6 +12,7 @@ class User(UserMixin, db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||||
|
full_name = db.Column(db.String(150), nullable=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(
|
role = db.Column(
|
||||||
@@ -37,5 +38,10 @@ class User(UserMixin, db.Model):
|
|||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self):
|
||||||
|
"""Return full name if set, otherwise fall back to username."""
|
||||||
|
return self.full_name.strip() if self.full_name and self.full_name.strip() else self.username
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
|
|||||||
+6
-3
@@ -74,6 +74,7 @@ def profile():
|
|||||||
form = ProfileForm(user=current_user, obj=current_user)
|
form = ProfileForm(user=current_user, obj=current_user)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
current_user.full_name = form.full_name.data.strip() or None
|
||||||
current_user.email = form.email.data
|
current_user.email = form.email.data
|
||||||
|
|
||||||
if form.new_password.data:
|
if form.new_password.data:
|
||||||
@@ -134,6 +135,7 @@ def create_user():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User(
|
user = User(
|
||||||
username=form.username.data,
|
username=form.username.data,
|
||||||
|
full_name=form.full_name.data.strip() or None,
|
||||||
email=form.email.data,
|
email=form.email.data,
|
||||||
role=form.role.data
|
role=form.role.data
|
||||||
)
|
)
|
||||||
@@ -158,9 +160,10 @@ def edit_user(user_id):
|
|||||||
form = UserForm(user=user, obj=user)
|
form = UserForm(user=user, obj=user)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user.username = form.username.data
|
user.username = form.username.data
|
||||||
user.email = form.email.data
|
user.full_name = form.full_name.data.strip() or None
|
||||||
user.role = form.role.data
|
user.email = form.email.data
|
||||||
|
user.role = form.role.data
|
||||||
|
|
||||||
if form.password.data:
|
if form.password.data:
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ def create():
|
|||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User(
|
user = User(
|
||||||
username = form.username.data,
|
username = form.username.data,
|
||||||
email = form.email.data,
|
full_name = form.full_name.data.strip() or None,
|
||||||
role = 'customer',
|
email = form.email.data,
|
||||||
active = True,
|
role = 'customer',
|
||||||
|
active = True,
|
||||||
)
|
)
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
@@ -139,8 +140,9 @@ def edit(customer_id):
|
|||||||
form = CustomerUserForm(user=customer, obj=customer)
|
form = CustomerUserForm(user=customer, obj=customer)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
customer.username = form.username.data
|
customer.username = form.username.data
|
||||||
customer.email = form.email.data
|
customer.full_name = form.full_name.data.strip() or None
|
||||||
|
customer.email = form.email.data
|
||||||
if form.password.data:
|
if form.password.data:
|
||||||
customer.set_password(form.password.data)
|
customer.set_password(form.password.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
<i class="bi bi-person-circle"></i>
|
<i class="bi bi-person-circle"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mb-1 fw-bold">{{ current_user.username }}</h4>
|
<h4 class="mb-1 fw-bold">{{ current_user.display_name }}</h4>
|
||||||
|
{% if current_user.full_name %}
|
||||||
|
<p class="text-muted mb-1" style="font-size:.85rem;">@{{ current_user.username }}</p>
|
||||||
|
{% endif %}
|
||||||
<p class="text-muted mb-2">{{ current_user.email }}</p>
|
<p class="text-muted mb-2">{{ current_user.email }}</p>
|
||||||
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
<span class="badge fs-6 bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
|
||||||
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'supervisor' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
<i class="bi bi-{% if current_user.role == 'admin' %}shield-fill{% elif current_user.role == 'supervisor' %}star-fill{% else %}person-badge-fill{% endif %} me-1"></i>
|
||||||
@@ -92,6 +95,15 @@
|
|||||||
<form method="POST" action="{{ url_for('auth.profile') }}">
|
<form method="POST" action="{{ url_for('auth.profile') }}">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<!-- Full Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.full_name.label(class="form-label fw-semibold") }}
|
||||||
|
{{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }}
|
||||||
|
{% for error in form.full_name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.email.label(class="form-label fw-semibold") }}
|
{{ form.email.label(class="form-label fw-semibold") }}
|
||||||
|
|||||||
@@ -56,6 +56,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.full_name.label(class="form-label") }}
|
||||||
|
{{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }}
|
||||||
|
{% if form.full_name.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for error in form.full_name.errors %}{{ error }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
{{ form.email.label(class="form-label") }}
|
{{ form.email.label(class="form-label") }}
|
||||||
{{ form.email(class="form-control") }}
|
{{ form.email(class="form-control") }}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
|
<th>Full Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr class="{{ 'table-secondary text-muted' if not user.active else '' }}">
|
<tr class="{{ 'table-secondary text-muted' if not user.active else '' }}">
|
||||||
<td><strong>{{ user.username }}</strong></td>
|
<td><strong>{{ user.username }}</strong></td>
|
||||||
|
<td>{{ user.full_name or '—' }}</td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'supervisor' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'supervisor' %}warning{% elif user.role == 'project_manager' %}primary{% elif user.role == 'customer' %}success{% else %}info{% endif %}">
|
||||||
|
|||||||
@@ -52,6 +52,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
{{ form.full_name.label(class="form-label") }}
|
||||||
|
{{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }}
|
||||||
|
{% if form.full_name.errors %}
|
||||||
|
<div class="text-danger small mt-1">
|
||||||
|
{% for e in form.full_name.errors %}{{ e }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
{{ form.email.label(class="form-label") }}
|
{{ form.email.label(class="form-label") }}
|
||||||
{{ form.email(class="form-control") }}
|
{{ form.email(class="form-control") }}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
|
<th>Full Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Assigned Projects</th>
|
<th>Assigned Projects</th>
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ customer.full_name or '—' }}</td>
|
||||||
<td class="small text-muted">{{ customer.email }}</td>
|
<td class="small text-muted">{{ customer.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if customer.active %}
|
{% if customer.active %}
|
||||||
|
|||||||
+4
-1
@@ -18,6 +18,7 @@ class LoginForm(FlaskForm):
|
|||||||
|
|
||||||
class ProfileForm(FlaskForm):
|
class ProfileForm(FlaskForm):
|
||||||
"""Self-service profile update form — available to all authenticated users."""
|
"""Self-service profile update form — available to all authenticated users."""
|
||||||
|
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||||
current_password = PasswordField('Current Password', validators=[Optional()])
|
current_password = PasswordField('Current Password', validators=[Optional()])
|
||||||
new_password = PasswordField('New Password', validators=[Optional(), Length(min=6, max=100)])
|
new_password = PasswordField('New Password', validators=[Optional(), Length(min=6, max=100)])
|
||||||
@@ -45,6 +46,7 @@ class ProfileForm(FlaskForm):
|
|||||||
|
|
||||||
class UserForm(FlaskForm):
|
class UserForm(FlaskForm):
|
||||||
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||||
|
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||||
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')])
|
||||||
@@ -101,7 +103,7 @@ class FacilityForm(FlaskForm):
|
|||||||
class AreaForm(FlaskForm):
|
class AreaForm(FlaskForm):
|
||||||
name = StringField('Area Name', validators=[DataRequired(), Length(max=255)])
|
name = StringField('Area Name', validators=[DataRequired(), Length(max=255)])
|
||||||
area_type = SelectField('Area Type', choices=[
|
area_type = SelectField('Area Type', choices=[
|
||||||
('building','Building'), ('restroom','Restroom'), ('lobby','Lobby'), ('hallway','Hallway'),
|
('restroom','Restroom'), ('lobby','Lobby'), ('hallway','Hallway'),
|
||||||
('office','Office'), ('kitchen','Kitchen'), ('storage','Storage'),
|
('office','Office'), ('kitchen','Kitchen'), ('storage','Storage'),
|
||||||
('outdoor','Outdoor'), ('other','Other'),
|
('outdoor','Outdoor'), ('other','Other'),
|
||||||
], validators=[Optional()])
|
], validators=[Optional()])
|
||||||
@@ -198,6 +200,7 @@ class CustomerUserForm(FlaskForm):
|
|||||||
Password is required on create; optional on edit.
|
Password is required on create; optional on edit.
|
||||||
"""
|
"""
|
||||||
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||||
|
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||||
password = PasswordField('Password', validators=[Optional(), Length(min=8)])
|
password = PasswordField('Password', validators=[Optional(), Length(min=8)])
|
||||||
confirm_password = PasswordField('Confirm Password',
|
confirm_password = PasswordField('Confirm Password',
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Phase 9: Add full_name column to users table
|
||||||
|
|
||||||
|
Revision ID: phase9_user_full_name
|
||||||
|
Revises: phase8_notification_matrix
|
||||||
|
Create Date: 2026-04-02
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = 'phase9_user_full_name'
|
||||||
|
down_revision = 'phase8_notification_matrix'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
columns = [c['name'] for c in inspector.get_columns('users')]
|
||||||
|
|
||||||
|
if 'full_name' not in columns:
|
||||||
|
op.add_column(
|
||||||
|
'users',
|
||||||
|
sa.Column('full_name', sa.String(150), nullable=True, default=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('users', 'full_name')
|
||||||
Reference in New Issue
Block a user