Fix 5 bugs: dashboard stats, document access, Nextcloud filename, upload tab, results colours

- attempts.py: dashboard quiz_stats now queries by attempted quizzes not
  created quizzes — regular users now see per-quiz breakdown correctly
- documents.py: moderators/admins can fetch documents by ID (were getting
  404 despite seeing them in the list); also restrict section creation and
  delete to moderators via require_moderator
- nextcloud.py: sanitise filename in Content-Disposition header (strip
  quotes, backslashes, newlines to prevent header injection)
- UploadPage.jsx: importing from Nextcloud no longer switches tab to local,
  preserving the selected-file info display
- ResultsPage.jsx: fill-blank correct answers now show correct-bg (green)
  instead of always showing wrong-bg (red)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-31 19:17:46 +02:00
parent 84f5927e5d
commit 7817f89849
5 changed files with 29 additions and 33 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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:

View file

@ -140,7 +140,7 @@ export default function ResultsPage() {
{/* Fill-blank */}
{(!ans.options || ans.options.length === 0) && (
<div style={{ marginBottom: 14, fontSize: '0.9rem' }}>
<div style={{ padding: '8px 14px', background: 'var(--wrong-bg)', borderRadius: 6, marginBottom: 6 }}>
<div style={{ padding: '8px 14px', borderRadius: 6, marginBottom: 6, background: ans.is_correct ? 'var(--correct-bg)' : 'var(--wrong-bg)' }}>
<strong>Your answer:</strong> {ans.user_answer || <em style={{ color: 'var(--text-subtle)' }}>not answered</em>}
</div>
{!ans.is_correct && (

View file

@ -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')
}