| """ | |
| Bug Reports model layer. | |
| Handles all MongoDB operations for the bug/issue reporting system. | |
| """ | |
| from datetime import datetime, timezone | |
| from bson import ObjectId | |
| from pymongo import DESCENDING | |
| from ..core.db_connector import bug_reports_collection | |
| def _ensure_indexes(): | |
| try: | |
| bug_reports_collection.create_index([("created_at", DESCENDING)]) | |
| bug_reports_collection.create_index([("status", DESCENDING)]) | |
| bug_reports_collection.create_index([("author_id", DESCENDING)]) | |
| except Exception: | |
| pass | |
| _ensure_indexes() | |
| def _serialize_report(doc): | |
| """Convert a MongoDB report document into a JSON-serialisable dict.""" | |
| if not doc: | |
| return None | |
| admin_replies = [] | |
| for r in doc.get("admin_replies", []): | |
| admin_replies.append({ | |
| "_id": str(r.get("_id", ObjectId())), | |
| "body": r.get("body", ""), | |
| "created_at": r["created_at"].isoformat() if r.get("created_at") else None, | |
| "is_broadcast": r.get("is_broadcast", False), | |
| "type": "admin", | |
| }) | |
| user_replies = [] | |
| for r in doc.get("user_replies", []): | |
| user_replies.append({ | |
| "_id": str(r.get("_id", ObjectId())), | |
| "body": r.get("body", ""), | |
| "created_at": r["created_at"].isoformat() if r.get("created_at") else None, | |
| "type": "user", | |
| }) | |
| return { | |
| "_id": str(doc["_id"]), | |
| "title": doc.get("title", ""), | |
| "body": doc.get("body", ""), | |
| "category": doc.get("category", "bug"), | |
| "page_url": doc.get("page_url", ""), | |
| "author": doc.get("author", "Anonymous"), | |
| "author_id": str(doc["author_id"]) if doc.get("author_id") is not None else None, | |
| "author_avatar": doc.get("author_avatar"), | |
| "status": doc.get("status", "open"), | |
| "created_at": doc["created_at"].isoformat() if doc.get("created_at") else None, | |
| "updated_at": doc["updated_at"].isoformat() if doc.get("updated_at") else None, | |
| "admin_replies": admin_replies, | |
| "user_replies": user_replies, | |
| "admin_reply_count": len(admin_replies), | |
| "user_reply_count": len(user_replies), | |
| "reply_count": len(admin_replies) + len(user_replies), | |
| } | |
| def create_report(title, body, category, author, author_id, author_avatar=None, page_url=None): | |
| """Create a new bug report.""" | |
| doc = { | |
| "title": title.strip(), | |
| "body": body.strip(), | |
| "category": category or "bug", | |
| "page_url": (page_url or "").strip(), | |
| "author": author, | |
| "author_id": str(author_id) if author_id is not None else None, | |
| "author_avatar": author_avatar, | |
| "status": "open", | |
| "created_at": datetime.now(timezone.utc), | |
| "updated_at": datetime.now(timezone.utc), | |
| "admin_replies": [], | |
| "user_replies": [], | |
| } | |
| result = bug_reports_collection.insert_one(doc) | |
| doc["_id"] = result.inserted_id | |
| return _serialize_report(doc) | |
| def get_reports(page=1, per_page=20, status=None, author_id=None): | |
| """Get bug reports with pagination and optional filtering.""" | |
| query = {} | |
| if status: | |
| query["status"] = status | |
| if author_id: | |
| query["author_id"] = str(author_id) | |
| total = bug_reports_collection.count_documents(query) | |
| skip = (page - 1) * per_page | |
| cursor = bug_reports_collection.find(query).sort("created_at", DESCENDING).skip(skip).limit(per_page) | |
| reports = [_serialize_report(doc) for doc in cursor] | |
| return reports, total | |
| def get_report_by_id(report_id): | |
| """Get a single report by its ID.""" | |
| try: | |
| oid = ObjectId(report_id) | |
| except Exception: | |
| return None | |
| doc = bug_reports_collection.find_one({"_id": oid}) | |
| return _serialize_report(doc) | |
| def add_admin_reply(report_id, body, is_broadcast=False): | |
| """Add an admin reply to a bug report.""" | |
| try: | |
| oid = ObjectId(report_id) | |
| except Exception: | |
| return False | |
| reply = { | |
| "_id": ObjectId(), | |
| "body": body.strip(), | |
| "created_at": datetime.now(timezone.utc), | |
| "is_broadcast": is_broadcast, | |
| } | |
| result = bug_reports_collection.update_one( | |
| {"_id": oid}, | |
| { | |
| "$push": {"admin_replies": reply}, | |
| "$set": { | |
| "status": "in_progress", | |
| "updated_at": datetime.now(timezone.utc), | |
| }, | |
| } | |
| ) | |
| return result.modified_count > 0 | |
| def add_broadcast_reply(report_ids, body): | |
| """Add the same admin reply to multiple reports (group reply).""" | |
| reply = { | |
| "_id": ObjectId(), | |
| "body": body.strip(), | |
| "created_at": datetime.now(timezone.utc), | |
| "is_broadcast": True, | |
| } | |
| oids = [] | |
| for rid in report_ids: | |
| try: | |
| oids.append(ObjectId(rid)) | |
| except Exception: | |
| continue | |
| if not oids: | |
| return False | |
| result = bug_reports_collection.update_many( | |
| {"_id": {"$in": oids}}, | |
| { | |
| "$push": {"admin_replies": reply}, | |
| "$set": { | |
| "status": "in_progress", | |
| "updated_at": datetime.now(timezone.utc), | |
| }, | |
| } | |
| ) | |
| return result.modified_count > 0 | |
| def update_report_status(report_id, new_status): | |
| """Update the status of a bug report.""" | |
| try: | |
| oid = ObjectId(report_id) | |
| except Exception: | |
| return False | |
| valid_statuses = ("open", "in_progress", "resolved", "closed") | |
| if new_status not in valid_statuses: | |
| return False | |
| result = bug_reports_collection.update_one( | |
| {"_id": oid}, | |
| {"$set": {"status": new_status, "updated_at": datetime.now(timezone.utc)}} | |
| ) | |
| return result.modified_count > 0 | |
| def add_user_reply(report_id, author, author_id, body): | |
| """Add a user reply to their own bug report. Verifies the user owns the report.""" | |
| try: | |
| oid = ObjectId(report_id) | |
| except Exception: | |
| return False | |
| reply = { | |
| "_id": ObjectId(), | |
| "author": author, | |
| "author_id": str(author_id) if author_id is not None else None, | |
| "body": body.strip(), | |
| "created_at": datetime.now(timezone.utc), | |
| } | |
| result = bug_reports_collection.update_one( | |
| {"_id": oid}, | |
| { | |
| "$push": {"user_replies": reply}, | |
| "$set": { | |
| "updated_at": datetime.now(timezone.utc), | |
| }, | |
| } | |
| ) | |
| return result.modified_count > 0 | |
| def get_stats(): | |
| """Get bug report statistics for the admin panel.""" | |
| total = bug_reports_collection.count_documents({}) | |
| open_count = bug_reports_collection.count_documents({"status": "open"}) | |
| in_progress_count = bug_reports_collection.count_documents({"status": "in_progress"}) | |
| resolved_count = bug_reports_collection.count_documents({"status": "resolved"}) | |
| return { | |
| "total": total, | |
| "open": open_count, | |
| "in_progress": in_progress_count, | |
| "resolved": resolved_count, | |
| } | |