diff --git a/backend/app/routers/attempts.py b/backend/app/routers/attempts.py index 2e6b186..efcf640 100644 --- a/backend/app/routers/attempts.py +++ b/backend/app/routers/attempts.py @@ -209,9 +209,10 @@ def get_dashboard_stats( scores = [(a.score / a.total_questions * 100) if a.total_questions > 0 else 0 for a in completed_attempts] avg_score = round(sum(scores) / len(scores), 1) - # Per-quiz stats + # Per-quiz stats — based on quizzes the user has attempted, not created quiz_stats = [] - quizzes = db.query(Quiz).filter(Quiz.user_id == current_user.id).all() + attempted_quiz_ids = {a.quiz_id for a in completed_attempts} + quizzes = db.query(Quiz).filter(Quiz.id.in_(attempted_quiz_ids)).all() if attempted_quiz_ids else [] for quiz in quizzes: quiz_attempts = [a for a in completed_attempts if a.quiz_id == quiz.id] if quiz_attempts: diff --git a/backend/app/routers/documents.py b/backend/app/routers/documents.py index 348d6c0..8dd2494 100644 --- a/backend/app/routers/documents.py +++ b/backend/app/routers/documents.py @@ -90,19 +90,24 @@ def list_documents( return docs +def _get_doc_or_404(document_id: int, current_user: User, db) -> PDFDocument: + """Fetch document; moderators can access any, users only their own.""" + query = db.query(PDFDocument).filter(PDFDocument.id == document_id) + if not current_user.is_moderator: + query = query.filter(PDFDocument.user_id == current_user.id) + doc = query.first() + if not doc: + raise HTTPException(status_code=404, detail="Document not found") + return doc + + @router.get("/{document_id}", response_model=DocumentResponse) def get_document( document_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - doc = db.query(PDFDocument).filter( - PDFDocument.id == document_id, - PDFDocument.user_id == current_user.id, - ).first() - if not doc: - raise HTTPException(status_code=404, detail="Document not found") - return doc + return _get_doc_or_404(document_id, current_user, db) @router.get("/{document_id}/status", response_model=DocumentStatusResponse) @@ -111,12 +116,7 @@ def get_document_status( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - doc = db.query(PDFDocument).filter( - PDFDocument.id == document_id, - PDFDocument.user_id == current_user.id, - ).first() - if not doc: - raise HTTPException(status_code=404, detail="Document not found") + doc = _get_doc_or_404(document_id, current_user, db) return DocumentStatusResponse( id=doc.id, status=doc.status, @@ -130,12 +130,9 @@ def create_section( document_id: int, section_data: SectionCreate, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_moderator), ): - doc = db.query(PDFDocument).filter( - PDFDocument.id == document_id, - PDFDocument.user_id == current_user.id, - ).first() + doc = _get_doc_or_404(document_id, current_user, db) if not doc: raise HTTPException(status_code=404, detail="Document not found") if doc.status != "ready": @@ -163,14 +160,9 @@ def create_section( def delete_document( document_id: int, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_moderator), ): - doc = db.query(PDFDocument).filter( - PDFDocument.id == document_id, - PDFDocument.user_id == current_user.id, - ).first() - if not doc: - raise HTTPException(status_code=404, detail="Document not found") + doc = _get_doc_or_404(document_id, current_user, db) # Delete file file_path = os.path.join(settings.UPLOAD_DIR, doc.filename) diff --git a/backend/app/routers/nextcloud.py b/backend/app/routers/nextcloud.py index c10e3d9..9c37ff5 100644 --- a/backend/app/routers/nextcloud.py +++ b/backend/app/routers/nextcloud.py @@ -141,11 +141,13 @@ def download_file(req: NCRequest, _: User = Depends(require_moderator)): raise HTTPException(status_code=401, detail="Invalid credentials") if resp.status_code != 200: raise HTTPException(status_code=400, detail=f"Download failed: {resp.status_code}") - filename = req.path.split("/")[-1] + raw_name = req.path.split("/")[-1] + # Strip characters that would break the Content-Disposition header + safe_name = raw_name.replace('"', '').replace('\\', '').replace('\n', '').replace('\r', '') or "document.pdf" return StreamingResponse( io.BytesIO(resp.content), media_type="application/pdf", - headers={"Content-Disposition": f'attachment; filename="{filename}"', + headers={"Content-Disposition": f'attachment; filename="{safe_name}"', "Content-Length": str(len(resp.content))}, ) except HTTPException: diff --git a/frontend/src/pages/ResultsPage.jsx b/frontend/src/pages/ResultsPage.jsx index 70146cf..30528a9 100644 --- a/frontend/src/pages/ResultsPage.jsx +++ b/frontend/src/pages/ResultsPage.jsx @@ -140,7 +140,7 @@ export default function ResultsPage() { {/* Fill-blank */} {(!ans.options || ans.options.length === 0) && (
-
+
Your answer: {ans.user_answer || not answered}
{!ans.is_correct && ( diff --git a/frontend/src/pages/UploadPage.jsx b/frontend/src/pages/UploadPage.jsx index 0f883c3..7873070 100644 --- a/frontend/src/pages/UploadPage.jsx +++ b/frontend/src/pages/UploadPage.jsx @@ -32,7 +32,7 @@ function NextcloudBrowser({ onFile }) { { responseType: 'blob' } ) const file = new File([res.data], item.name, { type: 'application/pdf' }) - onFile(file) + onFile(file, true) } catch (err) { setError(err.response?.data?.detail || 'Download failed') } finally { setDownloading(null) } @@ -127,9 +127,10 @@ export default function UploadPage() { const hasNextcloud = !!localStorage.getItem('nc_username') - const handleFile = (f) => { + const handleFile = (f, fromNextcloud = false) => { if (f && f.type === 'application/pdf') { - setFile(f); setError(''); setTab('local') + setFile(f); setError('') + if (!fromNextcloud) setTab('local') } else { setError('Please select a PDF file') }