Skip to content
Merged

Dev #1360

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/format-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.10'
cache: 'pip'

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# this dockerfile is used for product deployments
FROM python:3.9
FROM python:3.10
LABEL maintainer "Kelley Chau <kelleyc2@illinois.edu>"

COPY requirements.txt requirements.txt
Expand Down
33 changes: 33 additions & 0 deletions backend/api/models/BugReport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from api.core import Mixin
from .base import db
from mongoengine import *
from flask_mongoengine import Document
from datetime import datetime


class BugReport(Document, Mixin):
"""Bug Report Collection."""

description = StringField(required=True)
user_name = StringField(required=True)
user_email = StringField(required=True)
user_id = ObjectIdField() # Link to Users if logged in
role = StringField()
context = StringField() # navigation-header, home-layout, etc.
page_url = StringField()
status = StringField(default="new") # new, in_progress, resolved, closed
priority = StringField(default="medium") # low, medium, high, critical
attachments = ListField(DictField()) # Store file URLs and metadata from GCS
date_submitted = DateTimeField(required=True, default=datetime.utcnow)
resolved_date = DateTimeField()
resolved_by = StringField()
notes = StringField() # Admin notes
email_sent = BooleanField(default=False)
email_error = StringField()

def __repr__(self):
return f"""<BugReport
user: {self.user_name} ({self.user_email})
status: {self.status}
date: {self.date_submitted}
description: {self.description[:50]}...>"""
2 changes: 2 additions & 0 deletions backend/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .SignedDocs import SignedDocs
from .Announcement import Announcement
from .CommunityLibrary import CommunityLibrary
from .BugReport import BugReport

__all__ = [
"db",
Expand Down Expand Up @@ -73,6 +74,7 @@
"PartnerGroupMessage",
"Announcement",
"CommunityLibrary",
"BugReport",
]

# You must import all of the new Models you create to this page
41 changes: 41 additions & 0 deletions backend/api/utils/google_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from google.cloud import storage
from api.core import logger
from io import BytesIO
import base64
import datetime
from uuid import uuid4

client = storage.Client()
BUCKET = "app-mentee-global-images"
Expand All @@ -15,6 +18,44 @@ def upload_image_to_storage(image, filename):
return blob.public_url


def upload_bug_report_attachment(file_content_base64, original_filename, content_type):
"""
Upload bug report attachment to Google Cloud Storage

:param file_content_base64: Base64 encoded file content
:param original_filename: Original filename
:param content_type: MIME type of the file
:return: tuple (public_url, gcs_filename)
"""
try:
# Decode base64 content
file_data = base64.b64decode(file_content_base64)

# Generate unique filename: bug-reports/timestamp_uuid_originalname
timestamp = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S")
unique_id = str(uuid4())[:8]
# Sanitize filename
safe_filename = "".join(
c for c in original_filename if c.isalnum() or c in "._- "
)
gcs_filename = f"bug-reports/{timestamp}_{unique_id}_{safe_filename}"

# Upload to GCS
bucket = client.get_bucket(BUCKET)
blob = bucket.blob(gcs_filename)
blob.upload_from_string(file_data, content_type=content_type)

# Note: Bucket has uniform bucket-level access enabled
# Public access is controlled at bucket level, not per-object

logger.info(f"Uploaded bug report attachment: {gcs_filename}")
return blob.public_url, gcs_filename

except Exception as e:
logger.error(f"Failed to upload bug report attachment {original_filename}: {e}")
raise


def delete_image_from_storage(filename):
"""Delete image from Google Cloud Storage"""
bucket = client.get_bucket(BUCKET)
Expand Down
118 changes: 118 additions & 0 deletions backend/api/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Guest,
Hub,
Image,
BugReport,
)
from api.utils.require_auth import admin_only
from api.utils.request_utils import get_profile_model, imgur_client
Expand Down Expand Up @@ -272,6 +273,123 @@ def upload_account_emailText():
return create_response(status=200, message="Add users successfully")


# Bug Reports Management - MUST come before /admin/<id> route
@admin.route("/admin/bug-reports", methods=["GET"])
@admin_only
def get_bug_reports():
"""Get all bug reports with optional filtering

Query params:
status: Filter by status (new, in_progress, resolved, closed)
priority: Filter by priority (low, medium, high, critical)
user_email: Filter by user email
limit: Number of results (default 100)
"""
try:
# Get query parameters
status = request.args.get("status")
priority = request.args.get("priority")
user_email = request.args.get("user_email")
limit = int(request.args.get("limit", 100))

# Build query
query = {}
if status:
query["status"] = status
if priority:
query["priority"] = priority
if user_email:
query["user_email"] = user_email

# Fetch bug reports, sorted by date (newest first)
if query:
bug_reports = (
BugReport.objects(**query).order_by("-date_submitted").limit(limit)
)
else:
bug_reports = BugReport.objects().order_by("-date_submitted").limit(limit)

return create_response(data={"bug_reports": bug_reports})

except Exception as e:
logger.error(f"Failed to fetch bug reports: {e}")
return create_response(status=500, message=str(e))


@admin.route("/admin/bug-reports/<string:id>", methods=["GET"])
@admin_only
def get_bug_report(id):
"""Get a specific bug report by ID"""
try:
bug_report = BugReport.objects.get(id=id)
return create_response(data={"bug_report": bug_report})
except:
msg = "Bug report not found"
logger.info(msg)
return create_response(status=404, message=msg)


@admin.route("/admin/bug-reports/<string:id>", methods=["PUT"])
@admin_only
def update_bug_report(id):
"""Update bug report status, priority, or notes

Request body:
status: new, in_progress, resolved, closed
priority: low, medium, high, critical
notes: Admin notes
resolved_by: Admin name who resolved it
"""
try:
bug_report = BugReport.objects.get(id=id)
data = request.get_json()

if "status" in data:
bug_report.status = data["status"]
# If marking as resolved, add timestamp
if data["status"] == "resolved" and not bug_report.resolved_date:
from datetime import datetime

bug_report.resolved_date = datetime.utcnow()

if "priority" in data:
bug_report.priority = data["priority"]

if "notes" in data:
bug_report.notes = data["notes"]

if "resolved_by" in data:
bug_report.resolved_by = data["resolved_by"]

bug_report.save()
logger.info(f"Bug report {id} updated")

return create_response(
data={"bug_report": bug_report}, message="Bug report updated successfully"
)

except Exception as e:
logger.error(f"Failed to update bug report {id}: {e}")
return create_response(status=500, message=str(e))


@admin.route("/admin/bug-reports/<string:id>", methods=["DELETE"])
@admin_only
def delete_bug_report(id):
"""Delete a bug report"""
try:
bug_report = BugReport.objects.get(id=id)
bug_report.delete()
logger.info(f"Bug report {id} deleted")

return create_response(message="Bug report deleted successfully")

except:
msg = "Bug report not found"
logger.info(msg)
return create_response(status=404, message=msg)


@admin.route("/admin/<id>", methods=["GET"])
def get_admin(id):
# return create_response(data={"admin": {"_id":{"$oid":"60765e9289899aeee51a8b27"},"email":"klhester3@gmail.com","firebase_uid":"xsW41z9Hc6Y9r6Te0JAcXhlYneA2","name":"candle"}})
Expand Down
Loading
Loading