Skip to content
Open
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
2 changes: 1 addition & 1 deletion FusionIIIT/Fusion/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)}<br/>'
f'{escape(settings.BONAFIDE_SIGNATORY_TITLE)}',
header_style,
),
Paragraph(
f'{escape(reference_number)}<br/>'
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'<b>{escape(context["salutation"])} {escape(context["name"])}</b> '
f'(Roll No. {escape(context["roll_number"])}) '
f'{escape(context["relation"])} '
f'<b>MR. {escape(context["father_name"])}</b> is a student of '
f'<b>{superscript_ordinal(context["year_ordinal"])} Year</b> '
f'({superscript_ordinal(context["semester_ordinal"])} Semester) '
f'<b>{escape(context["programme"])}</b> in '
f'<b>{escape(context["discipline"])}</b> '
f'({escape(context["duration_text"])} course duration: '
f'<b>{context["start_year"]}</b> to <b>{context["end_year"]}</b>) 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('<u>TO WHOM SO EVER IT MAY CONCERN</u>', 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
15 changes: 14 additions & 1 deletion FusionIIIT/applications/academic_procedures/api/urls.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -20,6 +21,18 @@
url(r'^acad/bonafide/certificates/(?P<certificate_id>\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<certificate_id>\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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
Loading
Loading