• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gcivil-nyu-org / team4-wed-fall25 / 63

10 Nov 2025 07:18PM UTC coverage: 75.054% (+11.0%) from 64.094%
63

push

travis-pro

web-flow
Merge pull request #77 from gcivil-nyu-org/feature/run_test_models_locally

fixed some UI changes and test cases

403 of 431 new or added lines in 11 files covered. (93.5%)

269 existing lines in 4 files now uncovered.

1047 of 1395 relevant lines covered (75.05%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.91
/note2webapp/views.py
1
# note2webapp/views.py
2
import os
1✔
3
import json
1✔
4
import importlib
1✔
5
import importlib.util
1✔
6
import inspect
1✔
7

8
from django.contrib import messages
1✔
9
from django.contrib.auth import login, logout, authenticate
1✔
10
from django.contrib.auth.decorators import login_required
1✔
11
from django.contrib.auth.models import Group, User
1✔
12
from django.shortcuts import render, redirect, get_object_or_404
1✔
13
from django.http import JsonResponse
1✔
14
from django.utils import timezone
1✔
15
from django import forms
1✔
16

17
# for admin stats
18
from django.contrib.admin.views.decorators import staff_member_required
1✔
19
from django.db.models import Count
1✔
20

21
from .forms import UploadForm, VersionForm
1✔
22
from .models import ModelUpload, ModelVersion, Profile
1✔
23
from .utils import (
1✔
24
    validate_model,
25
    test_model_on_cpu,
26
    sha256_uploaded_file,
27
    sha256_file_path,
28
    delete_version_files_and_dir,
29
    delete_model_media_tree,
30
)
31

32

33
# ---------------------------------------------------------
34
# AUTH
35
# ---------------------------------------------------------
36
def signup_view(request):
1✔
37
    if request.method == "POST":
1✔
38
        username = request.POST.get("username")
1✔
39
        password1 = request.POST.get("password1")
1✔
40
        password2 = request.POST.get("password2")
1✔
41

42
        errors = []
1✔
43
        if not username or not password1 or not password2:
1✔
UNCOV
44
            errors.append("All fields are required.")
×
45
        if password1 and password2 and password1 != password2:
1✔
46
            errors.append("Passwords do not match.")
1✔
47
        if User.objects.filter(username=username).exists():
1✔
UNCOV
48
            errors.append("Username already exists.")
×
49
        if password1 and len(password1) < 8:
1✔
50
            errors.append("Password must be at least 8 characters long.")
×
51

52
        if errors:
1✔
53
            for e in errors:
1✔
54
                messages.error(request, e)
1✔
55
            return render(request, "note2webapp/login.html")
1✔
56

UNCOV
57
        try:
×
UNCOV
58
            user = User.objects.create_user(username=username, password=password1)
×
59
            # normal signups become uploaders
UNCOV
60
            Profile.objects.create(user=user, role="uploader")
×
UNCOV
61
            group, _ = Group.objects.get_or_create(name="ModelUploader")
×
UNCOV
62
            user.groups.add(group)
×
UNCOV
63
            messages.success(request, "Account created successfully! Please login.")
×
64
            return redirect("login")
×
65
        except Exception as e:
×
66
            messages.error(request, f"Error creating account: {str(e)}")
×
67
            return render(request, "note2webapp/login.html")
×
68

69
    return render(request, "note2webapp/login.html")
×
70

71

72
def login_view(request):
1✔
73
    if request.method == "POST":
1✔
74
        username = request.POST.get("username")
1✔
75
        password = request.POST.get("password")
1✔
76
        user = authenticate(request, username=username, password=password)
1✔
77

78
        if user is not None:
1✔
79
            login(request, user)
1✔
80

81
            # Ensure superusers have admin role
82
            if user.is_superuser:
1✔
83
                prof, _ = Profile.objects.get_or_create(user=user)
1✔
84
                if prof.role != "admin":
1✔
NEW
85
                    prof.role = "admin"
×
NEW
86
                    prof.save()
×
87

88
                # Use different session key for admin
89
                request.session.set_expiry(3600)  # 1 hour for admins
1✔
90

91
            # Redirect based on role
92
            if hasattr(user, "profile") and user.profile.role == "reviewer":
1✔
93
                return redirect("reviewer_dashboard")
1✔
94
            return redirect("dashboard")
1✔
95
        else:
UNCOV
96
            messages.error(request, "Invalid username or password.")
×
97

UNCOV
98
    return render(request, "note2webapp/login.html")
×
99

100

101
def logout_view(request):
1✔
102
    logout(request)
×
UNCOV
103
    messages.success(request, "You have been logged out successfully.")
×
UNCOV
104
    return redirect("login")
×
105

106

107
# ---------------------------------------------------------
108
# MAIN DASHBOARD ROUTER
109
# ---------------------------------------------------------
110
@login_required
1✔
111
def dashboard(request):
1✔
112
    """
113
    Decide where to send the user.
114
    - Django staff/superuser: our nice stats page
115
    - uploader: uploader dashboard
116
    - reviewer: reviewer dashboard
117
    """
118
    if request.user.is_staff:
1✔
119
        return redirect("admin_stats")
1✔
120

121
    role = getattr(request.user.profile, "role", "uploader")
1✔
122
    if role == "uploader":
1✔
123
        return model_uploader_dashboard(request)
1✔
124
    elif role == "reviewer":
1✔
125
        return reviewer_dashboard(request)
1✔
126

127
    # fallback
NEW
128
    return model_uploader_dashboard(request)
×
129

130

131
@login_required
1✔
132
def validation_failed(request, version_id):
1✔
UNCOV
133
    version = get_object_or_404(ModelVersion, id=version_id, upload__user=request.user)
×
134
    if version.status != "FAIL":
×
135
        return redirect("model_versions", model_id=version.upload.id)
×
136
    return render(request, "note2webapp/validation_failed.html", {"version": version})
×
137

138

139
# ---------------------------------------------------------
140
# UPLOADER DASHBOARD
141
# ---------------------------------------------------------
142
@login_required
1✔
143
def model_uploader_dashboard(request):
1✔
144
    """
145
    Handles:
146
      - /dashboard/?page=list
147
      - /dashboard/?page=create
148
      - /dashboard/?page=detail&pk=...
149
      - /dashboard/?page=add_version&pk=...
150
    """
151
    page = request.GET.get("page", "list")
1✔
152
    pk = request.GET.get("pk")
1✔
153

154
    uploads = ModelUpload.objects.filter(user=request.user).order_by("-created_at")
1✔
155
    for upload in uploads:
1✔
156
        upload.active_versions_count = upload.versions.filter(
1✔
157
            is_deleted=False, status="PASS"
158
        ).count()
159

160
    context = {"uploads": uploads, "page": page}
1✔
161

162
    # 1) CREATE MODEL
163
    if page == "create":
1✔
164
        if request.method == "POST":
1✔
165
            form = UploadForm(request.POST)
1✔
166
            if form.is_valid():
1✔
167
                model_name = form.cleaned_data["name"]
1✔
168
                if ModelUpload.objects.filter(
1✔
169
                    user=request.user, name=model_name
170
                ).exists():
171
                    messages.error(
1✔
172
                        request,
173
                        f"A model with the name '{model_name}' already exists. Please choose a different name.",
174
                    )
175
                    context["form"] = form
1✔
176
                    return render(request, "note2webapp/home.html", context)
1✔
177
                upload = form.save(commit=False)
1✔
178
                upload.user = request.user
1✔
179
                upload.save()
1✔
180
                messages.success(request, f"Model '{model_name}' created successfully!")
1✔
181
                return redirect(f"/dashboard/?page=detail&pk={upload.pk}")
1✔
182
        else:
183
            form = UploadForm()
1✔
184
        context["form"] = form
1✔
185

186
    # 2) MODEL DETAIL
187
    elif page == "detail" and pk:
1✔
188
        upload = get_object_or_404(ModelUpload, pk=pk, user=request.user)
1✔
189
        versions = upload.versions.all().order_by("-created_at")
1✔
190
        version_counts = {
1✔
191
            "total": versions.count(),
192
            "active": versions.filter(
193
                is_active=True, is_deleted=False, status="PASS"
194
            ).count(),
195
            "available": versions.filter(is_deleted=False, status="PASS").count(),
196
            "failed": versions.filter(is_deleted=False, status="FAIL").count(),
197
            "deleted": versions.filter(is_deleted=True).count(),
198
        }
199
        context.update(
1✔
200
            {"upload": upload, "versions": versions, "version_counts": version_counts}
201
        )
202

203
    # 3) ADD VERSION
204
    elif page == "add_version" and pk:
1✔
205
        upload = get_object_or_404(ModelUpload, pk=pk, user=request.user)
1✔
206
        retry_version_id = request.GET.get("retry")
1✔
207

208
        if request.method == "POST":
1✔
209
            # quick missing file check
210
            missing_files = []
1✔
211
            if not request.FILES.get("model_file"):
1✔
212
                missing_files.append("Model file (.pt)")
1✔
213
            if not request.FILES.get("predict_file"):
1✔
214
                missing_files.append("Predict file (.py)")
1✔
215
            if not request.FILES.get("schema_file"):
1✔
216
                missing_files.append("Schema file (.json)")
1✔
217
            if missing_files:
1✔
218
                messages.error(
1✔
219
                    request, f"Missing required files: {', '.join(missing_files)}"
220
                )
221
                form = VersionForm(
1✔
222
                    initial={
223
                        "tag": request.POST.get("tag", ""),
224
                        "category": request.POST.get("category", "sentiment"),
225
                    }
226
                )
227
                context.update(
1✔
228
                    {
229
                        "form": form,
230
                        "upload": upload,
231
                        "retry_version_id": (
232
                            retry_version_id if retry_version_id else None
233
                        ),
234
                        "page": "add_version",
235
                        "pk": pk,
236
                    }
237
                )
238
                return render(request, "note2webapp/home.html", context)
1✔
239

240
            form = VersionForm(request.POST, request.FILES)
1✔
241
            if form.is_valid():
1✔
242
                # 1. Hash incoming files
243
                incoming_model_hash = sha256_uploaded_file(request.FILES["model_file"])
1✔
244
                incoming_predict_hash = sha256_uploaded_file(
1✔
245
                    request.FILES["predict_file"]
246
                )
247
                incoming_schema_hash = sha256_uploaded_file(
1✔
248
                    request.FILES["schema_file"]
249
                )
250

251
                # 2. Compare to every non-deleted version’s stored files
252
                duplicate_found = False
1✔
253
                for v in ModelVersion.objects.filter(is_deleted=False):
1✔
254
                    try:
1✔
255
                        if (
1✔
256
                            v.model_file
257
                            and v.predict_file
258
                            and v.schema_file
259
                            and os.path.isfile(v.model_file.path)
260
                            and os.path.isfile(v.predict_file.path)
261
                            and os.path.isfile(v.schema_file.path)
262
                        ):
263
                            if (
1✔
264
                                sha256_file_path(v.model_file.path)
265
                                == incoming_model_hash
266
                                and sha256_file_path(v.predict_file.path)
267
                                == incoming_predict_hash
268
                                and sha256_file_path(v.schema_file.path)
269
                                == incoming_schema_hash
270
                            ):
UNCOV
271
                                duplicate_found = True
×
UNCOV
272
                                break
×
273
                    except Exception:
×
274
                        continue
×
275

276
                if duplicate_found:
1✔
277
                    msg_text = (
×
278
                        "An identical model/predict/schema bundle is already present. "
279
                        "Please upload a new version or change the files."
280
                    )
281
                    messages.error(request, msg_text)
×
UNCOV
282
                    context.update(
×
283
                        {
284
                            "form": form,
285
                            "upload": upload,
286
                            "duplicate_error": msg_text,
287
                        }
288
                    )
UNCOV
289
                    return render(request, "note2webapp/home.html", context)
×
290

291
                # 3. Create or retry the version
292
                if retry_version_id:
1✔
UNCOV
293
                    try:
×
294
                        version = ModelVersion.objects.get(
×
295
                            id=retry_version_id,
296
                            upload=upload,
297
                            status="FAIL",
298
                            is_deleted=False,
299
                        )
UNCOV
300
                        version.model_file = form.cleaned_data["model_file"]
×
UNCOV
301
                        version.predict_file = form.cleaned_data["predict_file"]
×
UNCOV
302
                        version.schema_file = form.cleaned_data["schema_file"]
×
UNCOV
303
                        version.category = form.cleaned_data["category"]
×
UNCOV
304
                        version.information = form.cleaned_data["information"]
×
305
                        version.status = "PENDING"
×
UNCOV
306
                        version.log = ""
×
UNCOV
307
                        version.save()
×
UNCOV
308
                        messages.info(
×
309
                            request, f"Retrying upload for version '{version.tag}'"
310
                        )
311
                    except ModelVersion.DoesNotExist:
×
312
                        messages.error(request, "Invalid version to retry")
×
UNCOV
313
                        return redirect(f"/dashboard/?page=detail&pk={upload.pk}")
×
314
                else:
315
                    version = form.save(commit=False)
1✔
316
                    version.upload = upload
1✔
317
                    version.save()
1✔
318

319
                # 4. validate
320
                validate_model(version)
1✔
321

322
                if version.status == "FAIL":
1✔
UNCOV
323
                    return redirect("validation_failed", version_id=version.id)
×
324

325
                if not retry_version_id:
1✔
326
                    messages.success(
1✔
327
                        request, f"Version '{version.tag}' uploaded successfully!"
328
                    )
329
                return redirect(f"/dashboard/?page=detail&pk={upload.pk}")
1✔
330

331
            else:
UNCOV
332
                messages.error(request, "Please correct the errors below.")
×
333
        else:
334
            # GET
UNCOV
335
            initial = {}
×
UNCOV
336
            if retry_version_id:
×
UNCOV
337
                try:
×
UNCOV
338
                    retry_version = ModelVersion.objects.get(
×
339
                        id=retry_version_id,
340
                        upload=upload,
341
                        status="FAIL",
342
                        is_deleted=False,
343
                    )
UNCOV
344
                    initial = {
×
345
                        "tag": retry_version.tag,
346
                        "category": retry_version.category,
347
                    }
348
                    context["retrying"] = True
×
UNCOV
349
                except ModelVersion.DoesNotExist:
×
UNCOV
350
                    messages.error(request, "Invalid version to retry")
×
UNCOV
351
                    return redirect(f"/dashboard/?page=detail&pk={upload.pk}")
×
UNCOV
352
            form = VersionForm(initial=initial)
×
353

UNCOV
354
        context.update(
×
355
            {
356
                "form": form,
357
                "upload": upload,
358
                "retry_version_id": retry_version_id if retry_version_id else None,
359
            }
360
        )
361

362
    return render(request, "note2webapp/home.html", context)
1✔
363

364

365
# ---------------------------------------------------------
366
# REVIEWER DASHBOARD
367
# ---------------------------------------------------------
368
@login_required
1✔
369
def reviewer_dashboard(request):
1✔
370
    page = request.GET.get("page", "list")
1✔
371
    pk = request.GET.get("pk")
1✔
372
    context = {"page": page}
1✔
373

374
    if page == "list":
1✔
375
        uploads = (
1✔
376
            ModelUpload.objects.filter(
377
                versions__is_active=True,
378
                versions__status="PASS",
379
                versions__is_deleted=False,
380
            )
381
            .distinct()
382
            .order_by("-created_at")
383
        )
384
        for upload in uploads:
1✔
385
            upload.active_version = upload.versions.filter(
×
386
                is_active=True, status="PASS", is_deleted=False
387
            ).first()
388
        context["uploads"] = uploads
1✔
389
        return render(request, "note2webapp/reviewer.html", context)
1✔
390

UNCOV
391
    if page == "detail" and pk:
×
UNCOV
392
        upload = get_object_or_404(ModelUpload, pk=pk)
×
393
        active_version = upload.versions.filter(
×
394
            is_active=True, status="PASS", is_deleted=False
395
        ).first()
UNCOV
396
        if not active_version:
×
397
            messages.warning(request, "This model has no active version.")
×
UNCOV
398
            return redirect("/reviewer/?page=list")
×
UNCOV
399
        context.update({"upload": upload, "active_version": active_version})
×
UNCOV
400
        return render(request, "note2webapp/reviewer.html", context)
×
401

UNCOV
402
    if page == "add_feedback" and pk:
×
403
        version = get_object_or_404(ModelVersion, pk=pk)
×
UNCOV
404
        if request.method == "POST":
×
UNCOV
405
            comment = request.POST.get("comment", "").strip()
×
UNCOV
406
            if comment:
×
UNCOV
407
                messages.success(request, "Feedback submitted successfully!")
×
408
                return redirect(f"/reviewer/?page=detail&pk={version.upload.pk}")
×
409
            else:
410
                messages.error(request, "Please provide feedback comment.")
×
411
        context["version"] = version
×
412
        return render(request, "note2webapp/reviewer.html", context)
×
413

414
    return redirect("/reviewer/?page=list")
×
415

416

417
# ---------------------------------------------------------
418
# VERSION SOFT DELETE
419
# ---------------------------------------------------------
420
@login_required
1✔
421
def soft_delete_version(request, version_id):
1✔
422
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
423

424
    # permissions
425
    if request.user != version.upload.user and not request.user.is_staff:
1✔
426
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
427
            return JsonResponse(
×
428
                {"success": False, "error": "Permission denied"}, status=403
429
            )
430
        messages.error(request, "You don't have permission to delete this version.")
1✔
431
        return redirect("dashboard")
1✔
432

433
    # if it's active and there are other versions -> block
434
    if version.is_active:
1✔
435
        other_versions = ModelVersion.objects.filter(
1✔
436
            upload=version.upload, is_deleted=False
437
        ).exclude(id=version_id)
438
        if other_versions.exists():
1✔
UNCOV
439
            if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
UNCOV
440
                return JsonResponse(
×
441
                    {
442
                        "success": False,
443
                        "error": "Cannot delete active version. Please activate another version first.",
444
                    },
445
                    status=400,
446
                )
447
            messages.error(
×
448
                request,
449
                "Cannot delete active version. Please activate another version first.",
450
            )
UNCOV
451
            return redirect("dashboard")
×
452

453
    if request.method == "POST":
1✔
454
        version.is_deleted = True
1✔
455
        version.deleted_at = timezone.now()
1✔
456
        version.is_active = False
1✔
457
        version.save()
1✔
458

459
        # physically remove files + folder
460
        delete_version_files_and_dir(version)
1✔
461

462
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
463
            return JsonResponse(
1✔
464
                {
465
                    "success": True,
466
                    "message": f"Version (Tag: {version.tag}) deleted successfully",
467
                    "reload": True,
468
                }
469
            )
470

471
        messages.success(
1✔
472
            request, f"Version with tag '{version.tag}' has been deleted successfully."
473
        )
474
        return redirect(f"/model-versions/{version.upload.id}/")
1✔
475

476
    return redirect("dashboard")
×
477

478

479
# ---------------------------------------------------------
480
# ACTIVATE / DEPRECATE VERSION
481
# ---------------------------------------------------------
482
@login_required
1✔
483
def activate_version(request, version_id):
1✔
484
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
485
    if request.user != version.upload.user and not request.user.is_staff:
1✔
486
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
487
            return JsonResponse(
×
488
                {
489
                    "success": False,
490
                    "error": "You don't have permission to activate this version.",
491
                },
492
                status=403,
493
            )
494
        messages.error(request, "You don't have permission to activate this version.")
×
UNCOV
495
        return redirect("dashboard")
×
496

497
    if version.is_deleted:
1✔
498
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
499
            return JsonResponse(
1✔
500
                {"success": False, "error": "Cannot activate a deleted version."},
501
                status=400,
502
            )
503
        messages.error(request, "Cannot activate a deleted version.")
1✔
504
        return redirect("model_versions", model_id=version.upload.id)
1✔
505

UNCOV
506
    if version.status != "PASS":
×
507
        status_msg = (
×
508
            "pending validation"
509
            if version.status == "PENDING"
510
            else "that failed validation"
511
        )
UNCOV
512
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
UNCOV
513
            return JsonResponse(
×
514
                {
515
                    "success": False,
516
                    "error": f"Cannot activate a version that is {status_msg}.",
517
                },
518
                status=400,
519
            )
UNCOV
520
        messages.error(
×
521
            request,
522
            f"Cannot activate a version that is {status_msg}. Please wait for validation to complete or upload a new version.",
523
        )
UNCOV
524
        return redirect("model_versions", model_id=version.upload.id)
×
525

526
    # deactivate all versions of this model, then activate this one
UNCOV
527
    ModelVersion.objects.filter(upload=version.upload).update(is_active=False)
×
UNCOV
528
    version.is_active = True
×
UNCOV
529
    version.save()
×
530

531
    if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
UNCOV
532
        return JsonResponse(
×
533
            {"success": True, "message": f"Version '{version.tag}' is now active."}
534
        )
535

536
    messages.success(
×
537
        request,
538
        f"Version '{version.tag}' is now active. Other versions have been deactivated.",
539
    )
UNCOV
540
    return redirect("model_versions", model_id=version.upload.id)
×
541

542

543
@login_required
1✔
544
def deprecate_version(request, version_id):
1✔
545
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
546
    if request.user != version.upload.user and not request.user.is_staff:
1✔
UNCOV
547
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
UNCOV
548
            return JsonResponse(
×
549
                {"success": False, "error": "Permission denied"}, status=403
550
            )
UNCOV
551
        messages.error(request, "You don't have permission to deprecate this version.")
×
UNCOV
552
        return redirect("dashboard")
×
553

554
    if version.is_deleted:
1✔
UNCOV
555
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
UNCOV
556
            return JsonResponse(
×
557
                {"success": False, "error": "Cannot deprecate deleted version"},
558
                status=400,
559
            )
UNCOV
560
        messages.error(request, "Cannot deprecate a deleted version.")
×
561
        return redirect("dashboard")
×
562

563
    if request.method == "POST":
1✔
564
        version.is_active = False
1✔
565
        version.save()
1✔
566
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
567
            return JsonResponse(
×
568
                {
569
                    "success": True,
570
                    "message": f'Version with tag "{version.tag}" has been deprecated (deactivated)',
571
                    "version_id": version.id,
572
                }
573
            )
574
        messages.success(
1✔
575
            request, f"Version with tag '{version.tag}' has been deprecated."
576
        )
577
        return redirect(f"/model-versions/{version.upload.id}/")
1✔
578

UNCOV
579
    return redirect("dashboard")
×
580

581

582
# ---------------------------------------------------------
583
# DELETE MODEL
584
# ---------------------------------------------------------
585
@login_required
1✔
586
def delete_model(request, model_id):
1✔
587
    model_upload = get_object_or_404(ModelUpload, id=model_id)
1✔
588

589
    if request.user != model_upload.user and not request.user.is_staff:
1✔
UNCOV
590
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
×
591
            return JsonResponse(
×
592
                {"success": False, "error": "Permission denied"}, status=403
593
            )
UNCOV
594
        messages.error(request, "You don't have permission to delete this model.")
×
UNCOV
595
        return redirect("dashboard")
×
596

597
    # only count non-deleted versions
598
    remaining = ModelVersion.objects.filter(
1✔
599
        upload=model_upload, is_deleted=False
600
    ).count()
601

602
    if remaining > 0:
1✔
603
        msg = f"Cannot delete model with {remaining} active versions. Please delete all versions first."
1✔
604
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
UNCOV
605
            return JsonResponse({"success": False, "error": msg}, status=400)
×
606
        messages.error(request, msg)
1✔
607
        return redirect("dashboard")
1✔
608

609
    if request.method == "POST":
1✔
610
        model_name = model_upload.name
1✔
611
        delete_model_media_tree(model_upload)
1✔
612
        model_upload.delete()
1✔
613

614
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
1✔
615
            return JsonResponse(
×
616
                {
617
                    "success": True,
618
                    "message": f'Model "{model_name}" has been permanently deleted.',
619
                }
620
            )
621
        messages.success(request, f'Model "{model_name}" has been permanently deleted.')
1✔
622
        return redirect("dashboard")
1✔
623

UNCOV
624
    return redirect("dashboard")
×
625

626

627
# ---------------------------------------------------------
628
# MODEL VERSIONS PAGE
629
# ---------------------------------------------------------
630
@login_required
1✔
631
def model_versions(request, model_id):
1✔
632
    model_upload = get_object_or_404(ModelUpload, pk=model_id, user=request.user)
1✔
633
    versions = model_upload.versions.all().order_by("-created_at")
1✔
634

635
    context = {
1✔
636
        "model_upload": model_upload,
637
        "versions": versions,
638
        "total_count": versions.count(),
639
        "active_count": versions.filter(
640
            is_active=True, is_deleted=False, status="PASS"
641
        ).count(),
642
        "available_count": versions.filter(is_deleted=False, status="PASS").count(),
643
        "failed_count": versions.filter(is_deleted=False, status="FAIL").count(),
644
        "deleted_count": versions.filter(is_deleted=True).count(),
645
    }
646
    return render(request, "note2webapp/model_versions.html", context)
1✔
647

648

649
# ---------------------------------------------------------
650
# EDIT VERSION INFORMATION
651
# ---------------------------------------------------------
652
class VersionInformationForm(forms.ModelForm):
1✔
653
    class Meta:
1✔
654
        model = ModelVersion
1✔
655
        fields = ["information"]
1✔
656
        widgets = {
1✔
657
            "information": forms.Textarea(
658
                attrs={
659
                    "rows": 6,
660
                    "placeholder": "Enter information about this model version...",
661
                }
662
            )
663
        }
664

665
    def __init__(self, *args, **kwargs):
1✔
666
        super().__init__(*args, **kwargs)
1✔
667
        self.fields["information"].required = True
1✔
668
        self.fields["information"].label = "Model Information"
1✔
669

670

671
@login_required
1✔
672
def edit_version_information(request, version_id):
1✔
673
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
674
    if version.upload.user != request.user:
1✔
675
        messages.error(request, "You don't have permission to edit this version.")
×
UNCOV
676
        return redirect("dashboard")
×
677

678
    if request.method == "POST":
1✔
679
        form = VersionInformationForm(request.POST, instance=version)
1✔
680
        if form.is_valid():
1✔
681
            form.save()
1✔
682
            messages.success(
1✔
683
                request, f"Information for version {version.tag} updated successfully!"
684
            )
685
            return redirect("model_versions", model_id=version.upload.id)
1✔
686
    else:
687
        form = VersionInformationForm(instance=version)
1✔
688

689
    return render(
1✔
690
        request,
691
        "note2webapp/edit_version_information.html",
692
        {"form": form, "version": version},
693
    )
694

695

696
# ---------------------------------------------------------
697
# TEST MODEL (CPU)
698
# ---------------------------------------------------------
699
@login_required
1✔
700
def test_model_cpu(request, version_id):
1✔
701
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
702

703
    result = None
1✔
704
    parse_error = None
1✔
705
    last_input = ""
1✔
706

707
    if request.method == "POST":
1✔
708
        raw_input = request.POST.get("input_data", "").strip()
1✔
709
        last_input = raw_input
1✔
710

711
        if raw_input:
1✔
712
            try:
1✔
713
                parsed = json.loads(raw_input)
1✔
714

715
                if isinstance(parsed, list):
1✔
UNCOV
716
                    outputs = []
×
UNCOV
717
                    for item in parsed:
×
UNCOV
718
                        if not isinstance(item, dict):
×
UNCOV
719
                            outputs.append(
×
720
                                {
721
                                    "status": "error",
722
                                    "error": 'Each item in the list must be a JSON object like {"text": "..."}',
723
                                }
724
                            )
725
                        else:
UNCOV
726
                            outputs.append(test_model_on_cpu(version, item))
×
NEW
727
                    result = {"status": "ok", "batch": True, "outputs": outputs}
×
728

729
                elif isinstance(parsed, dict):
1✔
730
                    result = test_model_on_cpu(version, parsed)
1✔
731

732
                else:
UNCOV
733
                    parse_error = (
×
734
                        "Top-level JSON must be either an object {...} or a list [...]."
735
                    )
736

737
            except json.JSONDecodeError as e:
1✔
738
                raw_msg = str(e)
1✔
739
                stripped = raw_input.lstrip()
1✔
740
                friendly = raw_msg
1✔
741

742
                if raw_msg.startswith("Extra data"):
1✔
NEW
743
                    friendly = (
×
744
                        "Your JSON has extra data. Did you forget to wrap it in { ... } ? "
745
                        'Example: {"text": "Party"}'
746
                    )
747
                elif raw_msg.startswith(
1✔
748
                    "Expecting property name enclosed in double quotes"
749
                ):
750
                    friendly = 'Invalid JSON: Property names must be in double quotes. Example: {"text": "Hello"}'
1✔
NEW
751
                elif raw_msg.startswith("Expecting value") and stripped.startswith("{"):
×
NEW
752
                    friendly = 'Invalid JSON: String values must be in double quotes. Example: {"text": "Hello"}'
×
NEW
753
                elif (
×
754
                    raw_msg.startswith("Expecting value")
755
                    and not stripped.startswith("{")
756
                    and not stripped.startswith("[")
757
                ):
NEW
758
                    friendly = (
×
759
                        "JSON must start with { ... } (object) or [ ... ] (list). "
760
                        'Example: {"text": "Hello"}'
761
                    )
NEW
762
                elif raw_msg.startswith("Unterminated string starting at"):
×
NEW
763
                    friendly = "You have an opening quote without a closing quote."
×
NEW
764
                elif raw_msg.startswith("Expecting ',' delimiter"):
×
NEW
765
                    friendly = "You may be missing a comma between fields."
×
766

767
                parse_error = friendly
1✔
768

769
    return render(
1✔
770
        request,
771
        "note2webapp/test_model.html",
772
        {
773
            "version": version,
774
            "result": result,
775
            "last_input": last_input,
776
            "parse_error": parse_error,
777
        },
778
    )
779

780

781
# ---------------------------------------------------------
782
# SIMPLE API HOOKS
783
# ---------------------------------------------------------
784
@login_required
1✔
785
def run_model_from_path(request):
1✔
786
    if request.method != "POST":
1✔
787
        return JsonResponse({"error": "POST required"}, status=405)
1✔
788

789
    model_path = request.POST.get("model_path")
1✔
790
    predict_path = request.POST.get("predict_path")
1✔
791
    raw_input = request.POST.get("input_data", "{}")
1✔
792

793
    try:
1✔
794
        input_data = json.loads(raw_input)
1✔
795
    except Exception:
1✔
796
        return JsonResponse({"error": "Invalid JSON in input_data"}, status=400)
1✔
797

UNCOV
798
    spec = importlib.util.spec_from_file_location("predict_module", predict_path)
×
UNCOV
799
    module = importlib.util.module_from_spec(spec)
×
UNCOV
800
    spec.loader.exec_module(module)
×
801

UNCOV
802
    if not hasattr(module, "predict"):
×
UNCOV
803
        return JsonResponse({"error": "predict() not found in predict.py"}, status=400)
×
804

UNCOV
805
    sig = inspect.signature(module.predict)
×
UNCOV
806
    num_params = len(sig.parameters)
×
UNCOV
807
    if num_params == 1:
×
UNCOV
808
        out = module.predict(input_data)
×
809
    else:
UNCOV
810
        out = module.predict(model_path, input_data)
×
811

UNCOV
812
    return JsonResponse({"output": out})
×
813

814

815
@login_required
1✔
816
def run_model_by_version_id(request, version_id):
1✔
817
    version = get_object_or_404(ModelVersion, id=version_id)
1✔
818

819
    if request.method != "POST":
1✔
820
        return JsonResponse({"error": "POST required"}, status=405)
1✔
821

822
    raw_input = request.POST.get("input_data", "{}")
1✔
823
    try:
1✔
824
        input_data = json.loads(raw_input)
1✔
UNCOV
825
    except Exception:
×
UNCOV
826
        return JsonResponse({"error": "Invalid JSON"}, status=400)
×
827

828
    result = test_model_on_cpu(version, input_data)
1✔
829
    return JsonResponse(result)
1✔
830

831

832
# ---------------------------------------------------------
833
# ADMIN STATS VIEW  ( /admin/stats/ )
834
# ---------------------------------------------------------
835
@staff_member_required
1✔
836
def admin_stats(request):
1✔
837
    # top counts
838
    total_uploads = ModelUpload.objects.count()
1✔
839
    total_versions = ModelVersion.objects.count()
1✔
840
    total_users = User.objects.count()
1✔
841

842
    # version breakdown
843
    active_versions = ModelVersion.objects.filter(
1✔
844
        is_deleted=False, is_active=True, status="PASS"
845
    ).count()
846
    deleted_versions = ModelVersion.objects.filter(is_deleted=True).count()
1✔
847
    inactive_versions = ModelVersion.objects.filter(
1✔
848
        is_deleted=False, status="PASS", is_active=False
849
    ).count()
850

851
    # superusers shown as admin in the pie chart
852
    superusers_count = User.objects.filter(is_superuser=True).count()
1✔
853

854
    # roles for non-superusers
855
    role_rows = list(
1✔
856
        Profile.objects.filter(user__is_superuser=False)
857
        .values("role")
858
        .annotate(c=Count("id"))
859
        .order_by("role")
860
    )
861

862
    # users without profile (non-superuser)
863
    no_profile_count = User.objects.filter(
1✔
864
        profile__isnull=True, is_superuser=False
865
    ).count()
866
    if no_profile_count:
1✔
NEW
867
        role_rows.append({"role": "no-profile", "c": no_profile_count})
×
868

869
    # add admin bucket
870
    if superusers_count:
1✔
871
        role_rows.append({"role": "admin", "c": superusers_count})
1✔
872

873
    # versions by status
874
    status_rows = list(
1✔
875
        ModelVersion.objects.values("status").annotate(c=Count("id")).order_by("status")
876
    )
877

878
    # versions by category
879
    category_rows = list(
1✔
880
        ModelVersion.objects.values("category")
881
        .annotate(c=Count("id"))
882
        .order_by("category")
883
    )
884

885
    # top uploaders
886
    top_uploaders = list(
1✔
887
        ModelUpload.objects.values("user__username")
888
        .annotate(c=Count("id"))
889
        .order_by("-c")[:10]
890
    )
891

892
    context = {
1✔
893
        "total_uploads": total_uploads,
894
        "total_versions": total_versions,
895
        "total_users": total_users,
896
        "active_versions": active_versions,
897
        "deleted_versions": deleted_versions,
898
        "inactive_versions": inactive_versions,
899
        "role_counts_json": json.dumps(role_rows),
900
        "version_status_counts_json": json.dumps(status_rows),
901
        "version_category_counts_json": json.dumps(category_rows),
902
        "top_uploaders_json": json.dumps(top_uploaders),
903
        "top_uploaders": top_uploaders,
904
    }
905
    return render(request, "note2webapp/admin_stats.html", context)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc