Auto-commit local changes before build (2026-01-06 11:47:15)
This commit is contained in:
parent
551e0dec26
commit
cc0d969ebf
@ -1 +1 @@
|
||||
v20260106-06-customers-delete-fk-cascade-fix
|
||||
v20260106-07-feedback-open-reply
|
||||
|
||||
@ -172,6 +172,19 @@ def feedback_detail(item_id: int):
|
||||
resolved_by = User.query.get(item.resolved_by_user_id)
|
||||
resolved_by_name = resolved_by.username if resolved_by else ""
|
||||
|
||||
|
||||
replies = (
|
||||
FeedbackReply.query.filter(FeedbackReply.feedback_item_id == item.id)
|
||||
.order_by(FeedbackReply.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
reply_user_ids = sorted({int(r.user_id) for r in replies})
|
||||
reply_users = (
|
||||
User.query.filter(User.id.in_(reply_user_ids)).all() if reply_user_ids else []
|
||||
)
|
||||
reply_user_map = {int(u.id): (u.username or "") for u in reply_users}
|
||||
|
||||
return render_template(
|
||||
"main/feedback_detail.html",
|
||||
item=item,
|
||||
@ -179,8 +192,42 @@ def feedback_detail(item_id: int):
|
||||
resolved_by_name=resolved_by_name,
|
||||
vote_count=int(vote_count),
|
||||
user_voted=bool(user_voted),
|
||||
replies=replies,
|
||||
reply_user_map=reply_user_map,
|
||||
)
|
||||
|
||||
@main_bp.route("/feedback/<int:item_id>/reply", methods=["POST"])
|
||||
@login_required
|
||||
@roles_required("admin", "operator", "viewer")
|
||||
def feedback_reply(item_id: int):
|
||||
item = FeedbackItem.query.get_or_404(item_id)
|
||||
if item.deleted_at is not None:
|
||||
abort(404)
|
||||
|
||||
if (item.status or "").strip().lower() != "open":
|
||||
flash("Only open feedback items can be replied to.", "warning")
|
||||
return redirect(url_for("main.feedback_detail", item_id=item.id))
|
||||
|
||||
message = (request.form.get("message") or "").strip()
|
||||
if not message:
|
||||
flash("Reply message is required.", "danger")
|
||||
return redirect(url_for("main.feedback_detail", item_id=item.id))
|
||||
|
||||
reply = FeedbackReply(
|
||||
feedback_item_id=int(item.id),
|
||||
user_id=int(current_user.id),
|
||||
message=message,
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
db.session.add(reply)
|
||||
db.session.commit()
|
||||
|
||||
flash("Reply added.", "success")
|
||||
return redirect(url_for("main.feedback_detail", item_id=item.id))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@main_bp.route("/feedback/<int:item_id>/vote", methods=["POST"])
|
||||
@login_required
|
||||
|
||||
@ -50,6 +50,7 @@ from ..models import (
|
||||
RemarkJobRun,
|
||||
FeedbackItem,
|
||||
FeedbackVote,
|
||||
FeedbackReply,
|
||||
NewsItem,
|
||||
NewsRead,
|
||||
ReportDefinition,
|
||||
|
||||
@ -772,6 +772,7 @@ def run_migrations() -> None:
|
||||
migrate_mail_objects_table()
|
||||
migrate_object_persistence_tables()
|
||||
migrate_feedback_tables()
|
||||
migrate_feedback_replies_table()
|
||||
migrate_tickets_active_from_date()
|
||||
migrate_remarks_active_from_date()
|
||||
migrate_overrides_match_columns()
|
||||
@ -978,6 +979,40 @@ def migrate_job_runs_review_tracking() -> None:
|
||||
print("[migrations] migrate_job_runs_review_tracking completed.")
|
||||
|
||||
|
||||
|
||||
|
||||
def migrate_feedback_replies_table() -> None:
|
||||
"""Ensure feedback reply table exists.
|
||||
|
||||
Table:
|
||||
- feedback_replies (messages on open feedback items)
|
||||
"""
|
||||
engine = db.get_engine()
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS feedback_replies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feedback_item_id INTEGER NOT NULL REFERENCES feedback_items(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_feedback_replies_item_created_at
|
||||
ON feedback_replies (feedback_item_id, created_at);
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("[migrations] Feedback replies table ensured.")
|
||||
|
||||
|
||||
def migrate_tickets_active_from_date() -> None:
|
||||
"""Ensure tickets.active_from_date exists and is populated.
|
||||
|
||||
|
||||
@ -494,6 +494,20 @@ class FeedbackVote(db.Model):
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class FeedbackReply(db.Model):
|
||||
__tablename__ = "feedback_replies"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
feedback_item_id = db.Column(
|
||||
db.Integer, db.ForeignKey("feedback_items.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
||||
message = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
|
||||
class NewsItem(db.Model):
|
||||
__tablename__ = "news_items"
|
||||
|
||||
|
||||
@ -49,6 +49,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Replies</h5>
|
||||
{% if replies %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for r in replies %}
|
||||
<div class="list-group-item px-0">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<strong>{{ reply_user_map.get(r.user_id, '') or ('User #' ~ r.user_id) }}</strong>
|
||||
<span class="text-muted" style="font-size: 0.85rem;">
|
||||
{{ r.created_at.strftime('%d-%m-%Y %H:%M:%S') if r.created_at else '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="white-space: pre-wrap;">{{ r.message }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">No replies yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Add reply</h5>
|
||||
{% if item.status == 'open' %}
|
||||
<form method="post" action="{{ url_for('main.feedback_reply', item_id=item.id) }}">
|
||||
<div class="mb-2">
|
||||
<textarea class="form-control" name="message" rows="4" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Post reply</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-muted">Replies can only be added while the item is open.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
@ -46,6 +46,15 @@
|
||||
- Migration: enforced ON DELETE CASCADE on customer_objects.customer_id by recreating the FK constraint if needed (idempotent).
|
||||
- Customer delete flow: unlinks jobs (sets jobs.customer_id to NULL) before deleting the customer, so historical job/run data remains intact.
|
||||
|
||||
---
|
||||
|
||||
## v20260106-07-feedback-open-reply
|
||||
|
||||
- Added the ability for users to reply to Feedback items with status "Open".
|
||||
- Implemented reply functionality directly within the Feedback detail view.
|
||||
- Ensured replies are only allowed while the Feedback item remains in the Open state.
|
||||
- Stored user replies linked to the original Feedback item for audit and history purposes.
|
||||
|
||||
================================================================================================================================================
|
||||
## v0.1.16
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user