diff --git a/app/models/issue.py b/app/models/issue.py index e06c2cb..ca264f2 100644 --- a/app/models/issue.py +++ b/app/models/issue.py @@ -5,12 +5,13 @@ from app.utils.time_utils import now_eastern class IssueComment(db.Model): __tablename__ = 'issue_comments' - id = db.Column(db.Integer, primary_key=True) - issue_id = db.Column(db.Integer, db.ForeignKey('issues.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) - status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made - body = db.Column(db.Text, nullable=False) - created_at = db.Column(db.DateTime, default=now_eastern, nullable=False) + id = db.Column(db.Integer, primary_key=True) + issue_id = db.Column(db.Integer, db.ForeignKey('issues.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made + body = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=now_eastern, nullable=False) + is_customer_visible = db.Column(db.Boolean, nullable=False, default=False) # Relationships author = db.relationship('User', foreign_keys=[user_id]) diff --git a/app/routes/issues.py b/app/routes/issues.py index 1b1d119..c28dd9f 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -227,7 +227,28 @@ def view(issue_id): flash('Access denied.', 'danger') return redirect(url_for('issues.index')) if request.method == 'POST': - abort(403) + # Customers may only add a comment, and only on issues they follow or reported + can_comment = (issue.is_followed_by(current_user) or issue.reported_by == current_user.id) + if not can_comment: + abort(403) + comment_body = request.form.get('update_notes', '').strip() + if not comment_body: + flash('Comment cannot be empty.', 'warning') + return redirect(url_for('issues.view', issue_id=issue_id)) + comment = IssueComment( + issue_id=issue.id, + user_id=current_user.id, + status_at_time=issue.status, + body=comment_body, + is_customer_visible=True, # customer comments are always visible to all + ) + db.session.add(comment) + db.session.commit() + log_action(ACTION_UPDATE, 'Issue', issue.id, + f'#{issue.id}', + 'customer comment added') + flash('Comment posted.', 'success') + return redirect(url_for('issues.view', issue_id=issue_id)) form = IssueUpdateForm(obj=issue) staff = User.query.filter(User.role.in_(['admin', 'director', 'inspector'])).order_by(User.username).all() @@ -270,10 +291,11 @@ def view(issue_id): comment_body = form.update_notes.data.strip() if form.update_notes.data else '' if comment_body: comment = IssueComment( - issue_id = issue.id, - user_id = current_user.id, - status_at_time = issue.status, - body = comment_body, + issue_id = issue.id, + user_id = current_user.id, + status_at_time = issue.status, + body = comment_body, + is_customer_visible = 'is_customer_visible' in request.form, ) db.session.add(comment) @@ -418,7 +440,12 @@ def view(issue_id): return redirect(url_for('issues.view', issue_id=issue_id)) is_following = issue.is_followed_by(current_user) - comments = issue.comments.order_by(IssueComment.created_at.asc()).all() + if current_user.role == 'customer': + comments = (issue.comments + .filter_by(is_customer_visible=True) + .order_by(IssueComment.created_at.asc()).all()) + else: + comments = issue.comments.order_by(IssueComment.created_at.asc()).all() return render_template('issues/view.html', issue=issue, form=form, diff --git a/app/templates/issues/view.html b/app/templates/issues/view.html index 7bd2f5e..d4122c2 100644 --- a/app/templates/issues/view.html +++ b/app/templates/issues/view.html @@ -183,6 +183,20 @@ {% else %} {{ c.author.role|replace('_',' ')|title }} {% endif %} + {# Visibility indicator — staff only #} + {% if current_user.role != 'customer' %} + {% if c.is_customer_visible %} + + Customer visible + + {% else %} + + Staff only + + {% endif %} + {% endif %}
No comments yet.
+Add Comment
+Add Comment
++ Follow this issue to add comments. +
+Only assigned staff can add comments. diff --git a/migrations/versions/phase22_comment_visibility.py b/migrations/versions/phase22_comment_visibility.py new file mode 100644 index 0000000..c748ab1 --- /dev/null +++ b/migrations/versions/phase22_comment_visibility.py @@ -0,0 +1,41 @@ +"""phase22 — add is_customer_visible to issue_comments + +Staff comments default to hidden from customers (is_customer_visible=FALSE). +Staff can tick a checkbox to share a comment with the customer. +Customer comments are always visible (is_customer_visible=TRUE, set at write time). +""" + +import sqlalchemy as sa +from alembic import op + +revision = 'phase22_comment_visibility' +down_revision = 'phase21_performance_indexes' +branch_labels = None +depends_on = None + + +def _column_exists(bind, table: str, column: str) -> bool: + result = bind.execute(sa.text( + "SELECT COUNT(*) FROM information_schema.columns " + "WHERE table_schema = DATABASE() " + " AND table_name = :table " + " AND column_name = :column" + ), {'table': table, 'column': column}) + return result.scalar() > 0 + + +def upgrade(): + bind = op.get_bind() + if not _column_exists(bind, 'issue_comments', 'is_customer_visible'): + op.execute(sa.text( + 'ALTER TABLE issue_comments ' + 'ADD COLUMN is_customer_visible BOOLEAN NOT NULL DEFAULT FALSE' + )) + + +def downgrade(): + bind = op.get_bind() + if _column_exists(bind, 'issue_comments', 'is_customer_visible'): + op.execute(sa.text( + 'ALTER TABLE issue_comments DROP COLUMN is_customer_visible' + ))