Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions FusionIIIT/Fusion/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,20 @@
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Asia/Calcutta'

# Base URL of the React frontend, used to build links (e.g. emailed thesis
# examiner invitation/review links) that must point at frontend pages rather
# than raw backend API endpoints.
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:5173')

CELERY_BEAT_SCHEDULE = {
'leave-migration-task': {
'task': 'applications.leave.tasks.execute_leave_migrations',
'schedule': crontab(minute='1', hour='0')
},
'phd-thesis-review-invitations-task': {
'task': 'applications.academic_procedures.tasks.process_review_invitations',
'schedule': crontab(minute='0', hour='2')
}
}

Expand Down
5 changes: 5 additions & 0 deletions FusionIIIT/Fusion/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

ALLOWED_HOSTS = ['*']

# Local dev has no real SMTP credentials (EMAIL_HOST_PASSWORD is only set in
# production.py from the environment), so outgoing mail is printed to the
# runserver console instead of actually being sent.
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
Expand Down
4 changes: 3 additions & 1 deletion FusionIIIT/applications/academic_information/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,9 @@ def generate_xlsheet_api(request):
if programme_type.upper() == 'UG':
sql += " AND s.programme IN ('B.Tech', 'B.Des')"
elif programme_type.upper() == 'PG':
sql += " AND s.programme IN ('M.Tech', 'M.Des', 'PhD')"
sql += " AND s.programme IN ('M.Tech', 'M.Des')"
elif programme_type.upper() == 'PHD':
sql += " AND s.programme = 'PhD'"
else:
sql += " AND s.programme = %s"
params.append(programme_type)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.5 on 2026-03-06 11:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('academic_information', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='student',
name='specialization',
field=models.CharField(choices=[('Power and Control', 'Power and Control'), ('Power & Control', 'Power & Control'), ('Microwave and Communication Engineering', 'Microwave and Communication Engineering'), ('Communication and Signal Processing', 'Communication and Signal Processing'), ('Micro-nano Electronics', 'Micro-nano Electronics'), ('Nanoelectronics and VLSI Design', 'Nanoelectronics and VLSI Design'), ('CAD/CAM', 'CAD/CAM'), ('Design', 'Design'), ('Manufacturing', 'Manufacturing'), ('Manufacturing and Automation', 'Manufacturing and Automation'), ('CSE', 'CSE'), ('AI & ML', 'AI & ML'), ('Data Science', 'Data Science'), ('Mechatronics', 'Mechatronics'), ('MDes', 'MDes'), ('None', 'None'), ('', 'No Specialization')], default='', max_length=40, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.1.5 on 2026-03-14 16:04

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('academic_information', '0002_thesis_registration_models'),
('academic_information', '0002_auto_20260210_1820'),
]

operations = [
]
2 changes: 0 additions & 2 deletions FusionIIIT/applications/academic_information/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,6 @@ def update_calendar(request):


#Generate Attendance Sheet
@login_required(login_url='/accounts/login')
def sem_for_generate_sheet():
"""
This function generates semester grade sheet
Expand Down Expand Up @@ -1426,7 +1425,6 @@ def add_new_profile (request):
return render(request, "ais/ais.html", context)


@login_required(login_url='/accounts/login')
def get_faculty_list():
"""
to get faculty list from database
Expand Down
172 changes: 171 additions & 1 deletion FusionIIIT/applications/academic_procedures/admin.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,183 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone

from .models import (BranchChange, CoursesMtech, FeePayments, FinalRegistration, InitialRegistration,StudentRegistrationChecks,
MinimumCredits, Register, Thesis, CourseRequested,
ThesisTopicProcess, FeePayment, TeachingCreditRegistration,
SemesterMarks, MarkSubmissionCheck,Dues,MTechGraduateSeminarReport,PhDProgressExamination,AssistantshipClaim,MessDue,Assistantship_status, course_registration)
SemesterMarks, MarkSubmissionCheck,Dues,MTechGraduateSeminarReport,PhDProgressExamination,AssistantshipClaim,MessDue,Assistantship_status, course_registration,
ThesisSubmission, ReviewInvitation)

class RegisterAdmin(admin.ModelAdmin):
model = Register
search_fields = ('curr_id__course_code',)


@admin.register(ThesisSubmission)
class ThesisSubmissionAdmin(admin.ModelAdmin):
list_display = ['id', 'get_thesis_title', 'get_student', 'status', 'submitted_at', 'supervisor_approval', 'director_approval']
list_filter = ['status', 'submitted_at', 'supervisor_approved_at', 'director_approved_at']
search_fields = ['thesis__research_theme', 'thesis__student__id__user__username', 'thesis__student__id__user__first_name', 'thesis__student__id__user__last_name']
readonly_fields = ['file_token', 'submitted_at', 'updated_at', 'get_synopsis_link', 'get_report_link']
date_hierarchy = 'submitted_at'
ordering = ['-submitted_at']

fieldsets = (
('Basic Information', {
'fields': ('thesis', 'status', 'file_token')
}),
('Files', {
'fields': ('synopsis', 'get_synopsis_link', 'thesis_report', 'get_report_link')
}),
('Approvals', {
'fields': ('supervisor', 'supervisor_approved_at', 'director', 'director_approved_at')
}),
('Timestamps', {
'fields': ('submitted_at', 'updated_at'),
'classes': ('collapse',)
}),
)

def get_thesis_title(self, obj):
return obj.thesis.research_theme
get_thesis_title.short_description = 'Thesis Title'
get_thesis_title.admin_order_field = 'thesis__research_theme'

def get_student(self, obj):
if obj.thesis and obj.thesis.student:
return obj.thesis.student.id.user.get_full_name() or obj.thesis.student.id.user.username
return '-'
get_student.short_description = 'Student'

def supervisor_approval(self, obj):
if obj.supervisor_approved_at:
return format_html('<span style="color: green;">✓ {}</span>', obj.supervisor_approved_at.strftime('%Y-%m-%d'))
return format_html('<span style="color: orange;">Pending</span>')
supervisor_approval.short_description = 'Supervisor'

def director_approval(self, obj):
if obj.director_approved_at:
return format_html('<span style="color: green;">✓ {}</span>', obj.director_approved_at.strftime('%Y-%m-%d'))
return format_html('<span style="color: orange;">Pending</span>')
director_approval.short_description = 'Director'

def get_synopsis_link(self, obj):
if obj.synopsis:
return format_html('<a href="{}" target="_blank">View Synopsis</a>', obj.synopsis.url)
return '-'
get_synopsis_link.short_description = 'Synopsis Link'

def get_report_link(self, obj):
if obj.thesis_report:
return format_html('<a href="{}" target="_blank">View Report</a>', obj.thesis_report.url)
return '-'
get_report_link.short_description = 'Report Link'


@admin.register(ReviewInvitation)
class ReviewInvitationAdmin(admin.ModelAdmin):
list_display = ['id', 'prof_name', 'prof_email', 'get_thesis', 'status', 'priority', 'last_sent', 'expires_at', 'is_expired_badge']
list_filter = ['status', 'priority', 'created_at', 'expires_at']
search_fields = ['prof_name', 'prof_email', 'submission__thesis__research_theme']
readonly_fields = ['token', 'created_at', 'updated_at', 'get_accept_url', 'get_reject_url', 'get_review_url']
date_hierarchy = 'created_at'
ordering = ['submission', 'priority']
actions = ['resend_invitation', 'mark_expired', 'send_review_form']

fieldsets = (
('Professor Information', {
'fields': ('prof_name', 'prof_position', 'prof_email', 'prof_phone', 'prof_address', 'prof_time_ranking')
}),
('Invitation Details', {
'fields': ('submission', 'priority', 'token', 'status')
}),
('Action URLs', {
'fields': ('get_accept_url', 'get_reject_url', 'get_review_url'),
'classes': ('collapse',)
}),
('Email Tracking', {
'fields': ('last_sent', 'review_form_sent', 'expires_at')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

def get_thesis(self, obj):
return obj.submission.thesis.research_theme
get_thesis.short_description = 'Thesis'
get_thesis.admin_order_field = 'submission__thesis__research_theme'

def is_expired_badge(self, obj):
if obj.is_expired():
return format_html('<span style="color: red; font-weight: bold;">EXPIRED</span>')
elif obj.expires_at:
days_left = (obj.expires_at - timezone.now()).days
if days_left <= 7:
return format_html('<span style="color: orange;">{} days left</span>', days_left)
return format_html('<span style="color: green;">{} days left</span>', days_left)
return '-'
is_expired_badge.short_description = 'Expiry Status'

def get_accept_url(self, obj):
from django.conf import settings
url = getattr(settings, 'SITE_URL', 'http://localhost:8000')
url += reverse('procedures:invitation_action', args=[obj.token, 'accept'])
return format_html('<a href="{}" target="_blank">{}</a>', url, url)
get_accept_url.short_description = 'Accept URL'

def get_reject_url(self, obj):
from django.conf import settings
url = getattr(settings, 'SITE_URL', 'http://localhost:8000')
url += reverse('procedures:invitation_action', args=[obj.token, 'reject'])
return format_html('<a href="{}" target="_blank">{}</a>', url, url)
get_reject_url.short_description = 'Reject URL'

def get_review_url(self, obj):
from django.conf import settings
url = getattr(settings, 'SITE_URL', 'http://localhost:8000')
url += reverse('procedures:review_detail', args=[obj.token])
return format_html('<a href="{}" target="_blank">{}</a>', url, url)
get_review_url.short_description = 'Review Form URL'

def resend_invitation(self, request, queryset):
from .utils import send_invitation_email
count = 0
for inv in queryset:
if inv.status == 'pending' and not inv.is_expired():
try:
send_invitation_email(inv)
inv.last_sent = timezone.now()
inv.save(update_fields=['last_sent'])
count += 1
except Exception as e:
self.message_user(request, f"Failed to send to {inv.prof_email}: {str(e)}", level='error')
self.message_user(request, f"Successfully resent {count} invitation(s)")
resend_invitation.short_description = "Resend invitation email to selected reviewers"

def mark_expired(self, request, queryset):
count = queryset.filter(status='pending').update(status='expired')
self.message_user(request, f"Marked {count} invitation(s) as expired")
mark_expired.short_description = "Mark selected invitations as expired"

def send_review_form(self, request, queryset):
from .utils import send_review_form_email
count = 0
for inv in queryset:
if inv.status == 'accepted':
try:
send_review_form_email(inv)
inv.review_form_sent = timezone.now()
inv.save(update_fields=['review_form_sent'])
count += 1
except Exception as e:
self.message_user(request, f"Failed to send to {inv.prof_email}: {str(e)}", level='error')
self.message_user(request, f"Successfully sent review form to {count} reviewer(s)")
send_review_form.short_description = "Send review form email to accepted reviewers"


admin.site.register(Thesis)
admin.site.register(Register,RegisterAdmin)
admin.site.register(BranchChange)
Expand All @@ -31,3 +200,4 @@ class RegisterAdmin(admin.ModelAdmin):
admin.site.register(FinalRegistration)
admin.site.register(StudentRegistrationChecks)
admin.site.register(course_registration)

87 changes: 87 additions & 0 deletions FusionIIIT/applications/academic_procedures/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,91 @@

# pg TA
url(r'^ta/stipends/$', views.ta_stipends),

# ========================================================================
# PhD-SPECIFIC URLS (Added for PhD student management)
# ========================================================================

# PhD Thesis Registration endpoints

# Student endpoints
url(r'^stu/thesis/$', views.student_thesis_api, name='student-thesis'),
url(r'^stu/thesis/download/$', views.student_download_pdf_api, name='student-thesis-download'),

# Faculty list for dropdowns
url(r'^faculty/$', views.faculty_list_api, name='faculty-list'),

# Supervisor endpoints
url(r'^supervisor/dashboard/$', views.supervisor_thesis_topic_dashboard, name='supervisor-dashboard'),
url(r'^supervisor/thesis/(?P<pk>\d+)/review/$', views.supervisor_review_api, name='supervisor-thesis-review'),

# HOD endpoints
url(r'^hod/dashboard/$', views.hod_dashboard, name='hod-dashboard'),
url(r'^hod/thesis/(?P<pk>\d+)/review/$', views.hod_review_api, name='hod-thesis-review'),

# Dean endpoints
url(r'^dean/dashboard/$', views.dean_dashboard, name='dean-dashboard'),
url(r'^dean/thesis/(?P<pk>\d+)/review/$', views.dean_review_api, name='dean-thesis-review'),
url(r'^dean/thesis/(?P<pk>\d+)/generate/$', views.dean_generate_pdf_api, name='dean-thesis-generate'),

# PhD Seminar endpoints

# Student
url(r'^seminar-reports/$', views.list_reports),
url(r'^seminar-reports/create/(?P<thesis_pk>\d+)/$', views.create_report),
url(r'^seminar-reports/(?P<pk>\d+)/$', views.detail_report),

# RPC
url(r'^seminar-reports/list/$', views.rpc_seminar_list),
url(r'^seminar-reports/(?P<pk>\d+)/rpc-detail/$', views.rpc_detail),
url(r'^seminar-reports/(?P<pk>\d+)/rpc-consent/$', views.rpc_consent),
url(r'^seminar-reports/(?P<pk>\d+)/rpc-finalize/$', views.rpc_finalize),

# ========================================================================
# Thesis Slot Semester-Level Registration (Enrollment)
# ========================================================================
# Student
url(r'^stu/thesis-enrollment/$', views.student_thesis_enrollment_api, name='student-thesis-enrollment'),
# Acad Admin
url(r'^acadadmin/thesis-enrollments/$', views.admin_thesis_enrollment_list, name='admin-thesis-enrollment-list'),
url(r'^acadadmin/thesis-enrollments/verify/$', views.admin_verify_enrollments, name='admin-verify-enrollments'),
url(r'^acadadmin/thesis-enrollments/reject/$', views.admin_reject_enrollments, name='admin-reject-enrollments'),

# PhD Thesis Evaluation (block-based S/X grades)
# Supervisor — all blocks for a student are graded and submitted together
url(r'^supervisor/thesis-grades/$', views.supervisor_thesis_grades, name='supervisor-thesis-grades'),
# All blocks comprehensive upload
url(r'^supervisor/thesis-grades-all-template/$', views.supervisor_download_all_thesis_grades_template, name='supervisor-thesis-grades-all-template'),
url(r'^supervisor/thesis-grades-all/upload/$', views.supervisor_upload_all_thesis_grades, name='supervisor-thesis-grades-all-upload'),
url(r'^supervisor/thesis-grades-all/bulk-submit/$', views.supervisor_bulk_submit_all_thesis_grades, name='supervisor-thesis-grades-all-bulk-submit'),
# Acad Admin
url(r'^acadadmin/thesis-grades/$', views.admin_thesis_grades_list, name='admin-thesis-grades-list'),
url(r'^acadadmin/thesis-grades/verify/$', views.admin_verify_thesis_grades, name='admin-thesis-grades-verify'),
url(r'^acadadmin/thesis-grades/announce/$', views.admin_announce_thesis_grades, name='admin-thesis-grades-announce'),

# PhD Thesis Submission

# Student
url(r'^thesis/submit/$', views.thesis_submit, name='thesis_submit'),

# Supervisor
url(r'^thesis/supervisor-dashboard/$', views.supervisor_dashboard, name='supervisor_dashboard'),
url(r'^thesis/submission-detail/(?P<submission_id>\d+)/$', views.supervisor_submission_detail, name='supervisor_submission_detail'),
url(r'^thesis/supervisor-assign/$', views.supervisor_assign, name='supervisor_assign'),

# Dean (panel approval + invitation dispatch)
url(r'^thesis/dean-dashboard/$', views.dean_panel_dashboard, name='dean_panel_dashboard'),
url(r'^thesis/dean-panel-approve/$', views.dean_panel_approve, name='dean_panel_approve'),
url(r'^thesis/dean-send-invitations/$', views.dean_send_invitations, name='dean_send_invitations'),

# Director
url(r'^thesis/director-dashboard/$', views.director_dashboard, name='director_dashboard'),
url(r'^thesis/director-approve/$', views.director_approve, name='director_approve'),

# Professor Invitation (External reviewers)
url(r'^invitation/(?P<token>[0-9a-f-]+)/(?P<action>accept|reject)/$',
views.invitation_action, name='invitation_action'),

# Review Form (External reviewers)
url(r'^review/(?P<token>[0-9a-f-]+)/$', views.review_detail, name='review_detail'),
]
Loading
Loading