diff --git a/FusionIIIT/Fusion/settings/development.py b/FusionIIIT/Fusion/settings/development.py
index 65381434d..87836b129 100644
--- a/FusionIIIT/Fusion/settings/development.py
+++ b/FusionIIIT/Fusion/settings/development.py
@@ -15,7 +15,7 @@
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'fusionlab',
- 'HOST': os.environ.get("DB_HOST", default='localhost'),
+ 'HOST': os.environ.get("DB_HOST", default='127.0.0.1'),
'USER': 'fusion_admin',
'PASSWORD': 'hello123',
}
diff --git a/FusionIIIT/applications/academic_procedures/api/medium_of_instruction_certificate.py b/FusionIIIT/applications/academic_procedures/api/medium_of_instruction_certificate.py
new file mode 100644
index 000000000..d008fc6d7
--- /dev/null
+++ b/FusionIIIT/applications/academic_procedures/api/medium_of_instruction_certificate.py
@@ -0,0 +1,337 @@
+import re
+from io import BytesIO
+from xml.sax.saxutils import escape
+
+from django.conf import settings
+from django.core.paginator import Paginator
+from django.db import transaction
+from django.db.models import Max
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import ParagraphStyle
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
+from rest_framework import status
+from rest_framework.authentication import TokenAuthentication
+from rest_framework.decorators import api_view, authentication_classes, permission_classes
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from applications.academic_information.models import Student
+from applications.academic_procedures.models import BonafideCertificate
+from applications.globals.decorators import role_required
+
+from .bonafide_certificate import (
+ _certificate_reference,
+ _father_name,
+ _search_certificates,
+ _student_name,
+ _student_queryset,
+ build_certificate_context,
+ superscript_ordinal,
+)
+
+CERTIFICATE_TYPE = 'medium_of_instruction'
+
+
+def render_medium_of_instruction_pdf(context, reference_number, issued_on):
+ output = BytesIO()
+ document = SimpleDocTemplate(
+ output,
+ pagesize=A4,
+ topMargin=2.5 * cm,
+ rightMargin=1.5 * cm,
+ bottomMargin=2.5 * cm,
+ leftMargin=1.5 * cm,
+ title='Medium of Instruction Certificate',
+ author=settings.BONAFIDE_INSTITUTE_NAME,
+ )
+ header_style = ParagraphStyle(
+ 'Header',
+ fontName='Helvetica-Bold',
+ fontSize=13.5,
+ leading=18,
+ alignment=TA_LEFT,
+ )
+ header_right_style = ParagraphStyle(
+ 'HeaderRight',
+ parent=header_style,
+ alignment=TA_RIGHT,
+ )
+ heading_style = ParagraphStyle(
+ 'Heading',
+ fontName='Helvetica-Bold',
+ fontSize=15,
+ leading=19,
+ alignment=TA_CENTER,
+ )
+ body_style = ParagraphStyle(
+ 'Body',
+ fontName='Helvetica',
+ fontSize=13.5,
+ leading=22,
+ alignment=TA_JUSTIFY,
+ )
+ signature_style = ParagraphStyle(
+ 'Signature',
+ fontName='Helvetica-Bold',
+ fontSize=13.5,
+ leading=18,
+ alignment=TA_LEFT,
+ )
+
+ header = Table(
+ [[
+ Paragraph(
+ f'{escape(settings.BONAFIDE_SIGNATORY_NAME)}
'
+ f'{escape(settings.BONAFIDE_SIGNATORY_TITLE)}',
+ header_style,
+ ),
+ Paragraph(
+ f'{escape(reference_number)}
'
+ f'Date: {issued_on.strftime("%d.%m.%Y")}',
+ header_right_style,
+ ),
+ ]],
+ colWidths=[9 * cm, 9 * cm],
+ )
+ header.setStyle(TableStyle([
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
+ ('LEFTPADDING', (0, 0), (-1, -1), 0),
+ ('RIGHTPADDING', (0, 0), (-1, -1), 0),
+ ('TOPPADDING', (0, 0), (-1, -1), 0),
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
+ ]))
+
+ first_text = (
+ 'This is to certify that '
+ f'{escape(context["salutation"])} {escape(context["name"])} '
+ f'(Roll No. {escape(context["roll_number"])}) '
+ f'{escape(context["relation"])} '
+ f'MR. {escape(context["father_name"])} is a student of '
+ f'{superscript_ordinal(context["year_ordinal"])} Year '
+ f'({superscript_ordinal(context["semester_ordinal"])} Semester) '
+ f'{escape(context["programme"])} in '
+ f'{escape(context["discipline"])} '
+ f'({escape(context["duration_text"])} course duration: '
+ f'{context["start_year"]} to {context["end_year"]}) at '
+ f'{escape(settings.BONAFIDE_INSTITUTE_NAME)}.'
+ )
+ second_text = (
+ 'Institute provides education mode of instruction to the students in English only.'
+ )
+
+ story = [
+ header,
+ Spacer(1, 1.65 * cm),
+ Paragraph('TO WHOM SO EVER IT MAY CONCERN', heading_style),
+ Spacer(1, 1.25 * cm),
+ Paragraph(first_text, body_style),
+ Spacer(1, 0.9 * cm),
+ Paragraph(second_text, body_style),
+ Spacer(1, 1.65 * cm),
+ Paragraph(f'({escape(settings.BONAFIDE_SIGNATORY_NAME)})', signature_style),
+ ]
+
+ document.build(story)
+ output.seek(0)
+ return output
+
+
+def _certificate_filename(certificate):
+ roll_number = certificate.student.id.user.username
+ safe_roll_number = re.sub(r'[^A-Za-z0-9_-]', '_', roll_number)
+ return f'{safe_roll_number}_Medium_of_Instruction_Certificate_{certificate.pk:03d}.pdf'
+
+
+def _certificate_pdf(certificate):
+ if certificate.pdf_content:
+ return bytes(certificate.pdf_content)
+
+ context = build_certificate_context(certificate.student)
+ if not context['is_ready']:
+ return None
+ document = render_medium_of_instruction_pdf(
+ context,
+ _certificate_reference(certificate),
+ certificate.issued_at.date(),
+ )
+ return document.getvalue()
+
+
+@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
+@role_required(['acadadmin'])
+def medium_of_instruction_student(request):
+ roll_number = (request.query_params.get('roll_number') or '').strip().upper()
+ if not roll_number:
+ return Response(
+ {'error': 'roll_number is required.'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ student = get_object_or_404(
+ _student_queryset(),
+ pk=roll_number,
+ id__user__is_active=True,
+ id__user_status='PRESENT',
+ )
+ context = build_certificate_context(student)
+ issued_on = timezone.now().date()
+ prefix = settings.BONAFIDE_REFERENCE_PREFIX.rstrip('/')
+ next_serial = (
+ BonafideCertificate.objects.aggregate(max_id=Max('pk'))['max_id'] or 0
+ ) + 1
+
+ return Response({
+ 'student': context,
+ 'certificate': {
+ 'signatory_name': settings.BONAFIDE_SIGNATORY_NAME,
+ 'signatory_title': settings.BONAFIDE_SIGNATORY_TITLE,
+ 'institute_name': settings.BONAFIDE_INSTITUTE_NAME,
+ 'issued_on': issued_on.strftime('%d.%m.%Y'),
+ 'reference_preview': (
+ f'{prefix}/{issued_on.year}/{issued_on.month:02d}/'
+ f'{context["roll_number"]}/{next_serial:03d}'
+ ),
+ },
+ })
+
+
+@api_view(['POST'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
+@role_required(['acadadmin'])
+def generate_medium_of_instruction_pdf(request):
+ student_id = request.data.get('student_id')
+ if not student_id:
+ return Response({'error': 'student_id is required.'}, status=status.HTTP_400_BAD_REQUEST)
+
+ student = get_object_or_404(_student_queryset(), pk=student_id)
+ context = build_certificate_context(student)
+ if not context['is_ready']:
+ return Response(
+ {'error': 'Student data is incomplete.', 'details': context['validation_errors']},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ issued_on = timezone.now().date()
+ prefix = settings.BONAFIDE_REFERENCE_PREFIX.rstrip('/')
+ with transaction.atomic():
+ certificate = BonafideCertificate.objects.create(
+ student=student,
+ certificate_type=CERTIFICATE_TYPE,
+ purpose='Medium of Instruction',
+ custom_purpose='',
+ issued_by=request.user,
+ )
+ reference_number = (
+ f'{prefix}/{issued_on.year}/{issued_on.month:02d}/'
+ f'{context["roll_number"]}/{certificate.pk:03d}'
+ )
+ certificate.reference_number = reference_number
+ certificate.save(update_fields=['reference_number'])
+ document = render_medium_of_instruction_pdf(
+ context,
+ reference_number,
+ issued_on,
+ )
+ document_bytes = document.getvalue()
+ certificate.pdf_content = document_bytes
+ certificate.save(update_fields=['pdf_content'])
+
+ response = HttpResponse(
+ document_bytes,
+ content_type='application/pdf',
+ )
+ response['Content-Disposition'] = (
+ f'attachment; filename="{_certificate_filename(certificate)}"'
+ )
+ response['X-Certificate-Reference'] = reference_number
+ return response
+
+
+@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
+@role_required(['acadadmin'])
+def medium_of_instruction_certificates(request):
+ search = (request.query_params.get('search') or '').strip()
+ try:
+ page_size = int(request.query_params.get('page_size', 20))
+ except (TypeError, ValueError):
+ page_size = 20
+ page_size = min(max(page_size, 1), 100)
+
+ queryset = BonafideCertificate.objects.filter(
+ certificate_type=CERTIFICATE_TYPE,
+ ).select_related(
+ 'student__id__user',
+ ).order_by('-issued_at', '-pk')
+ queryset = _search_certificates(queryset, search).distinct()
+ page = Paginator(queryset, page_size).get_page(
+ request.query_params.get('page', 1)
+ )
+
+ results = []
+ for certificate in page.object_list:
+ results.append({
+ 'id': certificate.pk,
+ 'serial_number': f'{certificate.pk:03d}',
+ 'reference_number': _certificate_reference(certificate),
+ 'roll_number': certificate.student.id.user.username,
+ 'name': _student_name(certificate.student),
+ 'purpose': 'Medium of Instruction',
+ 'issued_on': certificate.issued_at.strftime('%d.%m.%Y'),
+ })
+
+ return Response({
+ 'results': results,
+ 'count': page.paginator.count,
+ 'page': page.number,
+ 'page_size': page_size,
+ 'total_pages': page.paginator.num_pages,
+ })
+
+
+@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
+@role_required(['acadadmin'])
+def medium_of_instruction_certificate_pdf(request, certificate_id):
+ certificate = get_object_or_404(
+ BonafideCertificate.objects.filter(
+ certificate_type=CERTIFICATE_TYPE,
+ ).select_related(
+ 'student__id__user',
+ 'student__id__department',
+ 'student__batch_id__discipline',
+ 'student__batch_id__curriculum__programme',
+ ),
+ pk=certificate_id,
+ )
+ document_bytes = _certificate_pdf(certificate)
+ if document_bytes is None:
+ return Response(
+ {
+ 'error': 'The certificate cannot be rendered because the '
+ 'student record is now incomplete.'
+ },
+ status=status.HTTP_409_CONFLICT,
+ )
+
+ disposition = (
+ 'attachment'
+ if request.query_params.get('download') in {'1', 'true', 'True'}
+ else 'inline'
+ )
+ response = HttpResponse(document_bytes, content_type='application/pdf')
+ response['Content-Disposition'] = (
+ f'{disposition}; filename="{_certificate_filename(certificate)}"'
+ )
+ response['X-Certificate-Reference'] = _certificate_reference(certificate)
+ return response
diff --git a/FusionIIIT/applications/academic_procedures/api/urls.py b/FusionIIIT/applications/academic_procedures/api/urls.py
index 8f808b2f8..f60ab49f3 100644
--- a/FusionIIIT/applications/academic_procedures/api/urls.py
+++ b/FusionIIIT/applications/academic_procedures/api/urls.py
@@ -1,7 +1,8 @@
from django.conf.urls import url
from . import views
from . import (bonafide_certificate, demand_letter, fee_certificate,
- fee_structure, fee_structure_certificate)
+ fee_structure, fee_structure_certificate,
+ medium_of_instruction_certificate)
from .. import views as procedures_views
@@ -20,6 +21,18 @@
url(r'^acad/bonafide/certificates/(?P\d+)/pdf/$',
bonafide_certificate.bonafide_certificate_pdf,
name='bonafide-certificate-pdf'),
+ url(r'^acad/medium-of-instruction/student/$',
+ medium_of_instruction_certificate.medium_of_instruction_student,
+ name='medium-of-instruction-student'),
+ url(r'^acad/medium-of-instruction/pdf/$',
+ medium_of_instruction_certificate.generate_medium_of_instruction_pdf,
+ name='medium-of-instruction-pdf'),
+ url(r'^acad/medium-of-instruction/certificates/$',
+ medium_of_instruction_certificate.medium_of_instruction_certificates,
+ name='medium-of-instruction-certificates'),
+ url(r'^acad/medium-of-instruction/certificates/(?P\d+)/pdf/$',
+ medium_of_instruction_certificate.medium_of_instruction_certificate_pdf,
+ name='medium-of-instruction-certificate-pdf'),
url(r'^acad/fee-certificate/student/$', fee_certificate.fee_student,
name='fee-certificate-student'),
url(r'^acad/fee-certificate/pdf/$', fee_certificate.generate_fee_pdf,
diff --git a/FusionIIIT/applications/academic_procedures/migrations/0061_alter_bonafidecertificate_certificate_type.py b/FusionIIIT/applications/academic_procedures/migrations/0061_alter_bonafidecertificate_certificate_type.py
new file mode 100644
index 000000000..dcf050bf0
--- /dev/null
+++ b/FusionIIIT/applications/academic_procedures/migrations/0061_alter_bonafidecertificate_certificate_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.25 on 2026-10-12 09:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('academic_procedures', '0060_demandletterbankaccounts'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='bonafidecertificate',
+ name='certificate_type',
+ field=models.CharField(choices=[('bonafide', 'Bonafide Certificate'), ('fee', 'Paid / Unpaid Fee Certificate'), ('demand', 'Demand Letter'), ('fee_structure', 'Fee Structure Certificate'), ('medium_of_instruction', 'Medium of Instruction Certificate')], db_index=True, default='bonafide', max_length=32),
+ ),
+ ]
diff --git a/FusionIIIT/applications/academic_procedures/models.py b/FusionIIIT/applications/academic_procedures/models.py
index 68d1f1e7c..347fd3283 100644
--- a/FusionIIIT/applications/academic_procedures/models.py
+++ b/FusionIIIT/applications/academic_procedures/models.py
@@ -387,10 +387,11 @@ class BonafideCertificate(models.Model):
('fee', 'Paid / Unpaid Fee Certificate'),
('demand', 'Demand Letter'),
('fee_structure', 'Fee Structure Certificate'),
+ ('medium_of_instruction', 'Medium of Instruction Certificate'),
)
certificate_type = models.CharField(
- max_length=16, choices=TYPE_CHOICES, default='bonafide', db_index=True)
+ max_length=32, choices=TYPE_CHOICES, default='bonafide', db_index=True)
#: The semester rows exactly as printed, so a reissue matches the original.
fee_rows = models.JSONField(default=list, blank=True)
diff --git a/FusionIIIT/applications/globals/views.py b/FusionIIIT/applications/globals/views.py
index d8fe9d05b..43a1b35fe 100644
--- a/FusionIIIT/applications/globals/views.py
+++ b/FusionIIIT/applications/globals/views.py
@@ -1,4 +1,4 @@
-from audioop import reverse
+from django.urls import reverse
import json
from django.contrib.auth import logout
diff --git a/requirements.txt b/requirements.txt
index 21a3bfdec..be105eb54 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,77 +1,79 @@
-amqp==5.0.2
-asgiref==3.3.1
-attrs==20.3.0
-beautifulsoup4==4.9.3
-billiard==3.6.3.0
-Brotli==1.0.9
-bs4==0.0.1
-celery==5.0.5
-certifi==2020.12.5
-cffi==1.14.4
-chardet==4.0.0
-click==7.1.2
-click-didyoumean==0.0.3
-click-plugins==1.1.1
-click-repl==0.1.6
-cryptography==36.0.2
-defusedxml==0.6.0
-Django==3.1.5
-django-allauth==0.44.0
-django-cleanup==5.1.0
-django-cors-headers==3.7.0
-django-debug-toolbar==3.2.4
-django-extensions==3.1.1
-django-filter==2.4.0
-django-markdown-deux==1.0.5
-django-model-utils==4.1.1
-django-notifications-hq==1.6.0
-django-pagedown==2.2.0
-djangorestframework==3.12.2
-django-semanticui-forms==1.6.5
-django-unused-media==0.2.2
-future==0.18.2
+amqp>=5.0.2
+asgiref>=3.3.1
+attrs>=20.3.0
+beautifulsoup4>=4.9.3
+billiard>=3.6.3.0
+Brotli>=1.0.9
+bs4>=0.0.1
+celery>=5.0.5
+certifi>=2020.12.5
+cffi>=1.14.4
+chardet>=4.0.0
+click>=7.1.2
+click-didyoumean>=0.0.3
+click-plugins>=1.1.1
+click-repl>=0.1.6
+cryptography>=36.0.2
+defusedxml>=0.6.0
+Django>=3.2.0,<4.0.0
+django-allauth>=0.44.0,<0.56.0
+django-cleanup>=5.1.0
+django-cors-headers>=3.7.0,<4.0.0
+django-debug-toolbar>=3.2.4,<4.0.0
+django-extensions>=3.1.1,<3.2.0
+django-filter>=2.4.0,<24.0.0
+django-markdown-deux>=1.0.5
+django-model-utils>=4.1.1
+django-notifications-hq>=1.6.0
+django-pagedown>=2.2.0
+djangorestframework>=3.12.2,<3.15.0
+django-semanticui-forms>=1.6.5
+django-unused-media>=0.2.2
+future>=0.18.2
# gunicorn
-html-text==0.5.2
-html5lib==1.1
-idna==2.10
-ipython==8.2.0
-ipython-genutils==0.2.0
-jsonfield==3.1.0
-jsonschema==3.2.0
-kombu==5.0.2
-lxml==4.8.0
-markdown2==2.4.2
-numpy==1.22.3
-oauthlib==3.1.0
-openpyxl==3.0.7
-Pillow==8.1.0
-prompt-toolkit==3.0.10
-psycopg2-binary==2.8.6
-pycparser==2.20
-PyJWT==2.6.0
-PyPDF2==1.26.0
-pyrsistent==0.17.3
-python-bidi==0.4.2
-python-dateutil==2.9.0
-python3-openid==3.2.0
-pytz==2020.5
-reportlab==3.5.59
-requests==2.25.1
-requests-oauthlib==1.3.0
-six==1.15.0
-soupsieve==2.1
-sqlparse==0.4.2
-swapper==1.1.2.post1
-urllib3==1.26.8
-vine==5.0.0
-wcwidth==0.2.5
-webencodings==0.5.1
-whitenoise==5.2.0
-xhtml2pdf==0.2.5
-xlrd==2.0.1
-XlsxWriter==1.3.7
-xlwt==1.3.0
-django-crontab==0.7.1
-zipfile2==0.0.12
-pandas==2.0.3
-PyYAML==5.4.1 # placement_cell spec-driven test suite
+html-text>=0.5.2
+html5lib>=1.1
+idna>=2.10
+ipython>=8.2.0
+ipython-genutils>=0.2.0
+jsonfield>=3.1.0,<3.2.0
+jsonschema>=3.2.0
+kombu>=5.0.2
+lxml>=4.8.0
+markdown2>=2.4.2
+numpy>=1.22.3
+oauthlib>=3.1.0
+openpyxl>=3.0.7
+Pillow>=8.1.0
+prompt-toolkit>=3.0.10
+psycopg2-binary>=2.8.6
+pycparser>=2.20
+PyJWT>=2.6.0
+PyPDF2>=1.26.0
+pyrsistent>=0.17.3
+python-bidi>=0.4.2
+python-dateutil>=2.9.0
+python3-openid>=3.2.0
+pytz>=2020.5
+reportlab>=3.5.59
+requests>=2.25.1
+requests-oauthlib>=1.3.0
+six>=1.15.0
+soupsieve>=2.1
+sqlparse>=0.4.2
+swapper>=1.1.2
+urllib3>=1.26.8
+vine>=5.0.0
+wcwidth>=0.2.5
+webencodings>=0.5.1
+whitenoise>=5.2.0
+xhtml2pdf>=0.2.5
+xlrd>=2.0.1
+XlsxWriter>=1.3.7
+xlwt>=1.3.0
+django-crontab>=0.7.1
+zipfile2>=0.0.12
+pandas>=2.0.3
+PyYAML>=5.4.1
+
+