diff --git a/FusionIIIT/applications/academic_procedures/api/pointer_conversion_certificate.py b/FusionIIIT/applications/academic_procedures/api/pointer_conversion_certificate.py
new file mode 100644
index 000000000..587929252
--- /dev/null
+++ b/FusionIIIT/applications/academic_procedures/api/pointer_conversion_certificate.py
@@ -0,0 +1,418 @@
+import os
+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.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_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_procedures.models import BonafideCertificate
+from applications.globals.decorators import role_required
+from applications.globals.programme_scope import (
+ canonical_programme_name,
+ programme_display_name,
+)
+
+from .bonafide_certificate import (
+ _search_certificates,
+ _student_name,
+ _student_queryset,
+ ordinal,
+ register_certificate_fonts,
+)
+
+from applications.examination.api.views import calculate_cpi_for_student
+
+CERTIFICATE_TYPE = 'pointer_conv'
+
+
+def resolve_student_cpi(student):
+ """
+ Dynamically calculate student's CPI using the canonical calculate_cpi_for_student()
+ function (from examination) that calculates live weighted CPI across all completed
+ course grades. If current semester grades are not submitted yet, checks prior semesters.
+ Falls back to student.cpi if positive.
+ """
+ curr_sem = student.curr_semester_no or 1
+ # 1. Try calculate_cpi_for_student for current semester
+ try:
+ cpi_val, _, _ = calculate_cpi_for_student(student, curr_sem, None)
+ if cpi_val and float(cpi_val) > 0:
+ return float(cpi_val), curr_sem
+ except Exception:
+ pass
+
+ # 2. Check previous semesters descending
+ for sem in range(curr_sem - 1, 0, -1):
+ try:
+ cpi_val, _, _ = calculate_cpi_for_student(student, sem, None)
+ if cpi_val and float(cpi_val) > 0:
+ return float(cpi_val), sem
+ except Exception:
+ pass
+
+ # 3. Fallback to student.cpi if positive
+ if student.cpi and float(student.cpi) > 0:
+ return float(student.cpi), None
+
+ return 0.0, None
+
+
+def build_pointer_conversion_context(student):
+ batch = student.batch_id
+ name = _student_name(student)
+ gender = (student.id.sex or '').strip().upper()
+ is_female = gender == 'F'
+ salutation = 'Ms.' if is_female else 'Mr.'
+ roll_number = student.id.user.username
+ programme_full = programme_display_name(student.programme)
+ programme_abbr = canonical_programme_name(student.programme)
+
+ discipline = ''
+ if batch and batch.discipline:
+ discipline = batch.discipline.name
+ elif student.id.department:
+ discipline = student.id.department.name
+
+ semester = student.curr_semester_no
+ errors = []
+ if not name:
+ errors.append('Student name is unavailable.')
+ if not roll_number:
+ errors.append('Roll number is unavailable.')
+ if not discipline:
+ errors.append('Discipline is unavailable.')
+
+ cpi_val, till_sem = resolve_student_cpi(student)
+ if cpi_val and cpi_val > 0:
+ cpi_str = f"{cpi_val:.2f}"
+ if cpi_str.endswith('0'):
+ cpi_str = f"{cpi_val:.1f}"
+ cpi_display = cpi_str
+ else:
+ cpi_display = ''
+
+ return {
+ 'student_id': student.pk,
+ 'name': name,
+ 'roll_number': roll_number,
+ 'gender': 'Female' if is_female else 'Male' if gender == 'M' else '',
+ 'salutation': salutation,
+ 'programme': programme_full,
+ 'programme_abbr': programme_abbr,
+ 'discipline': discipline,
+ 'semester': semester,
+ 'semester_ordinal': ordinal(semester) if semester else '',
+ 'cpi': cpi_val,
+ 'cpi_display': cpi_display,
+ 'min_cpi': '5.0',
+ 'is_ready': not errors,
+ 'validation_errors': errors,
+ }
+
+
+def render_pointer_conversion_pdf(
+ context, cpi_text, min_cpi, reference_number, issued_on):
+ output = BytesIO()
+ document = SimpleDocTemplate(
+ output,
+ pagesize=A4,
+ topMargin=3.5 * cm,
+ rightMargin=1.5 * cm,
+ bottomMargin=2 * cm,
+ leftMargin=1.5 * cm,
+ title='Pointer Conversion Certificate',
+ author=settings.BONAFIDE_INSTITUTE_NAME,
+ )
+ register_certificate_fonts()
+
+ header_style = ParagraphStyle(
+ 'Header', fontName='Caladea-Bold', fontSize=11,
+ leading=15, alignment=TA_JUSTIFY,
+ )
+ header_right_style = ParagraphStyle(
+ 'HeaderRight', parent=header_style, alignment=TA_RIGHT,
+ )
+ heading_style = ParagraphStyle(
+ 'Heading', fontName='Caladea-Bold', fontSize=11,
+ leading=15, alignment=TA_CENTER,
+ )
+ body_style = ParagraphStyle(
+ 'Body', fontName='Caladea', fontSize=12,
+ leading=20, alignment=TA_JUSTIFY,
+ )
+ detail_label_style = ParagraphStyle(
+ 'DetailLabel', fontName='Caladea-Bold', fontSize=12,
+ leading=20,
+ )
+ detail_val_style = ParagraphStyle(
+ 'DetailVal', fontName='Caladea', fontSize=12,
+ leading=20,
+ )
+ signature_style = ParagraphStyle(
+ 'Signature', fontName='Caladea-Bold', fontSize=11,
+ leading=15, alignment=TA_JUSTIFY,
+ )
+
+ 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),
+ ]))
+
+ detail_rows = [
+ [
+ Paragraph('Name', detail_label_style),
+ Paragraph(':', detail_label_style),
+ Paragraph(f'{escape(context["salutation"])} {escape(context["name"])}', detail_val_style),
+ ],
+ [
+ Paragraph('Roll No.', detail_label_style),
+ Paragraph(':', detail_label_style),
+ Paragraph(escape(context["roll_number"]), detail_val_style),
+ ],
+ [
+ Paragraph('Branch', detail_label_style),
+ Paragraph(':', detail_label_style),
+ Paragraph(escape(context["discipline"]), detail_val_style),
+ ],
+ [
+ Paragraph('CPI', detail_label_style),
+ Paragraph(':', detail_label_style),
+ Paragraph(escape(str(cpi_text or '……………')), detail_val_style),
+ ],
+ ]
+ detail_table = Table(detail_rows, colWidths=[2.5 * cm, 0.6 * cm, 13 * cm])
+ detail_table.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), 2),
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 2),
+ ]))
+
+ story = [
+ header,
+ Spacer(1, 46),
+ Paragraph('TO WHOM SO EVER IT MAY CONCERN', heading_style),
+ Spacer(1, 36),
+ Paragraph(' This is to certify that:', body_style),
+ Spacer(1, 14),
+ Paragraph(
+ f'1. The graduation requirement for {escape(context["programme"])} '
+ f'({escape(context["programme_abbr"])}) programme at PDPM-IIITDM Jabalpur '
+ f'is a minimum CPI of {escape(str(min_cpi))} out of the maximum of 10.0.',
+ body_style,
+ ),
+ Spacer(1, 12),
+ Paragraph(
+ '2. Notionally, the CPI may be multiplied by a factor of 10 to obtain the numerical percentage.',
+ body_style,
+ ),
+ Spacer(1, 24),
+ Paragraph('Issued to :', body_style),
+ Spacer(1, 12),
+ detail_table,
+ Spacer(1, 46),
+ Paragraph(
+ f'{escape(settings.BONAFIDE_SIGNATORY_NAME)}
'
+ f'{escape(settings.BONAFIDE_SIGNATORY_TITLE)}',
+ signature_style,
+ ),
+ ]
+
+ document.build(story)
+ return output
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+@authentication_classes([TokenAuthentication])
+@role_required(['acadadmin'])
+def pointer_conversion_student(request):
+ roll_number = (request.query_params.get('roll_number') or '').strip().upper()
+ if not roll_number:
+ return Response(
+ {'error': 'A roll number is required.'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ try:
+ student = _student_queryset().get(id__user__username=roll_number)
+ except Exception:
+ return Response(
+ {'error': f"No student found with roll number '{roll_number}'."},
+ status=status.HTTP_404_NOT_FOUND,
+ )
+
+ context = build_pointer_conversion_context(student)
+ now = timezone.now()
+ prefix = settings.BONAFIDE_REFERENCE_PREFIX.rstrip('/')
+ reference_preview = f'{prefix}/{now.year}/{now.month:02d}/{roll_number}'
+
+ certificate_meta = {
+ 'signatory_name': settings.BONAFIDE_SIGNATORY_NAME,
+ 'signatory_title': settings.BONAFIDE_SIGNATORY_TITLE,
+ 'reference_preview': reference_preview,
+ 'issued_on': now.strftime('%d.%m.%Y'),
+ 'institute_name': settings.BONAFIDE_INSTITUTE_NAME,
+ }
+
+ return Response({
+ 'student': context,
+ 'certificate': certificate_meta,
+ })
+
+
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+@authentication_classes([TokenAuthentication])
+@role_required(['acadadmin'])
+def generate_pointer_conversion_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_pointer_conversion_context(student)
+ if not context['is_ready']:
+ return Response({
+ 'error': 'Cannot issue certificate for student with incomplete data.',
+ 'details': context['validation_errors'],
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ cpi_text = request.data.get('cpi_text')
+ if not cpi_text:
+ cpi_text = context['cpi_display'] or '……………'
+ cpi_text = str(cpi_text).strip()
+
+ min_cpi = request.data.get('min_cpi')
+ if not min_cpi:
+ min_cpi = context['min_cpi']
+ min_cpi = str(min_cpi).strip()
+
+ issued_on = timezone.now().date()
+ prefix = settings.BONAFIDE_REFERENCE_PREFIX.rstrip('/')
+ roll_number = student.id.user.username
+ reference_number = f'{prefix}/{issued_on.year}/{issued_on.month:02d}/{roll_number}'
+
+ with transaction.atomic():
+ certificate = BonafideCertificate.objects.create(
+ student=student,
+ purpose='Pointer Conversion',
+ custom_purpose='',
+ certificate_type=CERTIFICATE_TYPE,
+ reference_number=reference_number,
+ issued_by=request.user,
+ fee_rows={'cpi': cpi_text, 'min_cpi': min_cpi},
+ )
+ pdf_bytes = render_pointer_conversion_pdf(
+ context, cpi_text, min_cpi, reference_number, issued_on
+ ).getvalue()
+ certificate.pdf_content = pdf_bytes
+ certificate.save(update_fields=['pdf_content'])
+
+ filename = f'{roll_number}_Pointer_Conversion_Certificate.pdf'
+ response = HttpResponse(pdf_bytes, content_type='application/pdf')
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
+ response['X-Certificate-Reference'] = reference_number
+ return response
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+@authentication_classes([TokenAuthentication])
+@role_required(['acadadmin'])
+def pointer_conversion_certificates(request):
+ """List previously issued pointer conversion certificates."""
+ page_number = int(request.query_params.get('page', 1))
+ page_size = min(int(request.query_params.get('page_size', 20)), 100)
+ search = (request.query_params.get('search') or '').strip()
+
+ queryset = BonafideCertificate.objects.filter(
+ certificate_type=CERTIFICATE_TYPE
+ ).select_related('student__id__user', 'issued_by')
+
+ queryset = _search_certificates(queryset, search)
+ paginator = Paginator(queryset, page_size)
+ page = paginator.get_page(page_number)
+
+ results = []
+ for cert in page.object_list:
+ student = cert.student
+ results.append({
+ 'id': cert.pk,
+ 'reference_number': cert.reference_number or f'CERT-{cert.pk:04d}',
+ 'roll_number': student.id.user.username,
+ 'student_name': _student_name(student),
+ 'issued_on': cert.issued_at.strftime('%d.%m.%Y'),
+ 'issued_by': cert.issued_by.get_full_name() or cert.issued_by.username,
+ 'purpose': cert.purpose,
+ })
+
+ return Response({
+ 'count': paginator.count,
+ 'page': page.number,
+ 'page_size': page_size,
+ 'total_pages': paginator.num_pages,
+ 'results': results,
+ })
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+@authentication_classes([TokenAuthentication])
+@role_required(['acadadmin'])
+def pointer_conversion_certificate_pdf(request, certificate_id):
+ cert = get_object_or_404(
+ BonafideCertificate.objects.select_related('student__id__user'),
+ pk=certificate_id, certificate_type=CERTIFICATE_TYPE,
+ )
+ if not cert.pdf_content:
+ context = build_pointer_conversion_context(cert.student)
+ cpi_data = cert.fee_rows or {}
+ cpi_text = cpi_data.get('cpi') if isinstance(cpi_data, dict) else context['cpi_display']
+ min_cpi = cpi_data.get('min_cpi') if isinstance(cpi_data, dict) else context['min_cpi']
+ pdf_bytes = render_pointer_conversion_pdf(
+ context, cpi_text, min_cpi, cert.reference_number, cert.issued_at.date()
+ ).getvalue()
+ cert.pdf_content = pdf_bytes
+ cert.save(update_fields=['pdf_content'])
+ else:
+ pdf_bytes = bytes(cert.pdf_content)
+
+ filename = f'{cert.student.id.user.username}_Pointer_Conversion_Certificate.pdf'
+ response = HttpResponse(pdf_bytes, content_type='application/pdf')
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
+ response['X-Certificate-Reference'] = cert.reference_number or f'CERT-{cert.pk:04d}'
+ return response
diff --git a/FusionIIIT/applications/academic_procedures/api/urls.py b/FusionIIIT/applications/academic_procedures/api/urls.py
index 8f808b2f8..146be6081 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,
+ pointer_conversion_certificate)
from .. import views as procedures_views
@@ -50,6 +51,18 @@
url(r'^acad/fee-structure-certificate/certificates/(?P\d+)/pdf/$',
fee_structure_certificate.fee_structure_certificate_pdf,
name='fee-structure-certificate-pdf-detail'),
+ url(r'^acad/pointer-conversion/student/$',
+ pointer_conversion_certificate.pointer_conversion_student,
+ name='pointer-conversion-student'),
+ url(r'^acad/pointer-conversion/pdf/$',
+ pointer_conversion_certificate.generate_pointer_conversion_pdf,
+ name='pointer-conversion-pdf'),
+ url(r'^acad/pointer-conversion/certificates/$',
+ pointer_conversion_certificate.pointer_conversion_certificates,
+ name='pointer-conversion-certificates'),
+ url(r'^acad/pointer-conversion/certificates/(?P\d+)/pdf/$',
+ pointer_conversion_certificate.pointer_conversion_certificate_pdf,
+ name='pointer-conversion-certificate-pdf-detail'),
url(r'^acad/fee-structures/$', fee_structure.fee_structures,
name='fee-structures'),
url(r'^acad/fee-structures/template/$',
diff --git a/FusionIIIT/applications/academic_procedures/migrations/0061_pointer_conversion_certificate_type.py b/FusionIIIT/applications/academic_procedures/migrations/0061_pointer_conversion_certificate_type.py
new file mode 100644
index 000000000..914567429
--- /dev/null
+++ b/FusionIIIT/applications/academic_procedures/migrations/0061_pointer_conversion_certificate_type.py
@@ -0,0 +1,27 @@
+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'),
+ ('pointer_conv', 'Pointer Conversion Certificate'),
+ ],
+ db_index=True,
+ default='bonafide',
+ max_length=16,
+ ),
+ ),
+ ]
diff --git a/FusionIIIT/applications/academic_procedures/models.py b/FusionIIIT/applications/academic_procedures/models.py
index 68d1f1e7c..1e2dc0b10 100644
--- a/FusionIIIT/applications/academic_procedures/models.py
+++ b/FusionIIIT/applications/academic_procedures/models.py
@@ -387,6 +387,7 @@ class BonafideCertificate(models.Model):
('fee', 'Paid / Unpaid Fee Certificate'),
('demand', 'Demand Letter'),
('fee_structure', 'Fee Structure Certificate'),
+ ('pointer_conv', 'Pointer Conversion Certificate'),
)
certificate_type = models.CharField(
diff --git a/FusionIIIT/applications/academic_procedures/test_pointer_conversion_certificate.py b/FusionIIIT/applications/academic_procedures/test_pointer_conversion_certificate.py
new file mode 100644
index 000000000..711ff9c77
--- /dev/null
+++ b/FusionIIIT/applications/academic_procedures/test_pointer_conversion_certificate.py
@@ -0,0 +1,121 @@
+from django.contrib.auth.models import User
+from django.test import TestCase
+from rest_framework.authtoken.models import Token
+from rest_framework.test import APIClient
+
+from applications.academic_information.models import Student
+from applications.academic_procedures.api import pointer_conversion_certificate as pcc
+from applications.academic_procedures.models import BonafideCertificate
+from applications.globals.models import Designation, ExtraInfo, HoldsDesignation
+from applications.programme_curriculum.models import Batch, Curriculum, Discipline, Programme
+
+
+class PointerConversionCertificateTests(TestCase):
+ def setUp(self):
+ self.programme = Programme.objects.create(
+ category='UG', name='Bachelor of Technology', programme_begin_year=2026)
+ self.discipline = Discipline.objects.create(
+ name='Computer Science and Engineering', acronym='CSE')
+ self.curriculum = Curriculum.objects.create(
+ programme=self.programme,
+ name='B.Tech CSE 2026',
+ no_of_semester=8,
+ )
+ self.batch = Batch.objects.create(
+ name='B.Tech',
+ discipline=self.discipline,
+ year=2026,
+ curriculum=self.curriculum,
+ )
+
+ self.student_user = User.objects.create_user(
+ username='21BCS032',
+ first_name='Arpan',
+ last_name='Karjee',
+ )
+ self.student_extra = ExtraInfo.objects.create(
+ id='21BCS032',
+ user=self.student_user,
+ user_type='student',
+ sex='M',
+ user_status='PRESENT',
+ )
+ self.student = Student.objects.create(
+ id=self.student_extra,
+ programme='B.Tech',
+ batch=2026,
+ batch_id=self.batch,
+ category='GEN',
+ father_name='Mr. Narayan Karjee',
+ curr_semester_no=6,
+ cpi=7.3,
+ )
+
+ self.admin = User.objects.create_user(username='certificate-admin')
+ designation = Designation.objects.create(name='acadadmin')
+ HoldsDesignation.objects.create(
+ user=self.admin, working=self.admin, designation=designation)
+ self.token = Token.objects.create(user=self.admin)
+ self.client = APIClient()
+ self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
+
+ def test_student_context(self):
+ context = pcc.build_pointer_conversion_context(self.student)
+ self.assertTrue(context['is_ready'])
+ self.assertEqual(context['name'], 'Arpan Karjee')
+ self.assertEqual(context['roll_number'], '21BCS032')
+ self.assertEqual(context['salutation'], 'Mr.')
+ self.assertEqual(context['cpi_display'], '7.3')
+ self.assertEqual(context['min_cpi'], '5.0')
+ self.assertEqual(context['programme'], 'Bachelor of Technology')
+ self.assertEqual(context['programme_abbr'], 'B.Tech')
+
+ def test_student_api(self):
+ response = self.client.get(
+ '/academic-procedures/api/acad/pointer-conversion/student/',
+ {'roll_number': '21BCS032'}
+ )
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(data['student']['name'], 'Arpan Karjee')
+ self.assertEqual(data['student']['cpi_display'], '7.3')
+ self.assertIn('reference_preview', data['certificate'])
+
+ def test_generate_pdf_api(self):
+ response = self.client.post(
+ '/academic-procedures/api/acad/pointer-conversion/pdf/',
+ {
+ 'student_id': self.student.pk,
+ 'cpi_text': '7.3',
+ 'min_cpi': '5.0',
+ }
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['Content-Type'], 'application/pdf')
+ self.assertIn('21BCS032_Pointer_Conversion_Certificate.pdf', response['Content-Disposition'])
+
+ cert = BonafideCertificate.objects.filter(
+ certificate_type='pointer_conv', student=self.student
+ ).first()
+ self.assertIsNotNone(cert)
+ self.assertEqual(cert.fee_rows.get('cpi'), '7.3')
+
+ def test_list_and_history_pdf_api(self):
+ # Generate one first
+ self.client.post(
+ '/academic-procedures/api/acad/pointer-conversion/pdf/',
+ {'student_id': self.student.pk}
+ )
+ # Check list
+ response = self.client.get('/academic-procedures/api/acad/pointer-conversion/certificates/')
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(data['count'], 1)
+ cert_id = data['results'][0]['id']
+
+ # Check detail PDF
+ detail_response = self.client.get(
+ f'/academic-procedures/api/acad/pointer-conversion/certificates/{cert_id}/pdf/'
+ )
+ self.assertEqual(detail_response.status_code, 200)
+ self.assertEqual(detail_response['Content-Type'], 'application/pdf')