Skip to content
Merged
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
20 changes: 18 additions & 2 deletions FusionIIIT/applications/academic_procedures/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3767,8 +3767,24 @@ def upload_excel_replacement(request):
}

def check_role(request, required_role):
role = request.query_params.get('role') if request.method=='GET' else request.data.get('role')
return role == required_role
# Authorize from the user's real designation, never a client-supplied role
from django.db.models import Q
user = request.user
if not getattr(user, "is_authenticated", False):
return False
held = HoldsDesignation.objects.filter(Q(working=user) | Q(user=user))
if required_role == 'hod':
return held.filter(designation__name__istartswith='HOD').exists()
if required_role == 'faculty':
return (
Faculty.objects.filter(id__user=user).exists()
or held.filter(
designation__name__in=[
'Professor', 'Associate Professor', 'Assistant Professor',
]
).exists()
)
return held.filter(designation__name=required_role).exists()

def get_allowed_specs(user):
"""
Expand Down
2 changes: 2 additions & 0 deletions FusionIIIT/applications/examination/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
url(r'result-announcements/', views.ResultAnnouncementListAPI.as_view(), name="result-announcements"),
url(r'update-announcement/', views.UpdateAnnouncementAPI.as_view(), name="update-announcement"),
url(r'create-announcement/', views.CreateAnnouncementAPI.as_view(), name="create-announcement"),
url(r'^announcement-students/$', views.AnnouncementStudentsAPI.as_view(), name="announcement-students"),
url(r'^publish-result-selected/$', views.PublishResultSelectedAPI.as_view(), name="publish-result-selected"),
url(r'unique-course-reg-years/', views.UniqueRegistrationYearsView.as_view(), name="unique-course-reg-years"),
url(r'unique-stu-grades-years/', views.UniqueStudentGradeYearsView.as_view(), name="unique-stu-grades-years"),
url(r'^student/result_semesters/$', views.StudentSemesterListView.as_view(), name='get_student_semesters'),
Expand Down
225 changes: 183 additions & 42 deletions FusionIIIT/applications/examination/api/views.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('examination', '0003_resultannouncement_semester_type'),
]

operations = [
migrations.AddField(
model_name='resultannouncement',
name='per_student_selection',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='PublishedResultStudent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('roll_no', models.CharField(max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('announcement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='published_students', to='examination.resultannouncement')),
],
options={
'unique_together': {('announcement', 'roll_no')},
},
),
]
28 changes: 28 additions & 0 deletions FusionIIIT/applications/examination/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class ResultAnnouncement(models.Model):
blank=True,
)
announced = models.BooleanField(default=False)
# True once the result is published via per-student selection. When True,
# only students listed in PublishedResultStudent see their result; when
# False the announcement is published for the whole batch (legacy).
per_student_selection = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
Expand All @@ -65,3 +69,27 @@ def __str__(self):
else:
sem_label = f"Sem {self.semester}"
return f"{self.batch.name} - {sem_label} - {status}"


class PublishedResultStudent(models.Model):
"""Per-student selection for a result announcement.

When the academic admin publishes a result they can pick exactly which
students of the batch are included. A row here means "this student's result
for this announcement is published". If an announcement has no rows at all,
it is treated as published for the whole batch (legacy behaviour).
"""

announcement = models.ForeignKey(
ResultAnnouncement,
on_delete=models.CASCADE,
related_name="published_students",
)
roll_no = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = [("announcement", "roll_no")]

def __str__(self):
return f"{self.announcement_id} - {self.roll_no}"
128 changes: 128 additions & 0 deletions FusionIIIT/applications/globals/access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@











"""Server-side role / designation checks.
Authorization must be derived from the authenticated user's real designation
"""

from django.db.models import Q


def user_holds_role(user, role):
"""True if ``user`` actually holds the designation named ``role``."""
if not role or not getattr(user, "is_authenticated", False):
return False
from applications.globals.models import HoldsDesignation

return HoldsDesignation.objects.filter(
Q(working=user) | Q(user=user),
designation__name=role,
).exists()


def user_holds_any_role(user, roles):
"""True if ``user`` holds any designation in ``roles``."""
if not getattr(user, "is_authenticated", False):
return False
names = [r for r in roles if r]
if not names:
return False
from applications.globals.models import HoldsDesignation

return HoldsDesignation.objects.filter(
Q(working=user) | Q(user=user),
designation__name__in=names,
).exists()


# --- Reusable DRF permission classes (declarative, fail-closed) -------------
# Prefer these on admin views: `permission_classes = [IsAcadAdminOrDean]` or
# `permission_classes = [has_any_role("acadadmin", "Dean Academic")]`. They
# authorize from the real designation and deny by default.
from rest_framework.permissions import BasePermission # noqa: E402


class HasDesignation(BasePermission):
"""Allow only users who hold one of ``allowed_roles`` (designation names)."""

allowed_roles = ()
message = "You do not have permission to perform this action."

def has_permission(self, request, view):
return user_holds_any_role(request.user, self.allowed_roles)


def has_any_role(*roles):
"""Factory: return a permission class allowing any of ``roles``."""

class _HasAnyRole(HasDesignation):
allowed_roles = roles

return _HasAnyRole


class IsAcadAdmin(HasDesignation):
allowed_roles = ("acadadmin",)


class IsDeanAcademic(HasDesignation):
allowed_roles = ("Dean Academic",)


class IsAcadAdminOrDean(HasDesignation):
allowed_roles = ("acadadmin", "Dean Academic")


# --- Decorator for PLAIN Django views (non-DRF) -----------------------------
# Plain function views (e.g. those using @require_http_methods) bypass DRF, so
# DRF auth/permission don't apply and the token header is ignored. This
# decorator authenticates the token itself and enforces the designation, so
# such views are no longer effectively open. Apply it as the OUTERMOST decorator.
from functools import wraps # noqa: E402

from django.http import JsonResponse # noqa: E402


def _user_from_request(request):
"""Resolve the user from a DRF-authenticated request or a Token header."""
existing = getattr(request, "user", None)
if getattr(existing, "is_authenticated", False):
return existing
auth = request.META.get("HTTP_AUTHORIZATION", "")
if not auth.startswith("Token "):
return None
key = auth.split(" ", 1)[1].strip()
from rest_framework.authtoken.models import Token

try:
return Token.objects.select_related("user").get(key=key).user
except Token.DoesNotExist:
return None


def require_designation(*roles):
"""Gate a plain Django view to users holding one of ``roles`` (server-side)."""

def decorator(view_func):
@wraps(view_func)
def _wrapped(request, *args, **kwargs):
user = _user_from_request(request)
if user is None or not user_holds_any_role(user, roles):
return JsonResponse(
{"error": "You do not have permission to perform this action."},
status=403,
)
request.user = user
return view_func(request, *args, **kwargs)

return _wrapped

return decorator
Loading
Loading