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:
parent
84f5927e5d
commit
7817f89849
5 changed files with 29 additions and 33 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue