From 014fc8e7ba2f570e4384143413ac515fd1db2e73 Mon Sep 17 00:00:00 2001 From: Saumitra-agrahari Date: Sun, 19 Apr 2026 21:28:27 +0530 Subject: [PATCH 1/2] Changes --- FusionIIIT/Fusion/settings/common.py | 1 + FusionIIIT/Fusion/urls.py | 1 + FusionIIIT/applications/patent_system/apps.py | 2 +- .../patent_system/management/__init__.py | 1 + .../management/commands/__init__.py | 1 + .../commands/seed_patent_test_data_v2.py | 253 +++ .../commands/seed_saumitra_test_data.py | 51 + .../0002_pcc_admin_and_communication_log.py | 13 + .../migrations/0003_auto_20260418_1253.py | 176 ++ .../migrations/0004_auto_20260418_1519.py | 343 +++ ...ion_attorney_review_fields_and_statuses.py | 51 + .../patent_system/migrations/__init__.py | 0 .../applications/patent_system/models.py | 242 ++- .../applications/patent_system/serializers.py | 135 +- .../patent_system/tests/__init__.py | 0 .../tests/role_auth_flow_smoke.py | 164 ++ .../patent_system/tests/runtime_smoke.py | 146 ++ .../patent_system/tests/seed_role_accounts.py | 65 + .../patent_system/tests/test_workflow.py | 263 +++ .../patent_system/tests/uc_api_smoke.py | 342 +++ FusionIIIT/applications/patent_system/urls.py | 42 + .../applications/patent_system/views.py | 1925 +++++++++++++++-- FusionIIIT/assign_roles.py | 79 + docker-compose.yml | 2 +- tr patent | 314 +++ 25 files changed, 4385 insertions(+), 227 deletions(-) create mode 100644 FusionIIIT/applications/patent_system/management/__init__.py create mode 100644 FusionIIIT/applications/patent_system/management/commands/__init__.py create mode 100644 FusionIIIT/applications/patent_system/management/commands/seed_patent_test_data_v2.py create mode 100644 FusionIIIT/applications/patent_system/management/commands/seed_saumitra_test_data.py create mode 100644 FusionIIIT/applications/patent_system/migrations/0002_pcc_admin_and_communication_log.py create mode 100644 FusionIIIT/applications/patent_system/migrations/0003_auto_20260418_1253.py create mode 100644 FusionIIIT/applications/patent_system/migrations/0004_auto_20260418_1519.py create mode 100644 FusionIIIT/applications/patent_system/migrations/0005_application_attorney_review_fields_and_statuses.py create mode 100644 FusionIIIT/applications/patent_system/migrations/__init__.py create mode 100644 FusionIIIT/applications/patent_system/tests/__init__.py create mode 100644 FusionIIIT/applications/patent_system/tests/role_auth_flow_smoke.py create mode 100644 FusionIIIT/applications/patent_system/tests/runtime_smoke.py create mode 100644 FusionIIIT/applications/patent_system/tests/seed_role_accounts.py create mode 100644 FusionIIIT/applications/patent_system/tests/test_workflow.py create mode 100644 FusionIIIT/applications/patent_system/tests/uc_api_smoke.py create mode 100644 FusionIIIT/assign_roles.py create mode 100644 tr patent diff --git a/FusionIIIT/Fusion/settings/common.py b/FusionIIIT/Fusion/settings/common.py index bc97f1548..bdd090092 100644 --- a/FusionIIIT/Fusion/settings/common.py +++ b/FusionIIIT/Fusion/settings/common.py @@ -141,6 +141,7 @@ 'applications.hr2', 'applications.department', 'applications.iwdModuleV2', + 'applications.patent_system.apps.PatentsystemConfig', 'allauth', 'allauth.account', 'allauth.socialaccount', diff --git a/FusionIIIT/Fusion/urls.py b/FusionIIIT/Fusion/urls.py index e3b3f6792..374a58008 100755 --- a/FusionIIIT/Fusion/urls.py +++ b/FusionIIIT/Fusion/urls.py @@ -65,6 +65,7 @@ url(r'^recruitment/', include('applications.recruitment.urls')), url(r'^examination/', include('applications.examination.urls')), url(r'^otheracademic/', include('applications.otheracademic.urls')), + url(r'^patentsystem/', include('applications.patent_system.urls')), path( 'password-reset/', diff --git a/FusionIIIT/applications/patent_system/apps.py b/FusionIIIT/applications/patent_system/apps.py index 1d4241a17..ec98d4eb0 100644 --- a/FusionIIIT/applications/patent_system/apps.py +++ b/FusionIIIT/applications/patent_system/apps.py @@ -1,4 +1,4 @@ from django.apps import AppConfig class PatentsystemConfig(AppConfig): - name = 'patent_system' + name = 'applications.patent_system' diff --git a/FusionIIIT/applications/patent_system/management/__init__.py b/FusionIIIT/applications/patent_system/management/__init__.py new file mode 100644 index 000000000..0fddefdb0 --- /dev/null +++ b/FusionIIIT/applications/patent_system/management/__init__.py @@ -0,0 +1 @@ +"""Patent management package for Django management commands.""" diff --git a/FusionIIIT/applications/patent_system/management/commands/__init__.py b/FusionIIIT/applications/patent_system/management/commands/__init__.py new file mode 100644 index 000000000..ef3c2b477 --- /dev/null +++ b/FusionIIIT/applications/patent_system/management/commands/__init__.py @@ -0,0 +1 @@ +"""Patent management command package.""" diff --git a/FusionIIIT/applications/patent_system/management/commands/seed_patent_test_data_v2.py b/FusionIIIT/applications/patent_system/management/commands/seed_patent_test_data_v2.py new file mode 100644 index 000000000..98d1d4d10 --- /dev/null +++ b/FusionIIIT/applications/patent_system/management/commands/seed_patent_test_data_v2.py @@ -0,0 +1,253 @@ +from datetime import date, timedelta + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from applications.globals.models import Designation, DepartmentInfo, ExtraInfo, HoldsDesignation +from applications.patent_system.models import Applicant, Application, Attorney + + +class Command(BaseCommand): + help = "Seed patent test data for 23BCS226 and four attorneys." + + def add_arguments(self, parser): + parser.add_argument( + "--reset", + action="store_true", + help="Delete existing seeded patent test data before creating new records.", + ) + + def handle(self, *args, **options): + reset = options["reset"] + student_user = self._ensure_student_user() + attorneys = self._ensure_attorneys() + + if reset: + self._delete_existing_seed_data(student_user, attorneys) + + applicant = self._ensure_applicant(student_user) + scenarios = self._seed_student_scenarios(applicant, attorneys) + + self.stdout.write(self.style.SUCCESS("Seeded 10 patent scenarios for 23BCS226.")) + self.stdout.write(self.style.SUCCESS(f"Seeded {len(attorneys)} attorneys.")) + for application in scenarios: + self.stdout.write( + f"- {application.id}: {application.title} | {application.status} | {application.attorney.name if application.attorney else 'No attorney'}" + ) + + def _ensure_student_user(self): + user, _ = User.objects.get_or_create( + username="23BCS226", + defaults={ + "email": "23bcs226@example.com", + "first_name": "Saumitra", + "last_name": "Sharma", + }, + ) + user.email = "23bcs226@example.com" + user.first_name = "Saumitra" + user.last_name = "Sharma" + user.set_password("Pass1234!") + user.save() + + dept, _ = DepartmentInfo.objects.get_or_create(name="CSE") + extra, _ = ExtraInfo.objects.get_or_create( + user=user, + defaults={ + "id": f"EX{user.id}", + "title": "Mr.", + "sex": "M", + "user_status": "PRESENT", + "address": "Campus", + "phone_no": 9999999999, + "user_type": "student", + "department": dept, + }, + ) + extra.user_type = "student" + extra.department = dept + extra.last_selected_role = "student" + extra.save() + + designation, _ = Designation.objects.get_or_create( + name="student", + defaults={"full_name": "student", "type": "academic"}, + ) + HoldsDesignation.objects.get_or_create(user=user, working=user, designation=designation) + return user + + def _ensure_applicant(self, user): + applicant, _ = Applicant.objects.get_or_create( + user=user, + defaults={ + "name": "23BCS226", + "email": "23bcs226@example.com", + "mobile": "9999999999", + "address": "Campus", + }, + ) + applicant.name = "23BCS226" + applicant.email = "23bcs226@example.com" + applicant.mobile = "9999999999" + applicant.address = "Campus" + applicant.save() + return applicant + + def _ensure_attorneys(self): + attorney_specs = [ + ("Attorney One", "attorney1@example.com", "9000000001", "LexBridge LLP", "Patent Search"), + ("Attorney Two", "attorney2@example.com", "9000000002", "Nova Legal", "Drafting"), + ("Attorney Three", "attorney3@example.com", "9000000003", "IP Shield", "Office Actions"), + ("Attorney Four", "attorney4@example.com", "9000000004", "Crown IP", "Appeals"), + ] + attorneys = [] + for name, email, phone, firm, expertise in attorney_specs: + attorney, _ = Attorney.objects.get_or_create( + email=email, + defaults={ + "name": name, + "phone": phone, + "firm_name": firm, + "expertise_domain": expertise, + "is_panel_approved": True, + "current_workload": 0, + }, + ) + attorney.name = name + attorney.phone = phone + attorney.firm_name = firm + attorney.expertise_domain = expertise + attorney.is_panel_approved = True + attorney.save() + attorneys.append(attorney) + return attorneys + + def _delete_existing_seed_data(self, user, attorneys): + Application.objects.filter(primary_applicant__user=user).delete() + Attorney.objects.filter(email__in=[attorney.email for attorney in attorneys]).delete() + + def _seed_student_scenarios(self, applicant, attorneys): + scenarios = [ + { + "title": "Scenario 01 - Submitted", + "status": "Submitted", + "decision_status": "Pending", + "attorney": None, + "comments": "Fresh submission awaiting PCC review.", + }, + { + "title": "Scenario 02 - PCC Reviewed", + "status": "Reviewed by PCC Admin", + "decision_status": "Pending", + "attorney": None, + "comments": "Reviewed by PCC Admin and ready to forward.", + }, + { + "title": "Scenario 03 - Forwarded", + "status": "Forwarded for Director's Review", + "decision_status": "Pending", + "attorney": attorneys[0], + "comments": "Forwarded to Director with attorney assignment.", + }, + { + "title": "Scenario 04 - Director Approved", + "status": "Attorney Assigned", + "decision_status": "Pending", + "attorney": attorneys[1], + "comments": "Director approved and attorney review pending.", + }, + { + "title": "Scenario 05 - Attorney Returned", + "status": "Returned to Director", + "decision_status": "Reviewed by Attorney", + "attorney": attorneys[2], + "comments": "Attorney completed patentability check and returned to Director.", + "attorney_review_notes": "Prior-art analysis complete.", + }, + { + "title": "Scenario 06 - Needs Revision", + "status": "Needs Revision", + "decision_status": "Needs Revision", + "attorney": attorneys[3], + "comments": "PCC requested mandatory revision comments.", + "revision_requested_at": date.today() - timedelta(days=2), + "revision_due_date": date.today() + timedelta(days=28), + "is_revision_locked": True, + }, + { + "title": "Scenario 07 - Revision Expired", + "status": "Revision Expired", + "decision_status": "Pending", + "attorney": None, + "comments": "Revision deadline has already expired.", + "revision_requested_at": date.today() - timedelta(days=90), + "revision_due_date": date.today() - timedelta(days=30), + "is_revision_locked": True, + }, + { + "title": "Scenario 08 - Withdrawn", + "status": "Withdrawn", + "decision_status": "Rejected", + "attorney": None, + "comments": "Applicant withdrew the application.", + }, + { + "title": "Scenario 09 - Patent Filed", + "status": "Patent Filed", + "decision_status": "Pending", + "attorney": attorneys[0], + "comments": "Filed after attorney review.", + "token_no": "IIITDMJ/CSE/2026-04-19/000009/PAT/109", + "patent_filed_date": date.today() - timedelta(days=3), + }, + { + "title": "Scenario 10 - Patent Refused", + "status": "Patent Refused", + "decision_status": "Rejected", + "attorney": attorneys[1], + "comments": "Final refusal after director review.", + "token_no": "IIITDMJ/CSE/2026-04-19/000010/PAT/110", + }, + ] + + created = [] + for index, scenario in enumerate(scenarios, start=1): + defaults = { + "title": scenario["title"], + "status": scenario["status"], + "decision_status": scenario["decision_status"], + "submitted_date": date.today() - timedelta(days=30 - index), + "comments": scenario.get("comments", ""), + "attorney": scenario.get("attorney"), + "is_revision_locked": scenario.get("is_revision_locked", False), + "revision_requested_at": scenario.get("revision_requested_at"), + "revision_due_date": scenario.get("revision_due_date"), + "revised_submitted_at": scenario.get("revised_submitted_at"), + "attorney_review_notes": scenario.get("attorney_review_notes"), + "patent_filed_date": scenario.get("patent_filed_date"), + } + + application, _ = Application.objects.get_or_create( + primary_applicant=applicant, + title=scenario["title"], + defaults=defaults, + ) + + for field_name, field_value in defaults.items(): + setattr(application, field_name, field_value) + + application.token_no = scenario.get("token_no") + application.forwarded_to_director_date = application.submitted_date + timedelta(days=2) + application.reviewed_by_pcc_date = application.submitted_date + timedelta(days=1) + application.director_approval_date = ( + application.submitted_date + timedelta(days=3) + if scenario["status"] in ["Attorney Assigned", "Returned to Director", "Patent Filed", "Patent Refused"] + else None + ) + application.decision_date = application.submitted_date + timedelta(days=4) if scenario["status"] == "Patent Refused" else None + if scenario["status"] == "Attorney Assigned" and not application.attorney: + application.attorney = attorneys[0] + application.save() + created.append(application) + + return created diff --git a/FusionIIIT/applications/patent_system/management/commands/seed_saumitra_test_data.py b/FusionIIIT/applications/patent_system/management/commands/seed_saumitra_test_data.py new file mode 100644 index 000000000..49c750729 --- /dev/null +++ b/FusionIIIT/applications/patent_system/management/commands/seed_saumitra_test_data.py @@ -0,0 +1,51 @@ +from django.core.management.base import BaseCommand + +from applications.patent_system.models import Attorney + + +class Command(BaseCommand): + help = "Seed additional attorney records for patent workflow testing." + + def add_arguments(self, parser): + parser.add_argument( + "--count", + type=int, + default=4, + help="Number of attorney records to create or update. Defaults to 4.", + ) + + def handle(self, *args, **options): + count = max(1, min(options["count"], 4)) + attorney_specs = [ + ("Attorney Alpha", "alpha.attorney@example.com", "9000000101", "Alpha IP Law", "Patentability Review"), + ("Attorney Beta", "beta.attorney@example.com", "9000000102", "Beta Legal", "Prior Art Search"), + ("Attorney Gamma", "gamma.attorney@example.com", "9000000103", "Gamma Counsel", "Drafting and Filing"), + ("Attorney Delta", "delta.attorney@example.com", "9000000104", "Delta IP Partners", "Appeals and Responses"), + ] + + created_or_updated = [] + for name, email, phone, firm_name, expertise_domain in attorney_specs[:count]: + attorney, created = Attorney.objects.get_or_create( + email=email, + defaults={ + "name": name, + "phone": phone, + "firm_name": firm_name, + "expertise_domain": expertise_domain, + "is_panel_approved": True, + "current_workload": 0, + }, + ) + attorney.name = name + attorney.phone = phone + attorney.firm_name = firm_name + attorney.expertise_domain = expertise_domain + attorney.is_panel_approved = True + attorney.save() + created_or_updated.append((attorney, created)) + + for attorney, created in created_or_updated: + state = "created" if created else "updated" + self.stdout.write(f"{attorney.name} ({attorney.email}) {state}") + + self.stdout.write(self.style.SUCCESS(f"Processed {len(created_or_updated)} attorney records.")) diff --git a/FusionIIIT/applications/patent_system/migrations/0002_pcc_admin_and_communication_log.py b/FusionIIIT/applications/patent_system/migrations/0002_pcc_admin_and_communication_log.py new file mode 100644 index 000000000..17810286c --- /dev/null +++ b/FusionIIIT/applications/patent_system/migrations/0002_pcc_admin_and_communication_log.py @@ -0,0 +1,13 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('patent_system', '0001_initial'), + ] + + operations = [ + # The app's historical 0001 migration contains no model state. + # Keep this migration as a no-op and create the full schema in a later migration. + ] diff --git a/FusionIIIT/applications/patent_system/migrations/0003_auto_20260418_1253.py b/FusionIIIT/applications/patent_system/migrations/0003_auto_20260418_1253.py new file mode 100644 index 000000000..ad1e72766 --- /dev/null +++ b/FusionIIIT/applications/patent_system/migrations/0003_auto_20260418_1253.py @@ -0,0 +1,176 @@ +# Generated by Django 3.1.5 on 2026-04-18 12:53 + +import applications.patent_system.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('patent_system', '0002_pcc_admin_and_communication_log'), + ] + + operations = [ + migrations.CreateModel( + name='Applicant', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254, unique=True)), + ('mobile', models.CharField(max_length=15)), + ('address', models.CharField(max_length=255)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='applicant', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_applicant', + }, + ), + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('last_updated_at', models.DateTimeField(auto_now=True)), + ('token_no', models.CharField(blank=True, max_length=100, null=True)), + ('title', models.CharField(max_length=255)), + ('status', models.CharField(choices=[('Draft', 'Draft'), ('Submitted', 'Submitted'), ('Reviewed by PCC Admin', 'Reviewed by PCC Admin'), ("Forwarded for Director's Review", "Forwarded for Director's Review"), ("Director's Approval Received", "Director's Approval Received"), ('Patentability Check Started', 'Patentability Check Started'), ('Patentability Check Completed', 'Patentability Check Completed'), ('Patentability Search Report Generated', 'Patentability Search Report Generated'), ('Patent Filed', 'Patent Filed'), ('Patent Published', 'Patent Published'), ('Patent Granted', 'Patent Granted'), ('Patent Refused', 'Patent Refused')], default='Draft', max_length=50)), + ('submitted_date', models.DateField(blank=True, null=True)), + ('reviewed_by_pcc_date', models.DateField(blank=True, null=True)), + ('forwarded_to_director_date', models.DateField(blank=True, null=True)), + ('director_approval_date', models.DateField(blank=True, null=True)), + ('patentability_check_start_date', models.DateField(blank=True, null=True)), + ('patentability_check_completed_date', models.DateField(blank=True, null=True)), + ('search_report_generated_date', models.DateField(blank=True, null=True)), + ('patent_filed_date', models.DateField(blank=True, null=True)), + ('patent_published_date', models.DateField(blank=True, null=True)), + ('decision_date', models.DateField(blank=True, null=True)), + ('decision_status', models.CharField(choices=[('Approved', 'Approved'), ('Rejected', 'Rejected'), ('Pending', 'Pending')], default='Pending', max_length=50)), + ('comments', models.TextField(blank=True, null=True)), + ('assigned_pcc_admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patent_assigned_applications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_application', + }, + ), + migrations.CreateModel( + name='Attorney', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254, unique=True)), + ('phone', models.CharField(max_length=15)), + ('firm_name', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'db_table': 'patent_system_attorney', + }, + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('link', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='CommunicationLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_attorney_name', models.CharField(blank=True, max_length=255, null=True)), + ('external_attorney_email', models.EmailField(blank=True, max_length=254, null=True)), + ('message_content', models.TextField()), + ('status_or_notes', models.CharField(blank=True, max_length=255, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='communication_logs', to='patent_system.application')), + ('logged_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patent_communication_logs', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_communication_log', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='AssociatedWith', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('percentage_share', models.DecimalField(decimal_places=2, max_digits=5)), + ('applicant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patent_system.applicant')), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_associatedwith', + }, + ), + migrations.CreateModel( + name='ApplicationSectionIII', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('company_name', models.CharField(max_length=255)), + ('contact_person', models.CharField(max_length=255)), + ('contact_no', models.CharField(max_length=15)), + ('development_stage', models.CharField(choices=[('Embryonic', 'Embryonic'), ('Partially developed', 'Partially developed'), ('Off-the-shelf', 'Off-the-shelf')], max_length=30)), + ('form_iii', models.FileField(upload_to=applications.patent_system.models.generate_form_iii_file_path)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='section_iii', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_application_section_iii', + }, + ), + migrations.CreateModel( + name='ApplicationSectionII', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('funding_details', models.TextField()), + ('funding_source', models.TextField()), + ('source_agreement', models.FileField(blank=True, null=True, upload_to=applications.patent_system.models.generate_source_agreement_file_path)), + ('publication_details', models.TextField()), + ('mou_details', models.TextField()), + ('mou_file', models.FileField(blank=True, null=True, upload_to=applications.patent_system.models.generate_mou_file_path)), + ('research_details', models.TextField()), + ('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='section_ii', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_application_section_ii', + }, + ), + migrations.CreateModel( + name='ApplicationSectionI', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type_of_ip', models.CharField(choices=[('Patent', 'Patent'), ('Copyright', 'Copyright'), ('Trademark', 'Trademark'), ('Industrial Design', 'Industrial Design'), ('Trade Secret', 'Trade Secret'), ('Geographical Indication', 'Geographical Indication')], default='Patent', max_length=255)), + ('area', models.TextField()), + ('problem', models.TextField()), + ('objective', models.TextField()), + ('novelty', models.TextField()), + ('advantages', models.TextField()), + ('is_tested', models.BooleanField(default=False)), + ('poc_details', models.FileField(blank=True, null=True, upload_to=applications.patent_system.models.poc_file_upload_path)), + ('applications', models.TextField()), + ('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='section_i', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_application_section_i', + }, + ), + migrations.AddField( + model_name='application', + name='attorney', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='patent_system.attorney'), + ), + migrations.AddField( + model_name='application', + name='primary_applicant', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='patent_system.applicant'), + ), + ] diff --git a/FusionIIIT/applications/patent_system/migrations/0004_auto_20260418_1519.py b/FusionIIIT/applications/patent_system/migrations/0004_auto_20260418_1519.py new file mode 100644 index 000000000..64dc73f65 --- /dev/null +++ b/FusionIIIT/applications/patent_system/migrations/0004_auto_20260418_1519.py @@ -0,0 +1,343 @@ +# Generated by Django 3.1.5 on 2026-04-18 15:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('patent_system', '0003_auto_20260418_1253'), + ] + + operations = [ + migrations.CreateModel( + name='OfficeAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('office_name', models.CharField(max_length=120)), + ('action_reference', models.CharField(max_length=120)), + ('action_summary', models.TextField()), + ('due_date', models.DateField(blank=True, null=True)), + ('status', models.CharField(default='Open', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'patent_system_office_action', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='application', + name='budget_estimate', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True), + ), + migrations.AddField( + model_name='application', + name='budget_status', + field=models.CharField(default='Not Initiated', max_length=50), + ), + migrations.AddField( + model_name='application', + name='external_filing_status', + field=models.CharField(default='Not Initiated', max_length=50), + ), + migrations.AddField( + model_name='application', + name='is_revision_locked', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='application', + name='maintenance_tracking_active', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='application', + name='priority_score', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='application', + name='revised_submitted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='application', + name='revision_due_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='application', + name='revision_requested_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='attorney', + name='current_workload', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='attorney', + name='expertise_domain', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='attorney', + name='is_panel_approved', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='document', + name='application', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='patent_system.application'), + ), + migrations.AddField( + model_name='document', + name='current_version', + field=models.PositiveIntegerField(default=1), + ), + migrations.AddField( + model_name='document', + name='is_locked', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='application', + name='status', + field=models.CharField(choices=[('Draft', 'Draft'), ('Submitted', 'Submitted'), ('Needs Revision', 'Needs Revision'), ('Revision Expired', 'Revision Expired'), ('Withdrawn', 'Withdrawn'), ('Reviewed by PCC Admin', 'Reviewed by PCC Admin'), ("Forwarded for Director's Review", "Forwarded for Director's Review"), ("Director's Approval Received", "Director's Approval Received"), ('Patentability Check Started', 'Patentability Check Started'), ('Patentability Check Completed', 'Patentability Check Completed'), ('Patentability Search Report Generated', 'Patentability Search Report Generated'), ('Patent Filed', 'Patent Filed'), ('Patent Published', 'Patent Published'), ('Patent Granted', 'Patent Granted'), ('Patent Refused', 'Patent Refused')], default='Draft', max_length=50), + ), + migrations.CreateModel( + name='PriorArtReference', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference_type', models.CharField(max_length=80)), + ('citation', models.CharField(max_length=255)), + ('notes', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prior_art_references', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_prior_art_reference', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='OfficeActionResponse', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('response_text', models.TextField()), + ('response_reference', models.CharField(blank=True, max_length=120, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('office_action', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='patent_system.officeaction')), + ('responder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_office_action_response', + 'ordering': ['-created_at'], + }, + ), + migrations.AddField( + model_name='officeaction', + name='application', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='office_actions', to='patent_system.application'), + ), + migrations.CreateModel( + name='NotificationEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('recipient_role', models.CharField(blank=True, max_length=50, null=True)), + ('event_type', models.CharField(default='General', max_length=30)), + ('message', models.TextField()), + ('due_date', models.DateField(blank=True, null=True)), + ('is_escalated', models.BooleanField(default=False)), + ('is_read', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='patent_system.application')), + ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='patent_notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_notification_event', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='MaintenanceSchedule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('due_date', models.DateField()), + ('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('status', models.CharField(default='Upcoming', max_length=20)), + ('reminder_sent_at', models.DateTimeField(blank=True, null=True)), + ('paid_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_schedules', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_maintenance_schedule', + 'ordering': ['due_date'], + }, + ), + migrations.CreateModel( + name='LicensingRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('requester_name', models.CharField(max_length=120)), + ('requester_org', models.CharField(max_length=120)), + ('request_details', models.TextField()), + ('status', models.CharField(default='Pending', max_length=30)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='licensing_requests', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_licensing_request', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='LegalAssessment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('opinion', models.CharField(max_length=30)), + ('prior_art_summary', models.TextField()), + ('recommended_action', models.TextField()), + ('comments', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='legal_assessments', to='patent_system.application')), + ('attorney', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='legal_assessments', to='patent_system.attorney')), + ], + options={ + 'db_table': 'patent_system_legal_assessment', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='LegalAdviceMemo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('summary', models.TextField()), + ('recommendation', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='legal_memos', to='patent_system.application')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_legal_advice_memo', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='InventorConsent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('consent_given', models.BooleanField(default=False)), + ('agreement_reference', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('applicant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consents', to='patent_system.applicant')), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventor_consents', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_inventor_consent', + }, + ), + migrations.CreateModel( + name='ExternalFilingRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('patent_office', models.CharField(max_length=120)), + ('filing_reference', models.CharField(max_length=120)), + ('communication_notes', models.TextField(blank=True, null=True)), + ('filing_date', models.DateField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='external_filings', to='patent_system.application')), + ('filed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_filing_records', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_external_filing_record', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='ConflictDeclaration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('conflict_type', models.CharField(max_length=120)), + ('declaration_status', models.CharField(default='Declared', max_length=20)), + ('notes', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conflict_declarations', to='patent_system.application')), + ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='patent_conflict_declarations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_conflict_declaration', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='BudgetApproval', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('threshold', models.DecimalField(decimal_places=2, max_digits=12)), + ('status', models.CharField(default='Pending', max_length=20)), + ('comments', models.TextField(blank=True, null=True)), + ('decided_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='budget_approvals', to='patent_system.application')), + ('decided_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='budget_decisions', to=settings.AUTH_USER_MODEL)), + ('requested_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='budget_requests', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_budget_approval', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='AuditLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=120)), + ('details', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_entries', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_audit_log', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='AppealRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('appellant', models.CharField(max_length=120)), + ('grounds', models.TextField()), + ('status', models.CharField(default='Open', max_length=30)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appeals', to='patent_system.application')), + ], + options={ + 'db_table': 'patent_system_appeal_request', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='DocumentVersion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version_number', models.PositiveIntegerField()), + ('link', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='patent_system.document')), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'patent_system_document_version', + 'ordering': ['-version_number'], + 'unique_together': {('document', 'version_number')}, + }, + ), + ] diff --git a/FusionIIIT/applications/patent_system/migrations/0005_application_attorney_review_fields_and_statuses.py b/FusionIIIT/applications/patent_system/migrations/0005_application_attorney_review_fields_and_statuses.py new file mode 100644 index 000000000..743773496 --- /dev/null +++ b/FusionIIIT/applications/patent_system/migrations/0005_application_attorney_review_fields_and_statuses.py @@ -0,0 +1,51 @@ +# Generated by Django 3.1.5 on 2026-04-18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('patent_system', '0004_auto_20260418_1519'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='attorney_review_notes', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='application', + name='attorney_reviewed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='application', + name='status', + field=models.CharField( + choices=[ + ('Draft', 'Draft'), + ('Submitted', 'Submitted'), + ('Needs Revision', 'Needs Revision'), + ('Revision Expired', 'Revision Expired'), + ('Withdrawn', 'Withdrawn'), + ('Reviewed by PCC Admin', 'Reviewed by PCC Admin'), + ("Forwarded for Director's Review", "Forwarded for Director's Review"), + ("Director's Approval Received", "Director's Approval Received"), + ('Attorney Assigned', 'Attorney Assigned'), + ('Attorney Reviewed', 'Attorney Reviewed'), + ('Returned to Director', 'Returned to Director'), + ('Patentability Check Started', 'Patentability Check Started'), + ('Patentability Check Completed', 'Patentability Check Completed'), + ('Patentability Search Report Generated', 'Patentability Search Report Generated'), + ('Patent Filed', 'Patent Filed'), + ('Patent Published', 'Patent Published'), + ('Patent Granted', 'Patent Granted'), + ('Patent Refused', 'Patent Refused'), + ], + default='Draft', + max_length=50, + ), + ), + ] diff --git a/FusionIIIT/applications/patent_system/migrations/__init__.py b/FusionIIIT/applications/patent_system/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/patent_system/models.py b/FusionIIIT/applications/patent_system/models.py index 60d85a0ee..ec5f7f695 100644 --- a/FusionIIIT/applications/patent_system/models.py +++ b/FusionIIIT/applications/patent_system/models.py @@ -23,6 +23,9 @@ class Attorney(models.Model): email = models.EmailField(unique=True) phone = models.CharField(max_length=15) firm_name = models.CharField(max_length=255, blank=True, null=True) + expertise_domain = models.CharField(max_length=255, blank=True, null=True) + is_panel_approved = models.BooleanField(default=True) + current_workload = models.PositiveIntegerField(default=0) def str(self): return self.name @@ -34,9 +37,15 @@ class Application(models.Model): STATUS_CHOICES = [ ("Draft", "Draft"), ("Submitted", "Submitted"), + ("Needs Revision", "Needs Revision"), + ("Revision Expired", "Revision Expired"), + ("Withdrawn", "Withdrawn"), ("Reviewed by PCC Admin", "Reviewed by PCC Admin"), ("Forwarded for Director's Review", "Forwarded for Director's Review"), ("Director's Approval Received", "Director's Approval Received"), + ("Attorney Assigned", "Attorney Assigned"), + ("Attorney Reviewed", "Attorney Reviewed"), + ("Returned to Director", "Returned to Director"), ("Patentability Check Started", "Patentability Check Started"), ("Patentability Check Completed", "Patentability Check Completed"), ("Patentability Search Report Generated", "Patentability Search Report Generated"), @@ -58,6 +67,7 @@ class Application(models.Model): title = models.CharField(max_length=255) status = models.CharField(max_length=50, choices=STATUS_CHOICES, default = "Draft") attorney = models.ForeignKey(Attorney, on_delete=models.CASCADE, related_name="applications", blank=True, null=True) + assigned_pcc_admin = models.ForeignKey(User, on_delete=models.SET_NULL, related_name="patent_assigned_applications", blank=True, null=True) submitted_date = models.DateField(blank=True, null=True) reviewed_by_pcc_date = models.DateField(blank=True, null=True) forwarded_to_director_date = models.DateField(blank=True, null=True) @@ -70,6 +80,17 @@ class Application(models.Model): decision_date = models.DateField(blank=True, null=True) decision_status = models.CharField(max_length=50, choices=DECISION_STATUS_CHOICES, default = "Pending") comments = models.TextField(blank=True, null=True) + attorney_review_notes = models.TextField(blank=True, null=True) + attorney_reviewed_at = models.DateTimeField(blank=True, null=True) + revision_requested_at = models.DateTimeField(blank=True, null=True) + revision_due_date = models.DateField(blank=True, null=True) + revised_submitted_at = models.DateTimeField(blank=True, null=True) + is_revision_locked = models.BooleanField(default=False) + budget_status = models.CharField(max_length=50, default="Not Initiated") + budget_estimate = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True) + external_filing_status = models.CharField(max_length=50, default="Not Initiated") + maintenance_tracking_active = models.BooleanField(default=False) + priority_score = models.IntegerField(default=0) def __str__(self): return self.title @@ -229,6 +250,9 @@ class Meta: class Document(models.Model): title = models.CharField(max_length=255) link = models.URLField() + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="documents", blank=True, null=True) + is_locked = models.BooleanField(default=False) + current_version = models.PositiveIntegerField(default=1) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -236,4 +260,220 @@ def __str__(self): return self.title class Meta: - ordering = ['-created_at'] \ No newline at end of file + ordering = ['-created_at'] + + +class CommunicationLog(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="communication_logs") + logged_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name="patent_communication_logs") + external_attorney_name = models.CharField(max_length=255, blank=True, null=True) + external_attorney_email = models.EmailField(blank=True, null=True) + message_content = models.TextField() + status_or_notes = models.CharField(max_length=255, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Communication for application {self.application_id}" + + class Meta: + db_table = 'patent_system_communication_log' + ordering = ['-created_at'] + + +class ConflictDeclaration(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="conflict_declarations") + reviewer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="patent_conflict_declarations") + conflict_type = models.CharField(max_length=120) + declaration_status = models.CharField(max_length=20, default="Declared") + notes = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_conflict_declaration" + ordering = ["-created_at"] + + +class LegalAssessment(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="legal_assessments") + attorney = models.ForeignKey(Attorney, on_delete=models.CASCADE, related_name="legal_assessments") + opinion = models.CharField(max_length=30) + prior_art_summary = models.TextField() + recommended_action = models.TextField() + comments = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_legal_assessment" + ordering = ["-created_at"] + + +class NotificationEvent(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="notifications", blank=True, null=True) + recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="patent_notifications", blank=True, null=True) + recipient_role = models.CharField(max_length=50, blank=True, null=True) + event_type = models.CharField(max_length=30, default="General") + message = models.TextField() + due_date = models.DateField(blank=True, null=True) + is_escalated = models.BooleanField(default=False) + is_read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_notification_event" + ordering = ["-created_at"] + + +class BudgetApproval(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="budget_approvals") + requested_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="budget_requests") + amount = models.DecimalField(max_digits=12, decimal_places=2) + threshold = models.DecimalField(max_digits=12, decimal_places=2) + status = models.CharField(max_length=20, default="Pending") + comments = models.TextField(blank=True, null=True) + decided_by = models.ForeignKey(User, on_delete=models.SET_NULL, related_name="budget_decisions", blank=True, null=True) + decided_at = models.DateTimeField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_budget_approval" + ordering = ["-created_at"] + + +class ExternalFilingRecord(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="external_filings") + patent_office = models.CharField(max_length=120) + filing_reference = models.CharField(max_length=120) + communication_notes = models.TextField(blank=True, null=True) + filed_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name="external_filing_records") + filing_date = models.DateField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_external_filing_record" + ordering = ["-created_at"] + + +class MaintenanceSchedule(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="maintenance_schedules") + due_date = models.DateField() + amount = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True) + status = models.CharField(max_length=20, default="Upcoming") + reminder_sent_at = models.DateTimeField(blank=True, null=True) + paid_at = models.DateTimeField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_maintenance_schedule" + ordering = ["due_date"] + + +class DocumentVersion(models.Model): + document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="versions") + version_number = models.PositiveIntegerField() + link = models.URLField() + uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_document_version" + ordering = ["-version_number"] + unique_together = ("document", "version_number") + + +class InventorConsent(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="inventor_consents") + applicant = models.ForeignKey(Applicant, on_delete=models.CASCADE, related_name="consents") + consent_given = models.BooleanField(default=False) + agreement_reference = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_inventor_consent" + + +class OfficeAction(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="office_actions") + office_name = models.CharField(max_length=120) + action_reference = models.CharField(max_length=120) + action_summary = models.TextField() + due_date = models.DateField(blank=True, null=True) + status = models.CharField(max_length=20, default="Open") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_office_action" + ordering = ["-created_at"] + + +class OfficeActionResponse(models.Model): + office_action = models.ForeignKey(OfficeAction, on_delete=models.CASCADE, related_name="responses") + responder = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) + response_text = models.TextField() + response_reference = models.CharField(max_length=120, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_office_action_response" + ordering = ["-created_at"] + + +class LicensingRequest(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="licensing_requests") + requester_name = models.CharField(max_length=120) + requester_org = models.CharField(max_length=120) + request_details = models.TextField() + status = models.CharField(max_length=30, default="Pending") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_licensing_request" + ordering = ["-created_at"] + + +class AppealRequest(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="appeals") + appellant = models.CharField(max_length=120) + grounds = models.TextField() + status = models.CharField(max_length=30, default="Open") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_appeal_request" + ordering = ["-created_at"] + + +class PriorArtReference(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="prior_art_references") + reference_type = models.CharField(max_length=80) + citation = models.CharField(max_length=255) + notes = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_prior_art_reference" + ordering = ["-created_at"] + + +class LegalAdviceMemo(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="legal_memos") + author = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) + summary = models.TextField() + recommendation = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_legal_advice_memo" + ordering = ["-created_at"] + + +class AuditLog(models.Model): + action = models.CharField(max_length=120) + actor = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) + application = models.ForeignKey(Application, on_delete=models.SET_NULL, blank=True, null=True, related_name="audit_entries") + details = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "patent_system_audit_log" + ordering = ["-created_at"] \ No newline at end of file diff --git a/FusionIIIT/applications/patent_system/serializers.py b/FusionIIIT/applications/patent_system/serializers.py index 825b5ed01..a96bda442 100644 --- a/FusionIIIT/applications/patent_system/serializers.py +++ b/FusionIIIT/applications/patent_system/serializers.py @@ -1,5 +1,24 @@ from rest_framework import serializers -from .models import Attorney, Document +from .models import ( + AppealRequest, + Attorney, + AuditLog, + BudgetApproval, + CommunicationLog, + ConflictDeclaration, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LegalAdviceMemo, + LegalAssessment, + LicensingRequest, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + OfficeActionResponse, + PriorArtReference, +) class AttorneySerializer(serializers.ModelSerializer): class Meta: @@ -11,4 +30,116 @@ class DocumentSerializer(serializers.ModelSerializer): class Meta: model = Document fields = ['id', 'title', 'link', 'created_at', 'updated_at'] - read_only_fields = ['id', 'created_at', 'updated_at'] \ No newline at end of file + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class CommunicationLogSerializer(serializers.ModelSerializer): + class Meta: + model = CommunicationLog + fields = '__all__' + read_only_fields = ['id', 'application', 'logged_by', 'created_at', 'updated_at'] + + +class ConflictDeclarationSerializer(serializers.ModelSerializer): + class Meta: + model = ConflictDeclaration + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LegalAssessmentSerializer(serializers.ModelSerializer): + class Meta: + model = LegalAssessment + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class NotificationEventSerializer(serializers.ModelSerializer): + class Meta: + model = NotificationEvent + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class BudgetApprovalSerializer(serializers.ModelSerializer): + class Meta: + model = BudgetApproval + fields = '__all__' + read_only_fields = ['id', 'created_at', 'decided_at'] + + +class ExternalFilingRecordSerializer(serializers.ModelSerializer): + class Meta: + model = ExternalFilingRecord + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class MaintenanceScheduleSerializer(serializers.ModelSerializer): + class Meta: + model = MaintenanceSchedule + fields = '__all__' + read_only_fields = ['id', 'created_at', 'reminder_sent_at', 'paid_at'] + + +class DocumentVersionSerializer(serializers.ModelSerializer): + class Meta: + model = DocumentVersion + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class InventorConsentSerializer(serializers.ModelSerializer): + class Meta: + model = InventorConsent + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class OfficeActionSerializer(serializers.ModelSerializer): + class Meta: + model = OfficeAction + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class OfficeActionResponseSerializer(serializers.ModelSerializer): + class Meta: + model = OfficeActionResponse + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LicensingRequestSerializer(serializers.ModelSerializer): + class Meta: + model = LicensingRequest + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class AppealRequestSerializer(serializers.ModelSerializer): + class Meta: + model = AppealRequest + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class PriorArtReferenceSerializer(serializers.ModelSerializer): + class Meta: + model = PriorArtReference + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LegalAdviceMemoSerializer(serializers.ModelSerializer): + class Meta: + model = LegalAdviceMemo + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class AuditLogSerializer(serializers.ModelSerializer): + class Meta: + model = AuditLog + fields = '__all__' + read_only_fields = ['id', 'created_at'] \ No newline at end of file diff --git a/FusionIIIT/applications/patent_system/tests/__init__.py b/FusionIIIT/applications/patent_system/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/patent_system/tests/role_auth_flow_smoke.py b/FusionIIIT/applications/patent_system/tests/role_auth_flow_smoke.py new file mode 100644 index 000000000..8c6cc2dfc --- /dev/null +++ b/FusionIIIT/applications/patent_system/tests/role_auth_flow_smoke.py @@ -0,0 +1,164 @@ +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError + +from django.contrib.auth.models import User +from rest_framework.test import APIClient + +from applications.globals.models import Designation, DepartmentInfo, ExtraInfo, HoldsDesignation +from applications.patent_system.models import Applicant + + +PASSWORD = "Pass1234!" + + +def _ensure_account(username, first_name, last_name, user_type, role_name, designation_type): + user, _ = User.objects.get_or_create( + username=username, + defaults={ + "email": f"{username}@example.com", + "first_name": first_name, + "last_name": last_name, + }, + ) + user.first_name = first_name + user.last_name = last_name + user.email = f"{username}@example.com" + user.set_password(PASSWORD) + user.save() + + dept, _ = DepartmentInfo.objects.get_or_create(name="CSE") + extra, _ = ExtraInfo.objects.get_or_create( + user=user, + defaults={ + "id": f"EX{user.id}", + "title": "Dr.", + "sex": "M", + "user_status": "PRESENT", + "address": "Campus", + "phone_no": 9999999999, + "user_type": user_type, + "department": dept, + }, + ) + extra.user_type = user_type + extra.department = dept + extra.last_selected_role = role_name + extra.save() + + designation, _ = Designation.objects.get_or_create( + name=role_name, + defaults={"full_name": role_name, "type": designation_type}, + ) + HoldsDesignation.objects.get_or_create(user=user, working=user, designation=designation) + + if user_type == "student": + Applicant.objects.get_or_create( + user=user, + defaults={ + "name": f"{first_name} {last_name}".strip(), + "email": user.email, + "mobile": "9999999999", + "address": "Campus", + }, + ) + + +def _assert(condition, message): + if not condition: + raise AssertionError(message) + + +def _check_frontend_url(url): + request = Request(url, method="GET") + with urlopen(request, timeout=5) as response: + body = response.read().decode("utf-8", errors="ignore") + _assert(response.status == 200, f"frontend check failed for {url}: HTTP {response.status}") + _assert("Fusion" in body or "fusion" in body, f"frontend response did not contain expected app marker for {url}") + + +def _run_role_flow(client, username, expected_role, expected_endpoint): + login_response = client.post( + "/api/auth/login/", + {"username": username, "password": PASSWORD}, + format="json", + ) + _assert(login_response.status_code == 200, f"login failed for {username}: {login_response.status_code}") + + token = login_response.data.get("token") + _assert(token, f"login token missing for {username}") + + client.credentials(HTTP_AUTHORIZATION=f"Token {token}") + + me_response = client.get("/api/auth/me") + _assert(me_response.status_code == 200, f"auth/me failed for {username}: {me_response.status_code}") + + designation_info = me_response.data.get("designation_info", []) + _assert(expected_role in designation_info, f"expected role '{expected_role}' missing in designation_info for {username}: {designation_info}") + + update_role_response = client.patch( + "/api/update-role/", + {"last_selected_role": expected_role}, + format="json", + ) + _assert(update_role_response.status_code == 200, f"update-role failed for {username}: {update_role_response.status_code}") + + me_after_update = client.get("/api/auth/me") + _assert(me_after_update.status_code == 200, f"auth/me (after update-role) failed for {username}: {me_after_update.status_code}") + _assert( + me_after_update.data.get("last_selected_role") == expected_role, + f"last_selected_role mismatch for {username}: {me_after_update.data.get('last_selected_role')} != {expected_role}", + ) + + endpoint_response = client.get(expected_endpoint) + _assert(endpoint_response.status_code == 200, f"role endpoint failed for {username} on {expected_endpoint}: {endpoint_response.status_code}") + + +def run(): + _ensure_account("patent_student", "Patent", "Student", "student", "student", "academic") + _ensure_account("patent_pcc", "Patent", "PCC", "staff", "PCC Admin", "administrative") + _ensure_account("patent_director", "Patent", "Director", "staff", "Director", "administrative") + + # Frontend availability checks for login page and SPA route. + _check_frontend_url("http://127.0.0.1:5173/accounts/login") + _check_frontend_url("http://127.0.0.1:5173/patentsystem") + + client = APIClient() + client.raise_request_exception = False + + _run_role_flow( + client, + username="patent_student", + expected_role="student", + expected_endpoint="/patentsystem/applicant/insights/", + ) + _run_role_flow( + client, + username="patent_pcc", + expected_role="PCC Admin", + expected_endpoint="/patentsystem/pccAdmin/insights/", + ) + _run_role_flow( + client, + username="patent_director", + expected_role="Director", + expected_endpoint="/patentsystem/director/insights/", + ) + + # Authorization boundary check: Director must not access PCC-only insights endpoint. + login_director = client.post( + "/api/auth/login/", + {"username": "patent_director", "password": PASSWORD}, + format="json", + ) + director_token = login_director.data.get("token") + client.credentials(HTTP_AUTHORIZATION=f"Token {director_token}") + forbidden_response = client.get("/patentsystem/pccAdmin/insights/") + _assert(forbidden_response.status_code == 403, f"expected 403 for director on pcc insights, got {forbidden_response.status_code}") + + print("PATENT_ROLE_AUTH_FLOW_SMOKE: PASS") + + +try: + run() +except (HTTPError, URLError) as exc: + raise AssertionError(f"frontend availability check failed: {exc}") diff --git a/FusionIIIT/applications/patent_system/tests/runtime_smoke.py b/FusionIIIT/applications/patent_system/tests/runtime_smoke.py new file mode 100644 index 000000000..a2eb01bf7 --- /dev/null +++ b/FusionIIIT/applications/patent_system/tests/runtime_smoke.py @@ -0,0 +1,146 @@ +import uuid +from datetime import date + +from django.contrib.auth.models import User +from rest_framework.test import APIClient + +from applications.patent_system.models import Applicant, Application, CommunicationLog + + +def run(): + import uuid + + suffix = uuid.uuid4().hex[:8] + created_users = [] + created_apps = [] + + try: + pcc = User.objects.create_user( + username=f"pcc_{suffix}", + email=f"pcc_{suffix}@example.com", + password="pass1234", + ) + director = User.objects.create_user( + username=f"dir_{suffix}", + email=f"dir_{suffix}@example.com", + password="pass1234", + ) + app_user = User.objects.create_user( + username=f"app_{suffix}", + email=f"app_{suffix}@example.com", + password="pass1234", + ) + created_users.extend([pcc, director, app_user]) + + applicant = Applicant.objects.create( + user=app_user, + name="Smoke Applicant", + email=f"app.personal_{suffix}@example.com", + mobile="9999999999", + address="Campus", + ) + + app1 = Application.objects.create( + primary_applicant=applicant, + title="Smoke Patent 1", + status="Submitted", + decision_status="Pending", + submitted_date=date.today(), + ) + created_apps.append(app1) + + client = APIClient() + client.force_authenticate(user=pcc) + + review_response = client.post( + f"/patentsystem/pccAdmin/applications/new/review/{app1.id}/", + {"comments": "reviewed"}, + format="json", + ) + assert review_response.status_code == 200, review_response.content + + invalid_transition = client.post( + f"/patentsystem/pccAdmin/applications/ongoing/changeStatus/{app1.id}/", + {"next_status": "Patent Filed"}, + format="json", + ) + assert invalid_transition.status_code == 400, invalid_transition.content + + forward_response = client.post( + f"/patentsystem/pccAdmin/applications/new/forward/{app1.id}/", + { + "attorney_name": "External Counsel", + "attorney_email": "external@example.com", + "comments": "forwarding", + }, + format="json", + ) + assert forward_response.status_code == 200, forward_response.content + + app1.refresh_from_db() + assert app1.status == "Forwarded for Director's Review" + assert CommunicationLog.objects.filter(application=app1).exists() + + comm_response = client.post( + f"/patentsystem/pccAdmin/applications/{app1.id}/communication-logs/", + { + "external_attorney_name": "Counsel", + "external_attorney_email": "c@example.com", + "message_content": "shared draft", + "status_or_notes": "awaiting", + }, + format="json", + ) + assert comm_response.status_code == 201, comm_response.content + + client.force_authenticate(user=director) + reject_wrong_stage = client.post( + "/patentsystem/director/application/reject", + {"application_id": app1.id + 9999}, + format="json", + ) + assert reject_wrong_stage.status_code in (400, 404), reject_wrong_stage.content + + approve_response = client.post( + "/patentsystem/director/application/accept", + {"application_id": app1.id, "comments": "approved"}, + format="json", + ) + assert approve_response.status_code == 200, approve_response.content + app1.refresh_from_db() + assert app1.status == "Director's Approval Received" + assert app1.decision_status == "Pending" + + app2 = Application.objects.create( + primary_applicant=applicant, + title="Smoke Patent 2", + status="Forwarded for Director's Review", + decision_status="Pending", + submitted_date=date.today(), + ) + created_apps.append(app2) + + reject_response = client.post( + "/patentsystem/director/application/reject", + {"application_id": app2.id}, + format="json", + ) + assert reject_response.status_code == 200, reject_response.content + app2.refresh_from_db() + assert app2.status == "Patent Refused" + + print("PATENT_RUNTIME_SMOKE: PASS") + finally: + for app in created_apps: + try: + app.delete() + except Exception: + pass + for user in created_users: + try: + user.delete() + except Exception: + pass + + +run() diff --git a/FusionIIIT/applications/patent_system/tests/seed_role_accounts.py b/FusionIIIT/applications/patent_system/tests/seed_role_accounts.py new file mode 100644 index 000000000..85dd97ec3 --- /dev/null +++ b/FusionIIIT/applications/patent_system/tests/seed_role_accounts.py @@ -0,0 +1,65 @@ +from django.contrib.auth.models import User +from rest_framework.authtoken.models import Token + +from applications.globals.models import Designation, DepartmentInfo, ExtraInfo, HoldsDesignation + + +def ensure_account(username, password, first_name, last_name, user_type, role_name, designation_type="administrative"): + user, _ = User.objects.get_or_create( + username=username, + defaults={ + "email": f"{username}@example.com", + "first_name": first_name, + "last_name": last_name, + }, + ) + user.first_name = first_name + user.last_name = last_name + user.email = f"{username}@example.com" + user.set_password(password) + user.save() + + dept, _ = DepartmentInfo.objects.get_or_create(name="CSE") + + extra, _ = ExtraInfo.objects.get_or_create( + user=user, + defaults={ + "id": f"EX{user.id}", + "title": "Dr.", + "sex": "M", + "user_status": "PRESENT", + "address": "Campus", + "phone_no": 9999999999, + "user_type": user_type, + "department": dept, + }, + ) + extra.user_type = user_type + extra.department = dept + extra.last_selected_role = role_name + extra.save() + + designation, _ = Designation.objects.get_or_create( + name=role_name, + defaults={"full_name": role_name, "type": designation_type}, + ) + HoldsDesignation.objects.get_or_create(user=user, working=user, designation=designation) + + Token.objects.filter(user=user).delete() + token = Token.objects.create(user=user) + return user.username, token.key + + +def run(): + accounts = [ + ("patent_student", "Pass1234!", "Patent", "Student", "student", "student", "academic"), + ("patent_pcc", "Pass1234!", "Patent", "PCC", "staff", "PCC Admin", "administrative"), + ("patent_director", "Pass1234!", "Patent", "Director", "staff", "Director", "administrative"), + ] + + for account in accounts: + username, token = ensure_account(*account) + print(f"{username} {token}") + + +run() diff --git a/FusionIIIT/applications/patent_system/tests/test_workflow.py b/FusionIIIT/applications/patent_system/tests/test_workflow.py new file mode 100644 index 000000000..801ef1887 --- /dev/null +++ b/FusionIIIT/applications/patent_system/tests/test_workflow.py @@ -0,0 +1,263 @@ +from datetime import date + +from django.contrib.auth.models import User +from django.test import TestCase +from rest_framework.test import APIClient + +from applications.patent_system.models import Applicant, Application, Attorney, CommunicationLog, AssociatedWith + + +class PatentWorkflowTests(TestCase): + def setUp(self): + self.client = APIClient() + + self.pcc_admin = User.objects.create_user( + username="pccadmin", email="pcc@example.com", password="pass1234" + ) + self.director = User.objects.create_user( + username="director", email="director@example.com", password="pass1234" + ) + self.attorney_user = User.objects.create_user( + username="attorney", email="attorney@example.com", password="pass1234" + ) + self.applicant_user = User.objects.create_user( + username="applicant", email="applicant@example.com", password="pass1234" + ) + + self.applicant = Applicant.objects.create( + user=self.applicant_user, + name="Applicant One", + email="applicant.personal@example.com", + mobile="9999999999", + address="Campus", + ) + + self.attorney = Attorney.objects.create( + name="External Counsel", + email="attorney@example.com", + phone="9998887777", + firm_name="Counsel LLP", + ) + + self.application = Application.objects.create( + primary_applicant=self.applicant, + title="Smart Patent", + status="Submitted", + decision_status="Pending", + submitted_date=date.today(), + ) + + def test_pcc_review_updates_status_and_owner(self): + self.client.force_authenticate(user=self.pcc_admin) + + response = self.client.post( + f"/patentsystem/pccAdmin/applications/new/review/{self.application.id}/", + {"comments": "Reviewed and ready"}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + self.application.refresh_from_db() + self.assertEqual(self.application.status, "Reviewed by PCC Admin") + self.assertEqual(self.application.assigned_pcc_admin, self.pcc_admin) + self.assertEqual(self.application.comments, "Reviewed and ready") + + def test_pcc_forward_creates_communication_log(self): + self.application.status = "Reviewed by PCC Admin" + self.application.assigned_pcc_admin = self.pcc_admin + self.application.save() + + self.client.force_authenticate(user=self.pcc_admin) + + response = self.client.post( + f"/patentsystem/pccAdmin/applications/new/forward/{self.application.id}/", + { + "attorney_name": "External Counsel", + "attorney_email": "external@example.com", + "comments": "Forwarding for director approval", + }, + format="json", + ) + + self.assertEqual(response.status_code, 200) + self.application.refresh_from_db() + self.assertEqual(self.application.status, "Forwarded for Director's Review") + self.assertEqual(self.application.assigned_pcc_admin, self.pcc_admin) + + self.assertEqual(CommunicationLog.objects.filter(application=self.application).count(), 1) + log = CommunicationLog.objects.get(application=self.application) + self.assertEqual(log.external_attorney_name, "External Counsel") + self.assertEqual(log.external_attorney_email, "external@example.com") + + def test_change_status_rejects_invalid_transition_target(self): + self.client.force_authenticate(user=self.pcc_admin) + + response = self.client.post( + f"/patentsystem/pccAdmin/applications/ongoing/changeStatus/{self.application.id}/", + {"next_status": "Not A Valid Status"}, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("Invalid next_status", response.json().get("error", "")) + + def test_director_accept_sets_director_approval_received(self): + self.application.status = "Forwarded for Director's Review" + self.application.assigned_pcc_admin = self.pcc_admin + self.application.attorney = self.attorney + self.application.save() + + self.client.force_authenticate(user=self.director) + response = self.client.post( + "/patentsystem/director/application/accept", + {"application_id": self.application.id, "comments": "Approved"}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + self.application.refresh_from_db() + self.assertEqual(self.application.status, "Attorney Assigned") + self.assertEqual(self.application.decision_status, "Pending") + self.assertTrue(bool(self.application.token_no)) + self.assertEqual(self.application.attorney, self.attorney) + + def test_pcc_forward_requires_reviewed_status(self): + self.client.force_authenticate(user=self.pcc_admin) + + response = self.client.post( + f"/patentsystem/pccAdmin/applications/new/forward/{self.application.id}/", + { + "attorney_name": "External Counsel", + "attorney_email": "external@example.com", + "comments": "Forwarding for director approval", + }, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("Reviewed by PCC Admin", response.json().get("error", "")) + + def test_change_status_requires_sequential_transition(self): + self.application.status = "Forwarded for Director's Review" + self.application.assigned_pcc_admin = self.pcc_admin + self.application.save() + self.client.force_authenticate(user=self.pcc_admin) + + response = self.client.post( + f"/patentsystem/pccAdmin/applications/ongoing/changeStatus/{self.application.id}/", + {"next_status": "Patent Filed"}, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("Invalid status transition", response.json().get("error", "")) + + def test_director_reject_requires_forwarded_status(self): + self.application.status = "Submitted" + self.application.save() + self.client.force_authenticate(user=self.director) + + response = self.client.post( + "/patentsystem/director/application/reject", + {"application_id": self.application.id}, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("Forwarded for Director's Review", response.json().get("error", "")) + + def test_director_reject_maps_to_patent_refused(self): + self.application.status = "Forwarded for Director's Review" + self.application.save() + + self.client.force_authenticate(user=self.director) + response = self.client.post( + "/patentsystem/director/application/reject", + {"application_id": self.application.id}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + self.application.refresh_from_db() + self.assertEqual(self.application.status, "Needs Revision") + self.assertEqual(self.application.decision_status, "Needs Revision") + + def test_attorney_forward_returns_to_director(self): + self.application.status = "Attorney Assigned" + self.application.attorney = self.attorney + self.application.save() + + self.client.force_authenticate(user=self.attorney_user) + response = self.client.post( + f"/patentsystem/attorney/applications/{self.application.id}/forward/", + {"comments": "Patentability assessment completed"}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + self.application.refresh_from_db() + self.assertEqual(self.application.status, "Returned to Director") + self.assertEqual(self.application.attorney_review_notes, "Patentability assessment completed") + + def test_attorney_forward_requires_comments(self): + self.application.status = "Attorney Assigned" + self.application.attorney = self.attorney + self.application.save() + + self.client.force_authenticate(user=self.attorney_user) + response = self.client.post( + f"/patentsystem/attorney/applications/{self.application.id}/forward/", + {"comments": ""}, + format="json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("Comments are required", response.json().get("error", "")) + + def test_communication_log_post_and_get(self): + self.client.force_authenticate(user=self.pcc_admin) + + create_response = self.client.post( + f"/patentsystem/pccAdmin/applications/{self.application.id}/communication-logs/", + { + "external_attorney_name": "Counsel One", + "external_attorney_email": "counsel@example.com", + "message_content": "Shared claim draft", + "status_or_notes": "Awaiting feedback", + }, + format="json", + ) + self.assertEqual(create_response.status_code, 201) + + list_response = self.client.get( + f"/patentsystem/pccAdmin/applications/{self.application.id}/communication-logs/" + ) + self.assertEqual(list_response.status_code, 200) + self.assertEqual(len(list_response.json()), 1) + self.assertEqual(list_response.json()[0]["message_content"], "Shared claim draft") + + def test_same_applicant_can_file_multiple_patents(self): + second_application = Application.objects.create( + primary_applicant=self.applicant, + title="Second Patent Filing", + status="Submitted", + decision_status="Pending", + submitted_date=date.today(), + ) + + AssociatedWith.objects.create( + application=self.application, + applicant=self.applicant, + percentage_share=50, + ) + AssociatedWith.objects.create( + application=second_application, + applicant=self.applicant, + percentage_share=50, + ) + + self.assertEqual( + Application.objects.filter(primary_applicant=self.applicant).count(), + 2, + ) + self.assertEqual(AssociatedWith.objects.filter(applicant=self.applicant).count(), 2) diff --git a/FusionIIIT/applications/patent_system/tests/uc_api_smoke.py b/FusionIIIT/applications/patent_system/tests/uc_api_smoke.py new file mode 100644 index 000000000..f99ea43f2 --- /dev/null +++ b/FusionIIIT/applications/patent_system/tests/uc_api_smoke.py @@ -0,0 +1,342 @@ +from datetime import date +from decimal import Decimal + +from django.contrib.auth.models import User +from rest_framework.test import APIClient + +from applications.globals.models import Designation, HoldsDesignation +from applications.patent_system.models import Applicant, Application, Attorney, BudgetApproval, Document + + +def assert_status(response, allowed, label): + if response.status_code not in allowed: + raise AssertionError(f"{label} failed ({response.status_code})") + + +def run(): + import uuid + suffix = uuid.uuid4().hex[:8] + created_users = [] + created_apps = [] + created_documents = [] + + try: + pcc = User.objects.create_user( + username=f"pcc_{suffix}", + email=f"pcc_{suffix}@example.com", + password="pass1234", + ) + director = User.objects.create_user( + username=f"director_{suffix}", + email=f"director_{suffix}@example.com", + password="pass1234", + ) + applicant_user = User.objects.create_user( + username=f"applicant_{suffix}", + email=f"applicant_{suffix}@example.com", + password="pass1234", + ) + created_users.extend([pcc, director, applicant_user]) + + pcc_designation, _ = Designation.objects.get_or_create( + name="pcc_admin", + defaults={"full_name": "PCC Admin", "type": "administrative"}, + ) + director_designation, _ = Designation.objects.get_or_create( + name="director", + defaults={"full_name": "Director", "type": "administrative"}, + ) + + HoldsDesignation.objects.get_or_create(user=pcc, working=pcc, designation=pcc_designation) + HoldsDesignation.objects.get_or_create(user=director, working=director, designation=director_designation) + + applicant = Applicant.objects.create( + user=applicant_user, + name="UC Smoke Applicant", + email=f"uc_{suffix}@example.com", + mobile="9999999999", + address="Campus", + ) + + application = Application.objects.create( + primary_applicant=applicant, + title=f"UC Smoke Patent {suffix}", + status="Submitted", + decision_status="Pending", + submitted_date=date.today(), + ) + created_apps.append(application) + + attorney = Attorney.objects.create( + name=f"Counsel {suffix}", + email=f"counsel_{suffix}@example.com", + phone="9876543210", + firm_name="External Legal LLP", + ) + + client = APIClient() + client.raise_request_exception = False + + client.force_authenticate(user=pcc) + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/new/review/{application.id}/", + {"comments": "reviewed"}, + format="json", + ), + [200], + "review application", + ) + + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/new/forward/{application.id}/", + { + "attorney_name": "External Counsel", + "attorney_email": "external@example.com", + "comments": "forwarding to director", + }, + format="json", + ), + [200], + "forward application", + ) + + client.force_authenticate(user=director) + assert_status( + client.post( + "/patentsystem/director/application/accept", + {"application_id": application.id, "comments": "approved"}, + format="json", + ), + [200], + "director accept", + ) + + client.force_authenticate(user=pcc) + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/legal-assessment/", + { + "attorney": attorney.id, + "opinion": "Positive", + "prior_art_summary": "No direct overlap", + "recommended_action": "Proceed", + }, + format="json", + ), + [201], + "legal assessment create", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/legal-assessment/"), + [200], + "legal assessment list", + ) + + budget_response = client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/budget/", + {"amount": "75000", "threshold": "50000", "comments": "budget required"}, + format="json", + ) + assert_status(budget_response, [201], "budget create") + + budget_id = BudgetApproval.objects.filter(application=application).order_by("-id").values_list("id", flat=True).first() + + client.force_authenticate(user=director) + assert_status( + client.post( + f"/patentsystem/pccAdmin/budget/{budget_id}/decision/", + {"decision": "approve", "comments": "approved"}, + format="json", + ), + [200], + "budget decide", + ) + + client.force_authenticate(user=pcc) + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/external-filing/", + { + "patent_office": "IPO", + "filing_reference": f"REF-{suffix}", + "communication_notes": "Filed successfully", + "filing_date": str(date.today()), + }, + format="json", + ), + [201], + "external filing create", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/external-filing/"), + [200], + "external filing list", + ) + + office_action_resp = client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/office-actions/", + { + "office_name": "IPO", + "action_reference": f"OA-{suffix}", + "action_summary": "clarify claims", + "due_date": str(date.today()), + }, + format="json", + ) + assert_status(office_action_resp, [201], "office action create") + action_id = office_action_resp.json().get("id") + assert_status( + client.post( + f"/patentsystem/pccAdmin/office-actions/{action_id}/respond/", + {"response_text": "submitted revised claim set", "response_reference": f"R-{suffix}"}, + format="json", + ), + [201], + "office action respond", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/office-actions/"), + [200], + "office action list", + ) + + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/prior-art/", + {"reference_type": "Patent", "citation": f"US-{suffix}", "notes": "related"}, + format="json", + ), + [201], + "prior art create", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/prior-art/?q=US-"), + [200], + "prior art list", + ) + + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/licensing/", + { + "requester_name": "ACME", + "requester_org": "ACME Labs", + "request_details": "license request", + }, + format="json", + ), + [201], + "licensing create", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/licensing/"), + [200], + "licensing list", + ) + + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/inventor-consents/", + {"agreement_reference": f"AG-{suffix}"}, + format="json", + ), + [200], + "inventor consents ensure", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/inventor-consents/"), + [200], + "inventor consents list", + ) + + maintenance_response = client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/maintenance/", + {"due_date": str(date.today()), "amount": "1000"}, + format="json", + ) + assert_status(maintenance_response, [201], "maintenance setup") + schedule_id = maintenance_response.json().get("id") + assert_status( + client.post(f"/patentsystem/pccAdmin/maintenance/{schedule_id}/mark-paid/", {}, format="json"), + [200], + "maintenance mark paid", + ) + + assert_status(client.get("/patentsystem/pccAdmin/queue/prioritized/"), [200], "queue") + assert_status(client.get("/patentsystem/notifications/"), [200], "notifications") + assert_status(client.get("/patentsystem/pccAdmin/audit-logs/"), [200], "audit logs") + assert_status(client.get("/patentsystem/audit-logs/"), [200], "audit logs alias") + assert_status(client.get("/patentsystem/pccAdmin/insights/"), [200], "pcc insights") + + document = Document.objects.create(title=f"Doc {suffix}", link="https://example.com/doc-v1", application=application) + created_documents.append(document) + + assert_status( + client.post( + f"/patentsystem/documents/{document.id}/versions/upload/", + {"link": "https://example.com/doc-v2"}, + format="json", + ), + [201], + "document version upload", + ) + assert_status(client.get(f"/patentsystem/documents/{document.id}/versions/"), [200], "document version list") + assert_status(client.post(f"/patentsystem/documents/{document.id}/lock/", {}, format="json"), [200], "document lock") + + assert_status( + client.post( + f"/patentsystem/pccAdmin/applications/{application.id}/communication-logs/", + { + "external_attorney_name": "Counsel", + "external_attorney_email": "c@example.com", + "message_content": "status update", + "status_or_notes": "pending", + }, + format="json", + ), + [201], + "communication log create", + ) + assert_status( + client.get(f"/patentsystem/pccAdmin/applications/{application.id}/communication-logs/"), + [200], + "communication log list", + ) + + client.force_authenticate(user=applicant_user) + assert_status( + client.post( + f"/patentsystem/applicant/applications/{application.id}/appeals/", + {"grounds": "request reconsideration"}, + format="json", + ), + [201], + "appeal submit", + ) + assert_status(client.get(f"/patentsystem/pccAdmin/applications/{application.id}/appeals/"), [200], "appeal list") + assert_status(client.get("/patentsystem/applicant/insights/"), [200], "applicant insights") + + print("PATENT_UC_API_SMOKE: PASS") + finally: + for document in created_documents: + try: + document.delete() + except Exception: + pass + + for app in created_apps: + try: + app.delete() + except Exception: + pass + + for user in created_users: + try: + user.delete() + except Exception: + pass + + +run() diff --git a/FusionIIIT/applications/patent_system/urls.py b/FusionIIIT/applications/patent_system/urls.py index 381a44aaa..282bbbb71 100644 --- a/FusionIIIT/applications/patent_system/urls.py +++ b/FusionIIIT/applications/patent_system/urls.py @@ -4,11 +4,17 @@ from . import views urlpatterns = [ + path("", views.index, name="index"), + # Applicant-related paths path("applicant/applications/submit/", views.submit_application, name="submit_application"), path("applicant/applications/", views.view_applications, name="view_applications"), path("applicant/applications/details//", views.view_application_details_for_applicant, name="view_application_details"), path("applicant/drafts/", views.saved_drafts, name="saved_drafts"), + path("applicant/applications//withdraw/", views.withdraw_application, name="withdraw_application"), + path("applicant/applications//resubmit/", views.resubmit_application, name="resubmit_application"), + path("applicant/applications//appeals/", views.submit_appeal, name="submit_appeal"), + path("applicant/insights/", views.get_applicant_insights, name="get_applicant_insights"), # PCCAdmin-related paths path("pccAdmin/applications/new/", views.new_applications, name="new_applications"), @@ -19,6 +25,28 @@ path("pccAdmin/applications/ongoing/changeStatus//", views.change_application_status, name="change_application_status"), path("pccAdmin/applications/past/", views.past_applications, name="past_applications"), path("pccAdmin/applications/details//", views.view_application_details_for_pccAdmin, name="view_application_details_for_pccAdmin"), + path("pccAdmin/applications//communication-logs/", views.communication_logs, name="communication_logs"), + path("pccAdmin/applications//declare-conflict/", views.declare_conflict, name="declare_conflict"), + path("pccAdmin/applications//legal-assessment/", views.legal_assessment_api, name="legal_assessment_api"), + path("pccAdmin/applications//legal-memos/", views.legal_memos_api, name="legal_memos_api"), + path("pccAdmin/applications//budget/", views.budget_api, name="budget_api"), + path("pccAdmin/budget//decision/", views.budget_decision_by_id, name="budget_decision_by_id"), + path("pccAdmin/applications//external-filing/", views.external_filing_api, name="external_filing_api"), + path("pccAdmin/applications//office-actions/", views.office_actions_api, name="office_actions_api"), + path("pccAdmin/office-actions//respond/", views.respond_office_action, name="respond_office_action"), + path("pccAdmin/applications//prior-art/", views.prior_art_api, name="prior_art_api"), + path("pccAdmin/applications//appeals/", views.appeals_api, name="appeals_api"), + path("pccAdmin/applications//licensing/", views.licensing_api, name="licensing_api"), + path("pccAdmin/applications//inventor-consents/", views.inventor_consents_api, name="inventor_consents_api"), + path("pccAdmin/applications//maintenance/", views.maintenance_api, name="maintenance_api"), + path("pccAdmin/maintenance//mark-paid/", views.mark_maintenance_paid, name="mark_maintenance_paid"), + path("pccAdmin/reviewer-queue/", views.reviewer_queue, name="reviewer_queue"), + path("pccAdmin/queue/prioritized/", views.queue_prioritized, name="queue_prioritized"), + path("pccAdmin/notifications/", views.get_notifications, name="get_notifications"), + path("pccAdmin/notifications//read/", views.mark_notification_read, name="mark_notification_read"), + path("pccAdmin/audit-logs/", views.get_audit_logs, name="get_audit_logs"), + path("pccAdmin/insights/", views.pcc_insights, name="pcc_insights"), + path("pccAdmin/applications/new/resubmit//", views.pcc_resubmit_application, name="pcc_resubmit_application"), # Director-related paths path("director/applications/new/", views.director_new_applications, name="director_new_applications"), @@ -28,6 +56,10 @@ path("director/active", views.active_applications, name="active_applications"), path("director/application/details", views.director_application_view, name="director_application_view"), path("director/notifications/", views.director_notifications, name="director_notifications"), + path("director/applications//budget/decision/", views.director_decide_budget, name="director_decide_budget"), + path("director/insights/", views.director_insights, name="director_insights"), + path("attorney/applications/", views.attorney_applications, name="attorney_applications"), + path("attorney/applications//forward/", views.attorney_forward_to_director, name="attorney_forward_to_director"), # Attorney management URLs path("pccAdmin/attorneys/", views.get_attorney_list, name="get_attorney_list"), @@ -39,6 +71,16 @@ # Document Management URLs path('documents/', views.manage_documents, name='manage_documents'), path('pccAdmin/documents//delete/', views.delete_document, name='delete_document'), + path('pccAdmin/documents//versions/upload/', views.upload_document_version, name='upload_document_version'), + path('pccAdmin/documents//lock/', views.lock_document, name='lock_document'), + path('documents//versions/upload/', views.upload_document_version, name='upload_document_version_alias'), + path('documents//versions/', views.document_versions_api, name='document_versions_api'), + path('documents//lock/', views.lock_document, name='lock_document_alias'), + + # Global aliases used by imported frontend services + path('notifications/', views.notifications_root, name='notifications_root'), + path('audit-logs/', views.audit_logs_root, name='audit_logs_root'), + path('audit-logs//', views.audit_logs_by_application, name='audit_logs_by_application'), ] # Serve media files in development diff --git a/FusionIIIT/applications/patent_system/views.py b/FusionIIIT/applications/patent_system/views.py index 897fc71cf..1c00c0d3c 100644 --- a/FusionIIIT/applications/patent_system/views.py +++ b/FusionIIIT/applications/patent_system/views.py @@ -1,9 +1,12 @@ import os import json import logging +from datetime import timedelta +from decimal import Decimal -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from django.utils.timezone import now +from django.utils import timezone from django.core.exceptions import ObjectDoesNotExist from django.core.files.storage import default_storage from django.shortcuts import get_object_or_404 @@ -11,6 +14,7 @@ from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt from django.db import transaction +from django.db.models import Q from rest_framework import status from rest_framework.response import Response @@ -26,8 +30,24 @@ ApplicationSectionIII, AssociatedWith, Applicant, + AppealRequest, Attorney, - Document + AuditLog, + BudgetApproval, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LegalAdviceMemo, + LegalAssessment, + LicensingRequest, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + OfficeActionResponse, + PriorArtReference, + CommunicationLog, + ConflictDeclaration, ) from applications.globals.models import ( @@ -37,11 +57,45 @@ HoldsDesignation, ) -from .serializers import AttorneySerializer, DocumentSerializer +from .serializers import ( + AppealRequestSerializer, + AttorneySerializer, + AuditLogSerializer, + BudgetApprovalSerializer, + CommunicationLogSerializer, + ConflictDeclarationSerializer, + DocumentSerializer, + DocumentVersionSerializer, + ExternalFilingRecordSerializer, + InventorConsentSerializer, + LegalAdviceMemoSerializer, + LegalAssessmentSerializer, + LicensingRequestSerializer, + MaintenanceScheduleSerializer, + NotificationEventSerializer, + OfficeActionResponseSerializer, + OfficeActionSerializer, + PriorArtReferenceSerializer, +) # Logger setup - used for debugging and logging errors logger = logging.getLogger(__name__) + +def index(request): + return JsonResponse( + { + "message": "Patent Management module is running.", + "routes": [ + "/patentsystem/applicant/applications/submit/", + "/patentsystem/applicant/applications/", + "/patentsystem/pccAdmin/applications/new/", + "/patentsystem/pccAdmin/applications/past/", + "/patentsystem/director/applications/new/", + ], + } + ) + # ----------------------------------------- # 🔹 Applicant Views # ----------------------------------------- @@ -90,6 +144,15 @@ def submit_application(request): # Get the logged-in user user = request.user + if not _is_authorized_applicant_user(user): + return JsonResponse( + { + "error": ( + "Only authorized applicants, including faculty roles, can submit patent applications." + ) + }, + status=403, + ) # Check if the user has an applicant profile, create one if not applicant, created = Applicant.objects.get_or_create( @@ -236,8 +299,15 @@ def view_applications(request): # Get all application IDs associated with this applicant associated_apps = AssociatedWith.objects.filter(applicant=applicant).values_list('application_id', flat=True) - # Retrieve applications based on application IDs - applications = Application.objects.filter(id__in=associated_apps) + # Retrieve applications where the user is primary applicant or associated inventor + applications = ( + Application.objects.filter( + Q(primary_applicant=applicant) | Q(id__in=associated_apps) + ) + .select_related("attorney") + .distinct() + .order_by("-last_updated_at") + ) # Prepare response data applications_data = [] @@ -246,8 +316,11 @@ def view_applications(request): "application_id": app.id, "title": app.title, "token_no": app.token_no, + "application_number": app.token_no, "attorney_name": app.attorney.name if app.attorney else None, - "submitted_date": app.submitted_date if app.submitted_date else None + "submitted_date": app.submitted_date if app.submitted_date else None, + "status": app.status, + "decision_status": app.decision_status, }) return JsonResponse({"applications": applications_data}, safe=False) @@ -267,19 +340,20 @@ def view_application_details_for_applicant(request, application_id): except Applicant.DoesNotExist: return JsonResponse({"error": "Unauthorized: User is not an applicant"}, status=403) - # Verify if the user is associated with the application + # Fetch application details + application = get_object_or_404(Application, id=application_id) + + # Primary applicant or associated inventor can view details + is_primary_applicant = application.primary_applicant_id == applicant.id is_associated = AssociatedWith.objects.filter(application_id=application_id, applicant=applicant).exists() - if not is_associated: + if not (is_primary_applicant or is_associated): return JsonResponse({"error": "Forbidden: You are not associated with this application"}, status=403) - # Fetch application details - application = get_object_or_404(Application, id=application_id) + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username - # Fetch attorney details using attorney_id - attorney_name = None - if application.attorney_id: - attorney = Attorney.objects.filter(id=application.attorney_id).first() - attorney_name = attorney.name if attorney else None # Get attorney name safely + attorney_name = application.attorney.name if application.attorney else None # Fetch associated applicants associated_applicants = AssociatedWith.objects.filter(application=application) @@ -337,6 +411,7 @@ def view_application_details_for_applicant(request, application_id): "title": application.title, "status": application.status, "token_no": application.token_no if application.token_no else "Token not generated", + "assigned_pcc_admin": handler_name, "attorney_name": attorney_name, "dates": { "submitted_date": application.submitted_date if application.submitted_date else None, @@ -372,40 +447,46 @@ def saved_drafts(request): @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) def new_applications(request): - REVIEW_STATUSES = ["Submitted", "Reviewed by PCC Admin"] + try: + REVIEW_STATUSES = ["Submitted", "Reviewed by PCC Admin"] - applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") + applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") - application_dict = {} # Using a dictionary instead of a list + application_dict = {} # Using a dictionary instead of a list - for app in applications: - applicant = app.primary_applicant # Get the Applicant instance + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() - designation_name = holds_designation.designation.name if holds_designation else "Unknown" + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" - # Format response as a dictionary - application_dict[app.id] = { - # "application_no": app.id, - "title": app.title, - "submitted_by": applicant.name, - "submitted_by": applicant.name, - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown" - } + # Format response as a dictionary + application_dict[app.id] = { + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown" + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue - return JsonResponse({"applications": application_dict}, safe=False) + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in new_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) @api_view(['POST']) @permission_classes([IsAuthenticated]) @@ -424,9 +505,19 @@ def review_application(request, application_id): except Application.DoesNotExist: return JsonResponse({"error": "Application not found."}, status=404) - # Optional: Check if it's already reviewed to prevent redundant updates + # Enforce workflow stage: only Submitted applications can be reviewed. if application.status == "Reviewed by PCC Admin": return JsonResponse({"message": "Application already reviewed."}) + if application.status != "Submitted": + return JsonResponse( + { + "error": ( + "Only applications in 'Submitted' state can be reviewed. " + f"Current status: {application.status}" + ) + }, + status=400, + ) # Parse JSON body try: @@ -439,6 +530,7 @@ def review_application(request, application_id): application.status = "Reviewed by PCC Admin" if comments != "": application.comments = comments + application.assigned_pcc_admin = request.user application.reviewed_by_pcc_date = now() application.save() @@ -463,6 +555,12 @@ def review_application(request, application_id): def forward_application(request, application_id): if request.method == "POST": try: + if not _is_pcc_admin_user(request.user): + return JsonResponse( + {"error": "Only PCC Admin can assign attorneys and forward applications."}, + status=403, + ) + if not application_id: return JsonResponse({"error": "Application ID is required."}, status=400) @@ -472,25 +570,50 @@ def forward_application(request, application_id): except Application.DoesNotExist: return JsonResponse({"error": "Application not found."}, status=404) - # Prevent double forwarding + # Enforce workflow stage and PCC ownership. if application.status == "Forwarded for Director's Review": return JsonResponse({"message": "Application is already forwarded for Director's review."}, status=400) + if application.status != "Reviewed by PCC Admin": + return JsonResponse( + { + "error": ( + "Only applications in 'Reviewed by PCC Admin' state can be forwarded. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: + owner = application.assigned_pcc_admin + owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") + return JsonResponse( + { + "error": f"Application is assigned to another PCC Admin: {owner_name}.", + "assigned_pcc_admin": owner_name, + }, + status=403, + ) # Parse JSON body try: data = json.loads(request.body) - attorney_name = data.get("attorney_name", "").strip() - comments = data.get("comments", "") + external_attorney_name = data.get("attorney_name", "").strip() + external_attorney_email = data.get("attorney_email", "").strip() + comments = _require_comments(data) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON body."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) - if not attorney_name: + if not external_attorney_name: return JsonResponse({"error": "attorney_name is required in the request body."}, status=400) - # Get attorney (case-insensitive) - attorney = Attorney.objects.filter(name__iexact=attorney_name).first() + attorney = Attorney.objects.filter(name__iexact=external_attorney_name).first() if not attorney: - return JsonResponse({"error": f"Attorney with name '{attorney_name}' not found."}, status=404) + return JsonResponse( + {"error": f"Attorney with name '{external_attorney_name}' not found."}, + status=404, + ) # Optional: Limit comment length if comments and len(comments) > 1000: @@ -499,16 +622,44 @@ def forward_application(request, application_id): # Update the application application.status = "Forwarded for Director's Review" application.forwarded_to_director_date = now() + application.assigned_pcc_admin = request.user application.attorney = attorney - if comments != "": - application.comments = comments + application.comments = comments application.save() + if hasattr(attorney, "current_workload"): + attorney.current_workload = Application.objects.filter(attorney=attorney).count() + attorney.save(update_fields=["current_workload"]) + + if comments or external_attorney_name or external_attorney_email: + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + external_attorney_name=external_attorney_name or None, + external_attorney_email=external_attorney_email or None, + message_content=comments or "Application forwarded by PCC Admin", + status_or_notes="Forwarded to Director", + ) + + _create_audit( + "PCC Forwarded Application", + request.user, + application, + f"Forwarded to director with attorney {attorney.name}", + ) + _notify( + application, + f"Application {application.title} has been forwarded to the Director.", + recipient_role="Director", + event_type="Status Update", + ) + return JsonResponse({ "message": "Application forwarded to director.", "application_id": application.id, "new_status": application.status, "forwarded_to_director_date": application.forwarded_to_director_date, + "assigned_pcc_admin": request.user.get_full_name() or request.user.username, "attorney_id": attorney.id, "attorney_name": attorney.name, "comments": comments @@ -525,6 +676,12 @@ def forward_application(request, application_id): def request_application_modification(request, application_id): if request.method == "POST": try: + if not _is_pcc_admin_user(request.user): + return JsonResponse( + {"error": "Only PCC Admin can request application modification."}, + status=403, + ) + # Validate if application_id is provided if not application_id: return JsonResponse({"error": "Application ID is required."}, status=400) @@ -539,32 +696,62 @@ def request_application_modification(request, application_id): if application.status == "Draft": return JsonResponse({"message": "Application is already in Draft status."}, status=400) + allowed_statuses = [ + "Submitted", + "Reviewed by PCC Admin", + "Forwarded for Director's Review", + "Returned to Director", + ] + if application.status not in allowed_statuses: + return JsonResponse( + { + "error": ( + "Modification can be requested only before detailed patent processing starts. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + # Parse the request body for comments try: data = json.loads(request.body) - comments = data.get("comments", "").strip() # Remove leading/trailing whitespace + comments = _require_comments(data) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON body."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + # Move to revision state and notify applicant. + application.status = "Needs Revision" + application.revision_requested_at = now() + application.revision_due_date = (now() + timedelta(days=60)).date() + application.is_revision_locked = False + application.comments = comments + application.assigned_pcc_admin = request.user + application.save() - # Validate comments field - if not comments: - return JsonResponse({"error": "Comments are required."}, status=400) - if len(comments) > 1000: - return JsonResponse({"error": "Comments too long. Maximum 1000 characters allowed."}, status=400) - - # Update application fields - application.status = "Draft" - application.decision_date = now() - application.decision_status = "Draft" - application.comments = comments - application.save() + applicant_user = ( + application.primary_applicant.user + if application.primary_applicant and application.primary_applicant.user_id + else None + ) + _notify( + application, + "PCC Admin requested modifications. Please update and resubmit.", + recipient=applicant_user, + recipient_role="Applicant", + event_type="Status Update", + due_date=application.revision_due_date, + ) # Return a success response return JsonResponse({ - "message": "Application status updated to 'Draft'.", + "message": "Application status updated to 'Needs Revision'.", "application_id": application.id, "new_status": application.status, "last_updated_at": application.last_updated_at, + "revision_due_date": application.revision_due_date, "comments": comments, }) @@ -580,53 +767,61 @@ def request_application_modification(request, application_id): @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) def ongoing_applications(request): - REVIEW_STATUSES = [ - "Forwarded for Director's Review", - "Director's Approval Received", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Search Report Generated", - "Patent Filed", - "Patent Published", - "Patent Filed", - "Patent Published", - ] - - applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") - - - application_dict = {} # Using a dictionary instead of a list - - for app in applications: - applicant = app.primary_applicant # Get the Applicant instance - - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None + try: + REVIEW_STATUSES = [ + "Forwarded for Director's Review", + "Director's Approval Received", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Search Report Generated", + "Patent Filed", + "Patent Published", + "Patent Filed", + "Patent Published", + ] - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() + applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() - designation_name = holds_designation.designation.name if holds_designation else "Unknown" + application_dict = {} # Using a dictionary instead of a list - # Format response as a dictionary - application_dict[app.id] = { - "token_no": app.token_no if app.token_no else "Token not generated yet", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", - "status": app.status, - } + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance + + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None + + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" + + # Format response as a dictionary + application_dict[app.id] = { + "token_no": app.token_no if app.token_no else "Token not generated yet", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", + "status": app.status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue - return JsonResponse({"applications": application_dict}, safe=False) + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in ongoing_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) @api_view(['POST']) @permission_classes([IsAuthenticated]) @@ -643,6 +838,8 @@ def change_application_status(request, application_id): "Patent Granted", "Patent Refused", ] + # Normalize status strings to protect transitions from stray whitespace in DB/UI payloads. + normalized_statuses = [status.strip() for status in REVIEW_STATUSES] if request.method == "POST": try: # Validate if application_id is provided @@ -665,18 +862,72 @@ def change_application_status(request, application_id): # Validate next_status field if not next_status: return JsonResponse({"error": "next_status is required."}, status=400) - if next_status not in REVIEW_STATUSES: - return JsonResponse({"error": f"Invalid next_status. Allowed statuses: {REVIEW_STATUSES}"}, status=400) + if next_status not in normalized_statuses: + return JsonResponse({"error": f"Invalid next_status. Allowed statuses: {normalized_statuses}"}, status=400) + + if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: + owner = application.assigned_pcc_admin + owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") + return JsonResponse( + { + "error": f"Application is assigned to another PCC Admin: {owner_name}.", + "assigned_pcc_admin": owner_name, + }, + status=403, + ) # Check if the current status allows transitioning to the next status - current_status_index = REVIEW_STATUSES.index(application.status) if application.status in REVIEW_STATUSES else -1 - next_status_index = REVIEW_STATUSES.index(next_status) + current_status = (application.status or "").strip() + current_status_index = normalized_statuses.index(current_status) if current_status in normalized_statuses else -1 + next_status_index = normalized_statuses.index(next_status) + + if current_status_index == -1: + return JsonResponse( + { + "error": ( + "Current application status is not in ongoing workflow states. " + f"Current status: {application.status}" + ) + }, + status=400, + ) - # if next_status_index != current_status_index + 1: - # return JsonResponse({ - # "error": f"Invalid status transition. Current status: '{application.status}', " - # f"allowed next status: '{REVIEW_STATUSES[current_status_index + 1]}'" if current_status_index + 1 < len(REVIEW_STATUSES) else "None" - # }, status=400) + if next_status == "Patent Refused": + if current_status in ["Patent Granted", "Patent Refused"]: + return JsonResponse( + { + "error": ( + f"Invalid status transition from '{current_status}' to '{next_status}'. " + "The application is already in a terminal decision state." + ) + }, + status=400, + ) + + application.status = next_status + application.patent_refused_date = now() + application.decision_status = "Rejected" + application.decision_date = now() + application.save() + + return JsonResponse({ + "message": f"Application status updated to '{next_status}'.", + "application_id": application.id, + "new_status": application.status, + "last_updated_at": application.last_updated_at, + }) + + if next_status_index != current_status_index + 1: + allowed_next = normalized_statuses[current_status_index + 1] if current_status_index + 1 < len(normalized_statuses) else None + return JsonResponse( + { + "error": ( + f"Invalid status transition from '{current_status}' to '{next_status}'. " + f"Allowed next status: '{allowed_next}'." + ) + }, + status=400, + ) # Update application status and save application.status = next_status @@ -720,43 +971,51 @@ def change_application_status(request, application_id): @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) def past_applications(request): - DECISION_STATUSES = [ - "Approved", - "Rejected", - ] - - applications = Application.objects.filter(decision_status__in=DECISION_STATUSES).select_related("primary_applicant") - - application_dict = {} # Using a dictionary instead of a list - - for app in applications: - applicant = app.primary_applicant # Get the Applicant instance - - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None - - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() + try: + DECISION_STATUSES = [ + "Approved", + "Rejected", + ] - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + applications = Application.objects.filter(decision_status__in=DECISION_STATUSES).select_related("primary_applicant") - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() - designation_name = holds_designation.designation.name if holds_designation else "Unknown" + application_dict = {} # Using a dictionary instead of a list - # Format response as a dictionary - application_dict[app.id] = { - "token_no": app.token_no if app.token_no else "Token not generated yet", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", - "decision_status": app.decision_status, - } + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance + + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None + + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" + + # Format response as a dictionary + application_dict[app.id] = { + "token_no": app.token_no if app.token_no else "Token not generated yet", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", + "decision_status": app.decision_status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue - return JsonResponse({"applications": application_dict}, safe=False) + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in past_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) @api_view(['GET']) @permission_classes([IsAuthenticated]) @@ -771,11 +1030,11 @@ def view_application_details_for_pccAdmin(request, application_id): primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() primary_applicant_name = primary_applicant.name if primary_applicant else None # Get primary applicant name safely - # Fetch attorney details using attorney_id - attorney_name = None - if application.attorney_id: - attorney = Attorney.objects.filter(id=application.attorney_id).first() - attorney_name = attorney.name if attorney else None # Get attorney name safely + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username + + attorney_name = application.attorney.name if application.attorney else None # Fetch associated applicants associated_applicants = AssociatedWith.objects.filter(application=application) @@ -834,7 +1093,9 @@ def view_application_details_for_pccAdmin(request, application_id): "primary_applicant_name": primary_applicant_name, "title": application.title, "status": application.status, + "assigned_pcc_admin": handler_name, "attorney_name": attorney_name, + "communication_logs": CommunicationLogSerializer(application.communication_logs.all(), many=True).data, "dates": { "submitted_date": application.submitted_date if application.submitted_date else None, "reviewed_by_pcc_date": application.reviewed_by_pcc_date, @@ -861,41 +1122,164 @@ def view_application_details_for_pccAdmin(request, application_id): # 🔹 Director Views # ----------------------------------------- +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def attorney_forward_to_director(request, app_id): + if not _is_attorney_user(request.user): + return JsonResponse({"error": "Only Attorney users can forward applications to Director."}, status=403) + + application = get_object_or_404(Application, id=app_id) + attorney = _get_attorney_for_user(request.user) + if not attorney: + return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) + + if application.attorney_id != attorney.id: + return JsonResponse({"error": "This application is not assigned to the current attorney."}, status=403) + + allowed_statuses = {"Attorney Assigned", "Needs Revision", "Returned to Director"} + if application.status not in allowed_statuses: + return JsonResponse( + { + "error": ( + "Application must be in one of the statuses " + f"{sorted(allowed_statuses)}. Current status: {application.status}" + ) + }, + status=400, + ) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + + try: + comments = _require_comments(data, key="comments") + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + application.attorney_review_notes = comments + application.attorney_reviewed_at = now() + application.status = "Returned to Director" + application.decision_status = "Reviewed by Attorney" + application.save() + + _create_audit( + "Attorney Forwarded Application", + request.user, + application, + f"Attorney {attorney.name} forwarded application back to director", + ) + _notify( + application, + f"Attorney completed the assessment for {application.title} and returned it to Director.", + recipient_role="Director", + event_type="Status Update", + ) + + return JsonResponse( + { + "message": "Application returned to Director.", + "application_id": application.id, + "new_status": application.status, + "comments": comments, + } + ) + + @api_view(['GET']) @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) -def director_new_applications(request): - applications = Application.objects.filter( - status="Forwarded for Director's Review" - ).select_related("primary_applicant", "attorney") +def attorney_applications(request): + if not _is_attorney_user(request.user): + return JsonResponse({"error": "Only Attorney users can access this queue."}, status=403) + + attorney = _get_attorney_for_user(request.user) + if not attorney: + return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) + + applications = ( + Application.objects.filter( + attorney=attorney, + status__in=["Attorney Assigned", "Returned to Director", "Needs Revision"], + ) + .select_related("primary_applicant", "attorney") + .order_by("-last_updated_at") + ) + + payload = [] + for application in applications: + ui_status = ( + "Needs Revision" + if application.status == "Needs Revision" or application.decision_status == "Needs Revision" + else application.status + ) - application_dict = {} + payload.append( + { + "application_id": application.id, + "title": application.title, + "status": application.status, + "ui_status": ui_status, + "decision_status": application.decision_status, + "token_no": application.token_no, + "comments": application.comments, + "attorney_review_notes": application.attorney_review_notes, + "attorney_reviewed_at": application.attorney_reviewed_at, + "applicant_name": application.primary_applicant.name if application.primary_applicant else None, + "submitted_date": application.submitted_date, + } + ) - for app in applications: - applicant = app.primary_applicant - user = applicant.user if applicant else None + return JsonResponse({"applications": payload}, safe=False) - # Get department name from ExtraInfo - extra_info = ExtraInfo.objects.filter(user=user).first() - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - # Get attorney name using foreign key - assigned_attorney = app.attorney.name if app.attorney else "Not Assigned" +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_new_applications(request): + try: + applications = Application.objects.filter( + status__in=["Forwarded for Director's Review", "Returned to Director"] + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") - # Unique key for dictionary - key = app.id + application_dict = {} - # Build the application summary - application_dict[key] = { - "token_no": app.token_no if app.token_no else "Token not generated", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "department": department_name, - "forwarde_on": app.forwarded_to_director_date.strftime("%Y-%m-%d %H:%M:%S") if app.forwarded_to_director_date else "Unknown", - "assigned_attorney": assigned_attorney, - } + for app in applications: + try: + applicant = app.primary_applicant + user = applicant.user if applicant else None + + # Get department name from ExtraInfo + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" + assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" + + # Unique key for dictionary + key = app.id + + # Build the application summary + application_dict[key] = { + "token_no": app.token_no if app.token_no else "Token not generated", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "department": department_name, + "forwarded_on": app.forwarded_to_director_date.strftime("%Y-%m-%d") if app.forwarded_to_director_date else "Unknown", + "assigned_pcc_admin": assigned_pcc_admin, + "assigned_attorney": assigned_attorney, + "current_status": app.status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue - return JsonResponse({"applications": application_dict}, safe=False) + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in director_new_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) @api_view(['POST']) @permission_classes([IsAuthenticated]) @@ -905,6 +1289,7 @@ def director_reject(request): try: data = json.loads(request.body) application_id = data.get("application_id") + comments = _require_comments(data) if not application_id: return JsonResponse({"error": "Application ID is required."}, status=400) @@ -914,19 +1299,56 @@ def director_reject(request): except Application.DoesNotExist: return JsonResponse({"error": "Application not found."}, status=404) - application.status = "Rejected" + if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: + return JsonResponse( + { + "error": ( + "Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status to reject. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + + application.comments = comments + # Director rejection returns the application to PCC Admin for the next routing decision. + application.status = "Reviewed by PCC Admin" application.decision_date = now() - application.decision_status = "Rejected" + application.decision_status = "Needs Revision" + application.is_revision_locked = True application.save() + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + message_content=comments, + status_or_notes="Director requested revision", + ) + + _create_audit( + 'Director Rejected Application', + request.user, + application, + f'Director requested PCC Admin revision routing. Comments: {comments}', + ) + _notify( + application, + "Director requested revisions. PCC Admin will route feedback to the applicant.", + recipient_role="PCC Admin", + event_type="Status Update", + ) + return JsonResponse({ - "message": "Application status updated to Rejected", + "message": "Application sent back to PCC Admin for further action", "application_id": application.id, - "new_status": application.status + "new_status": application.status, + "comments": comments, }) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) return JsonResponse({"error": "Only POST requests are allowed."}, status=405) @@ -938,8 +1360,7 @@ def director_accept(request): try: data = json.loads(request.body) application_id = data.get("application_id") - attorney_id = data.get("attorney_id") - comments = data.get("comments", "") + comments = _require_comments(data) # Validate required fields if not application_id: @@ -951,21 +1372,11 @@ def director_accept(request): return JsonResponse({"error": "Application not found."}, status=404) # Status check - if application.status != "Forwarded for Director's Review": + if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: return JsonResponse({ - "error": f"Application must be in 'Forwarded for Director's Review' status. Current status: {application.status}" + "error": f"Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status. Current status: {application.status}" }, status=400) - # Process attorney assignment - attorney = None - if attorney_id: - try: - attorney = Attorney.objects.get(id=attorney_id) - if not application.attorney or application.attorney.id != attorney.id: - application.attorney = attorney - except Attorney.DoesNotExist: - return JsonResponse({"error": f"Attorney with ID '{attorney_id}' not found."}, status=404) - # Get department name using your provided logic applicant = application.primary_applicant user = applicant.user if applicant else None @@ -981,10 +1392,10 @@ def director_accept(request): # Generate reference number components app_id_part = f"{application.id:06d}" # 6-digit format - attorney_initials = ( - attorney.name.replace(" ", "")[:3].upper() - if attorney - else "XXX" + handler_initials = ( + (application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username).replace(" ", "")[:3].upper() + if application.assigned_pcc_admin + else "PCA" ) # Generate serial number (example implementation - adjust as needed) @@ -999,34 +1410,54 @@ def director_accept(request): f"{department_name}/" f"{submitted_date}/" f"{app_id_part}/" - f"{attorney_initials}/" + f"{handler_initials}/" f"{serial_number:03d}" # 3-digit serial number ) # Update application fields - if comments: - if len(comments) > 1000: - return JsonResponse({"error": "Comments too long. Max 1000 characters allowed."}, status=400) - application.comments = comments + application.comments = comments + # After director approval, application returns to PCC Admin workflow stage. application.status = "Director's Approval Received" - application.decision_date = now() - application.decision_status = "Director's Approval Received" + application.director_approval_date = now() + application.decision_status = "Pending" application.token_no = token_no application.save() + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + message_content=comments, + status_or_notes="Director approved and forwarded", + ) + + _create_audit( + 'Director Approved Application', + request.user, + application, + f'Director approved application and returned it to PCC Admin stage. Comments: {comments}', + ) + _notify( + application, + f"Director approved application {application.title}.", + recipient_role="PCC Admin", + event_type="Status Update", + ) + return JsonResponse({ "message": "Director's Approval Received", "application_id": application.id, "new_status": application.status, "token_no": token_no, - "attorney_id": application.attorney.id if application.attorney else None, + "assigned_pcc_admin": application.assigned_pcc_admin.get_full_name() if application.assigned_pcc_admin else None, "attorney_name": application.attorney.name if application.attorney else None, "comments": comments }) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) except Exception as e: return JsonResponse({"error": str(e)}, status=500) @@ -1039,6 +1470,8 @@ def director_reviewed_applications(request): # Define the list of statuses to include reviewed_statuses = [ "Director's Approval Received", + "Attorney Assigned", + "Returned to Director", "Patentability Check Started", "Patentability Check Completed", "Patentability Search Report Generated", @@ -1050,7 +1483,7 @@ def director_reviewed_applications(request): applications = Application.objects.filter( status__in=reviewed_statuses - ).select_related("primary_applicant", "attorney") + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") application_dict = {} @@ -1062,8 +1495,8 @@ def director_reviewed_applications(request): extra_info = ExtraInfo.objects.filter(user=user).first() department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - # Get attorney name using foreign key - assigned_attorney = app.attorney.name if app.attorney else "Not Assigned" + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" + assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" # Unique key for dictionary key = app.id @@ -1076,6 +1509,7 @@ def director_reviewed_applications(request): "department": department_name, "arrival_date": app.forwarded_to_director_date if app.submitted_date else "Unknown", "reviewed_date": app.decision_date if app.decision_date else "Unknown", + "assigned_pcc_admin": assigned_pcc_admin, "assigned_attorney": assigned_attorney, "current_status": app.status, } @@ -1089,6 +1523,8 @@ def active_applications(request): # Define statuses relevant to active applications active_statuses = [ "Director's Approval Received", + "Attorney Assigned", + "Returned to Director", "Patentability Check Started", "Patentability Check Completed", "Patentability Search Report Generated", @@ -1101,7 +1537,7 @@ def active_applications(request): applications = Application.objects.filter( status__in=active_statuses, decision_status="Pending" - ).select_related("primary_applicant", "attorney") + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") application_dict = {} @@ -1113,8 +1549,7 @@ def active_applications(request): extra_info = ExtraInfo.objects.filter(user=user).first() department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - # Get attorney name using foreign key - assigned_attorney = app.attorney.name if app.attorney else "Not Assigned" + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" # Unique key for dictionary key = str(app.token_no) if app.token_no else f"app_{app.id}" @@ -1126,7 +1561,8 @@ def active_applications(request): "submitted_by": applicant.name if applicant else "Unknown", "department": department_name, "submitted_on": app.submitted_date if app.submitted_date else "Unknown", - "assigned_attorney": assigned_attorney, + "assigned_pcc_admin": assigned_pcc_admin, + "assigned_attorney": assigned_pcc_admin, "current_status": app.status, } @@ -1136,7 +1572,11 @@ def active_applications(request): @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) def director_notifications(request): - return JsonResponse({"notifications": ["New submission", "Pending review"]}) + notifications = NotificationEvent.objects.filter( + Q(recipient=request.user) | Q(recipient__isnull=True) | Q(recipient_role__iexact="Director") + ).order_by("-created_at") + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) @api_view(['POST']) @permission_classes([IsAuthenticated]) @@ -1161,11 +1601,11 @@ def director_application_view(request): primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() primary_applicant_name = primary_applicant.name if primary_applicant else None - # Fetch attorney details - attorney_name = None - if application.attorney_id: - attorney = Attorney.objects.filter(id=application.attorney_id).first() - attorney_name = attorney.name if attorney else None + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username + + attorney_name = application.attorney.name if application.attorney else None # Fetch associated applicants associated_applicants = AssociatedWith.objects.filter(application=application) @@ -1224,6 +1664,7 @@ def director_application_view(request): "primary_applicant_name": primary_applicant_name, "title": application.title, "status": application.status, + "assigned_pcc_admin": handler_name, "attorney_name": attorney_name, "dates": { "submitted_date": application.submitted_date, @@ -1252,6 +1693,26 @@ def director_application_view(request): except Exception as e: return JsonResponse({'error': str(e)}, status=500) + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def communication_logs(request, application_id): + application = get_object_or_404(Application, id=application_id) + + if request.method == 'GET': + logs = application.communication_logs.select_related('logged_by').all() + serializer = CommunicationLogSerializer(logs, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + payload = request.data.copy() + serializer = CommunicationLogSerializer(data=payload) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + serializer.save(application=application, logged_by=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + # ----------------------------------------- # 🔹 PCC Admin Attorney Management Views # ----------------------------------------- @@ -1399,4 +1860,1024 @@ def delete_document(request, document_id): return Response( {'error': 'Failed to delete document'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) \ No newline at end of file + ) + + +def _role_names(user): + names = set( + HoldsDesignation.objects.filter(user=user).values_list("designation__name", flat=True) + ) + return {name.lower() for name in names if name} + + +def _is_pcc_admin_user(user): + role_names = _role_names(user) + return any("pcc" in role and "admin" in role for role in role_names) + + +def _is_director_user(user): + role_names = _role_names(user) + return any("director" in role for role in role_names) + + +def _is_authorized_applicant_user(user): + role_names = _role_names(user) + allowed_roles = { + "student", + "alumini", + "professor", + "associate professor", + "assistant professor", + "research engineer", + "faculty", + } + return bool(role_names & allowed_roles) or Applicant.objects.filter(user=user).exists() + + +def _get_attorney_for_user(user): + if not user or not user.email: + return None + attorney = Attorney.objects.filter(email__iexact=user.email).first() + if attorney: + return attorney + full_name = user.get_full_name().strip() + if full_name: + attorney = Attorney.objects.filter(name__iexact=full_name).first() + return attorney + + +def _is_attorney_user(user): + role_names = _role_names(user) + if any("attorney" in role for role in role_names): + return True + return _get_attorney_for_user(user) is not None + + +def _require_comments(payload, key="comments"): + comments = (payload.get(key) or "").strip() + if not comments: + raise ValueError("Comments are required.") + if len(comments) > 1000: + raise ValueError("Comments too long. Max 1000 characters allowed.") + return comments + + +def _create_audit(action, actor, application=None, details=""): + AuditLog.objects.create(action=action, actor=actor, application=application, details=details) + + +def _notify(application, message, recipient=None, recipient_role=None, event_type="General", due_date=None): + NotificationEvent.objects.create( + application=application, + recipient=recipient, + recipient_role=recipient_role, + event_type=event_type, + message=message, + due_date=due_date, + ) + + +def _reviewer_workload(user): + return Application.objects.filter(assigned_pcc_admin=user, status__in=["Submitted", "Reviewed by PCC Admin", "Needs Revision"]).count() + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def withdraw_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only primary applicant can withdraw.'}, status=status.HTTP_403_FORBIDDEN) + if application.status in ["Patent Granted", "Patent Refused"]: + return Response({'error': 'Cannot withdraw after final decision.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Withdrawn" + application.save(update_fields=['status', 'last_updated_at']) + _create_audit('Withdraw Application', request.user, application, 'Applicant withdrew application') + return Response({'message': 'Application withdrawn successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def resubmit_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id + is_pcc = _is_pcc_admin_user(request.user) + if not (is_owner or is_pcc): + return Response({'error': 'Only primary applicant can resubmit.'}, status=status.HTTP_403_FORBIDDEN) + if application.status != "Needs Revision": + return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) + if application.revision_due_date and timezone.now().date() > application.revision_due_date: + application.status = "Revision Expired" + application.save(update_fields=['status', 'last_updated_at']) + return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Submitted" + application.revised_submitted_at = timezone.now() + application.is_revision_locked = True + application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) + _create_audit('Resubmit Application', request.user, application, 'Applicant resubmitted revised application') + return Response({'message': 'Application resubmitted successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def declare_conflict(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can declare conflict.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = ConflictDeclarationSerializer(data={ + 'application': application.id, + 'reviewer': request.user.id, + 'conflict_type': request.data.get('conflict_type', 'General Conflict'), + 'notes': request.data.get('notes', ''), + 'declaration_status': 'Declared', + }) + if serializer.is_valid(): + serializer.save() + application.status = "Submitted" + application.assigned_pcc_admin = None + application.save(update_fields=['status', 'assigned_pcc_admin', 'last_updated_at']) + _create_audit('Conflict Declared', request.user, application, serializer.validated_data.get('conflict_type', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_legal_assessment(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: + return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) + attorney_id = request.data.get('attorney') + attorney = get_object_or_404(Attorney, id=attorney_id) + serializer = LegalAssessmentSerializer(data={ + 'application': application.id, + 'attorney': attorney.id, + 'opinion': request.data.get('opinion', 'Review Needed'), + 'prior_art_summary': request.data.get('prior_art_summary', ''), + 'recommended_action': request.data.get('recommended_action', ''), + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_legal_advice_memo(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = LegalAdviceMemoSerializer(data={ + 'application': application.id, + 'author': request.user.id, + 'summary': request.data.get('summary', ''), + 'recommendation': request.data.get('recommendation', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Memo Submitted', request.user, application, 'Memo added') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def initiate_budget_approval(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + amount = Decimal(str(request.data.get('amount', '0'))) + threshold = Decimal(str(request.data.get('threshold', '50000'))) + serializer = BudgetApprovalSerializer(data={ + 'application': application.id, + 'requested_by': request.user.id, + 'amount': amount, + 'threshold': threshold, + 'status': 'Pending', + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + application.budget_status = 'Pending Approval' + application.budget_estimate = amount + application.save(update_fields=['budget_status', 'budget_estimate', 'last_updated_at']) + _notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') + _create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_decide_budget(request, app_id): + if not _is_director_user(request.user): + return Response({'error': 'Only Director can decide budget approval.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + budget = BudgetApproval.objects.filter(application=application).order_by('-created_at').first() + if not budget: + return Response({'error': 'No budget request found.'}, status=status.HTTP_404_NOT_FOUND) + decision = request.data.get('decision', '').strip().lower() + if decision not in ['approve', 'reject']: + return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) + budget.status = 'Approved' if decision == 'approve' else 'Rejected' + budget.decided_by = request.user + budget.decided_at = timezone.now() + budget.comments = request.data.get('comments', budget.comments) + budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) + application.budget_status = budget.status + application.save(update_fields=['budget_status', 'last_updated_at']) + _create_audit('Budget Decision', request.user, application, budget.status) + return Response({'message': f'Budget {budget.status.lower()} successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def initiate_external_filing(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = ExternalFilingRecordSerializer(data={ + 'application': application.id, + 'patent_office': request.data.get('patent_office', ''), + 'filing_reference': request.data.get('filing_reference', ''), + 'communication_notes': request.data.get('communication_notes', ''), + 'filed_by': request.user.id, + 'filing_date': request.data.get('filing_date'), + }) + if serializer.is_valid(): + serializer.save() + application.external_filing_status = 'Filed' + if application.status == 'Patentability Search Report Generated': + application.status = 'Patent Filed' + application.patent_filed_date = timezone.now().date() + application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) + _create_audit('External Filing Initiated', request.user, application, 'External filing recorded') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def add_office_action(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = OfficeActionSerializer(data={ + 'application': application.id, + 'office_name': request.data.get('office_name', ''), + 'action_reference': request.data.get('action_reference', ''), + 'action_summary': request.data.get('action_summary', ''), + 'due_date': request.data.get('due_date'), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def respond_office_action(request, office_action_id): + office_action = get_object_or_404(OfficeAction, id=office_action_id) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can respond office action.'}, status=status.HTTP_403_FORBIDDEN) + serializer = OfficeActionResponseSerializer(data={ + 'office_action': office_action.id, + 'responder': request.user.id, + 'response_text': request.data.get('response_text', ''), + 'response_reference': request.data.get('response_reference', ''), + }) + if serializer.is_valid(): + serializer.save() + office_action.status = 'Responded' + office_action.save(update_fields=['status']) + _create_audit('Office Action Responded', request.user, office_action.application, office_action.action_reference) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def add_prior_art_reference(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = PriorArtReferenceSerializer(data={ + 'application': application.id, + 'reference_type': request.data.get('reference_type', ''), + 'citation': request.data.get('citation', ''), + 'notes': request.data.get('notes', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_appeal(request, app_id): + application = get_object_or_404(Application, id=app_id) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) + serializer = AppealRequestSerializer(data={ + 'application': application.id, + 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), + 'grounds': request.data.get('grounds', ''), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') + _create_audit('Appeal Submitted', request.user, application, 'Appeal opened') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_licensing_request(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = LicensingRequestSerializer(data={ + 'application': application.id, + 'requester_name': request.data.get('requester_name', ''), + 'requester_org': request.data.get('requester_org', ''), + 'request_details': request.data.get('request_details', ''), + 'status': 'Pending', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def collect_inventor_consents(request, app_id): + application = get_object_or_404(Application, id=app_id) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) + applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] + created = [] + for applicant in applicants: + if not applicant: + continue + consent, _ = InventorConsent.objects.get_or_create( + application=application, + applicant=applicant, + defaults={ + 'consent_given': False, + 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), + }, + ) + created.append(consent) + serializer = InventorConsentSerializer(created, many=True) + _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') + return Response(serializer.data) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def setup_maintenance_schedule(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + due_date = request.data.get('due_date') + amount = request.data.get('amount') + serializer = MaintenanceScheduleSerializer(data={ + 'application': application.id, + 'due_date': due_date, + 'amount': amount, + 'status': 'Upcoming', + }) + if serializer.is_valid(): + serializer.save() + application.maintenance_tracking_active = True + application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) + _create_audit('Maintenance Schedule Created', request.user, application, f'Due: {due_date}') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def mark_maintenance_paid(request, schedule_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can mark maintenance paid.'}, status=status.HTTP_403_FORBIDDEN) + schedule = get_object_or_404(MaintenanceSchedule, id=schedule_id) + schedule.status = 'Paid' + schedule.paid_at = timezone.now() + schedule.save(update_fields=['status', 'paid_at']) + _create_audit('Maintenance Paid', request.user, schedule.application, f'Schedule {schedule.id}') + return Response({'message': 'Maintenance marked as paid.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def reviewer_queue(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) + apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') + payload = [] + for app in apps: + score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 + app.priority_score = 10 if app.status == 'Needs Revision' else 5 + app.save(update_fields=['priority_score', 'last_updated_at']) + payload.append({ + 'id': app.id, + 'title': app.title, + 'status': app.status, + 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, + 'priority_score': app.priority_score, + 'reviewer_workload': score, + }) + payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) + return Response(payload) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_notifications(request): + role = (request.GET.get("role") or "").strip() + notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) + if role: + notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) + notifications = notifications.order_by('-created_at') + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def mark_notification_read(request, notification_id): + notification = get_object_or_404(NotificationEvent, id=notification_id) + if notification.recipient_id and notification.recipient_id != request.user.id: + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + notification.is_read = True + notification.save(update_fields=['is_read']) + return Response({'message': 'Notification marked as read.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_audit_logs(request): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.select_related('actor', 'application').all()[:300] + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_applicant_insights(request): + applicant = Applicant.objects.filter(user=request.user).first() + if not applicant: + return Response({'error': 'Applicant profile not found.'}, status=status.HTTP_404_NOT_FOUND) + apps = Application.objects.filter(primary_applicant=applicant) + summary = { + 'total': apps.count(), + 'draft': apps.filter(status='Draft').count(), + 'submitted': apps.filter(status='Submitted').count(), + 'under_review': apps.filter(status__in=['Reviewed by PCC Admin', "Forwarded for Director's Review"]).count(), + 'approved': apps.filter(status="Director's Approval Received").count(), + 'granted': apps.filter(status='Patent Granted').count(), + 'refused': apps.filter(status='Patent Refused').count(), + } + return Response(summary) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_insights(request): + if not _is_director_user(request.user): + return Response({'error': 'Only Director can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) + return _insights_response(request) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def upload_document_version(request, document_id): + document = get_object_or_404(Document, id=document_id) + if document.is_locked: + return Response({'error': 'Document is locked for new versions.'}, status=status.HTTP_400_BAD_REQUEST) + link = request.data.get('link') + if not link: + return Response({'error': 'link is required.'}, status=status.HTTP_400_BAD_REQUEST) + next_version = (document.versions.first().version_number + 1) if document.versions.exists() else document.current_version + 1 + serializer = DocumentVersionSerializer(data={ + 'document': document.id, + 'version_number': next_version, + 'link': link, + 'uploaded_by': request.user.id, + }) + if serializer.is_valid(): + serializer.save() + document.current_version = next_version + document.link = link + document.save(update_fields=['current_version', 'link', 'updated_at']) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def lock_document(request, document_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can lock documents.'}, status=status.HTTP_403_FORBIDDEN) + document = get_object_or_404(Document, id=document_id) + document.is_locked = True + document.save(update_fields=['is_locked', 'updated_at']) + return Response({'message': 'Document locked successfully.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def pcc_insights(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) + return _insights_response(request) + + +def _insights_response(request): + base_qs = Application.objects.all() + available_years = sorted( + { + dt.year + for dt in base_qs.exclude(submitted_date__isnull=True).values_list('submitted_date', flat=True) + if dt + } + ) + if not available_years: + available_years = [timezone.now().year] + + requested_year = request.GET.get('year') + if requested_year and requested_year.isdigit() and int(requested_year) in available_years: + selected_year = int(requested_year) + else: + selected_year = available_years[-1] + + apps = base_qs.filter(submitted_date__year=selected_year) + color_map = { + 'Submitted': '#4D96FF', + 'Reviewed by PCC Admin': '#00B894', + "Forwarded for Director's Review": '#F5A623', + "Director's Approval Received": '#2ECC71', + 'Patent Filed': '#8E44AD', + 'Patent Granted': '#16A085', + 'Patent Refused': '#E74C3C', + 'Needs Revision': '#D35400', + 'Withdrawn': '#7F8C8D', + } + status_order = [ + 'Submitted', + 'Reviewed by PCC Admin', + "Forwarded for Director's Review", + "Director's Approval Received", + 'Patent Filed', + 'Patent Granted', + 'Patent Refused', + 'Needs Revision', + 'Withdrawn', + ] + + status_counts = apps.values('status').annotate(count=Count('id')) + status_lookup = {item['status']: item['count'] for item in status_counts} + applications = [ + { + 'label': status_name, + 'count': status_lookup.get(status_name, 0), + 'color': color_map.get(status_name, '#5B7CFA'), + } + for status_name in status_order + ] + + total = sum(item['count'] for item in applications) + payload = { + 'applications': applications, + 'available_years': available_years, + 'selected_year': selected_year, + 'total': total, + } + + if request.GET.get('format') == 'csv': + rows = ['status,count,percentage'] + for item in applications: + percentage = (item['count'] / total * 100) if total else 0 + rows.append(f"{item['label']},{item['count']},{percentage:.2f}") + return HttpResponse('\n'.join(rows), content_type='text/csv') + + return Response(payload) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def pcc_resubmit_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id + is_pcc = _is_pcc_admin_user(request.user) + if not (is_owner or is_pcc): + return Response({'error': 'Only primary applicant or PCC Admin can resubmit.'}, status=status.HTTP_403_FORBIDDEN) + if application.status != "Needs Revision": + return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) + if application.revision_due_date and timezone.now().date() > application.revision_due_date: + application.status = "Revision Expired" + application.save(update_fields=['status', 'last_updated_at']) + return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Submitted" + application.revised_submitted_at = timezone.now() + application.is_revision_locked = True + application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) + _create_audit('Resubmit Application', request.user, application, 'Application resubmitted') + return Response({'message': 'Application resubmitted successfully.'}) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def legal_assessment_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LegalAssessmentSerializer(application.legal_assessments.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) + if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: + return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) + attorney = get_object_or_404(Attorney, id=request.data.get('attorney')) + serializer = LegalAssessmentSerializer(data={ + 'application': application.id, + 'attorney': attorney.id, + 'opinion': request.data.get('opinion', 'Review Needed'), + 'prior_art_summary': request.data.get('prior_art_summary', ''), + 'recommended_action': request.data.get('recommended_action', ''), + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def budget_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = BudgetApprovalSerializer(application.budget_approvals.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) + amount = Decimal(str(request.data.get('amount', '0'))) + threshold = Decimal(str(request.data.get('threshold', '50000'))) + serializer = BudgetApprovalSerializer(data={ + 'application': application.id, + 'requested_by': request.user.id, + 'amount': amount, + 'threshold': threshold, + 'status': 'Pending', + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + application.budget_status = 'Pending Approval' + application.budget_estimate = amount + application.save(update_fields=['budget_status', 'budget_estimate', 'last_updated_at']) + _notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') + _create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def budget_decision_by_id(request, budget_id): + budget = get_object_or_404(BudgetApproval, id=budget_id) + if not (_is_director_user(request.user) or _is_pcc_admin_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + decision = request.data.get('decision', '').strip().lower() + if decision not in ['approve', 'reject']: + return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) + budget.status = 'Approved' if decision == 'approve' else 'Rejected' + budget.decided_by = request.user + budget.decided_at = timezone.now() + budget.comments = request.data.get('comments', budget.comments) + budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) + budget.application.budget_status = budget.status + budget.application.save(update_fields=['budget_status', 'last_updated_at']) + _create_audit('Budget Decision', request.user, budget.application, budget.status) + return Response({'message': f'Budget {budget.status.lower()} successfully.'}) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def external_filing_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = ExternalFilingRecordSerializer(application.external_filings.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) + serializer = ExternalFilingRecordSerializer(data={ + 'application': application.id, + 'patent_office': request.data.get('patent_office', ''), + 'filing_reference': request.data.get('filing_reference', ''), + 'communication_notes': request.data.get('communication_notes', ''), + 'filed_by': request.user.id, + 'filing_date': request.data.get('filing_date'), + }) + if serializer.is_valid(): + serializer.save() + application.external_filing_status = 'Filed' + if application.status == 'Patentability Search Report Generated': + application.status = 'Patent Filed' + application.patent_filed_date = timezone.now().date() + application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) + _create_audit('External Filing Initiated', request.user, application, 'External filing recorded') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def maintenance_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = MaintenanceScheduleSerializer(application.maintenance_schedules.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) + serializer = MaintenanceScheduleSerializer(data={ + 'application': application.id, + 'due_date': request.data.get('due_date'), + 'amount': request.data.get('amount'), + 'status': 'Upcoming', + }) + if serializer.is_valid(): + serializer.save() + application.maintenance_tracking_active = True + application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) + _create_audit('Maintenance Schedule Created', request.user, application, f"Due: {request.data.get('due_date')}") + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def queue_prioritized(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) + apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') + payload = [] + for app in apps: + score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 + app.priority_score = 10 if app.status == 'Needs Revision' else 5 + app.save(update_fields=['priority_score', 'last_updated_at']) + payload.append({ + 'id': app.id, + 'title': app.title, + 'status': app.status, + 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, + 'priority_score': app.priority_score, + 'reviewer_workload': score, + }) + payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) + return Response(payload) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def notifications_root(request): + role = (request.GET.get("role") or "").strip() + notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) + if role: + notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) + notifications = notifications.order_by('-created_at') + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def audit_logs_root(request): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.select_related('actor', 'application').all()[:300] + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def audit_logs_by_application(request, application_id): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.filter(application_id=application_id).select_related('actor', 'application').all() + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def office_actions_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = OfficeActionSerializer(application.office_actions.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) + serializer = OfficeActionSerializer(data={ + 'application': application.id, + 'office_name': request.data.get('office_name', ''), + 'action_reference': request.data.get('action_reference', ''), + 'action_summary': request.data.get('action_summary', ''), + 'due_date': request.data.get('due_date'), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def prior_art_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + q = request.GET.get('q', '').strip() + refs = application.prior_art_references.all() + if q: + refs = refs.filter(Q(citation__icontains=q) | Q(notes__icontains=q)) + serializer = PriorArtReferenceSerializer(refs, many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) + serializer = PriorArtReferenceSerializer(data={ + 'application': application.id, + 'reference_type': request.data.get('reference_type', ''), + 'citation': request.data.get('citation', ''), + 'notes': request.data.get('notes', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def appeals_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = AppealRequestSerializer(application.appeals.all(), many=True) + return Response(serializer.data) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) + serializer = AppealRequestSerializer(data={ + 'application': application.id, + 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), + 'grounds': request.data.get('grounds', ''), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') + _create_audit('Appeal Submitted', request.user, application, 'Appeal opened') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def licensing_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LicensingRequestSerializer(application.licensing_requests.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) + serializer = LicensingRequestSerializer(data={ + 'application': application.id, + 'requester_name': request.data.get('requester_name', ''), + 'requester_org': request.data.get('requester_org', ''), + 'request_details': request.data.get('request_details', ''), + 'status': 'Pending', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def inventor_consents_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = InventorConsentSerializer(application.inventor_consents.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) + applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] + created = [] + for applicant in applicants: + if not applicant: + continue + consent, _ = InventorConsent.objects.get_or_create( + application=application, + applicant=applicant, + defaults={ + 'consent_given': False, + 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), + }, + ) + created.append(consent) + serializer = InventorConsentSerializer(created, many=True) + _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') + return Response(serializer.data) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def legal_memos_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LegalAdviceMemoSerializer(application.legal_memos.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) + serializer = LegalAdviceMemoSerializer(data={ + 'application': application.id, + 'author': request.user.id, + 'summary': request.data.get('summary', ''), + 'recommendation': request.data.get('recommendation', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Memo Submitted', request.user, application, 'Memo added') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def document_versions_api(request, document_id): + document = get_object_or_404(Document, id=document_id) + serializer = DocumentVersionSerializer(document.versions.all(), many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/FusionIIIT/assign_roles.py b/FusionIIIT/assign_roles.py new file mode 100644 index 000000000..8e3acdd63 --- /dev/null +++ b/FusionIIIT/assign_roles.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +import os +import sys +import django + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Fusion.settings.development') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +django.setup() + +from applications.globals.models import Designation, HoldsDesignation +from django.contrib.auth.models import User + +try: + # Get the user 23BCS226 + user = User.objects.get(username='23BCS226') + print(f"✓ Found user: {user.username} (ID: {user.id})") + + # Get Director designation + director = Designation.objects.get(name='Director') + print(f"✓ Found Director designation (ID: {director.id})") + + # Create PCC Admin designation if it doesn't exist + pcc_admin, created = Designation.objects.get_or_create( + name='PCC Admin', + defaults={ + 'full_name': 'PCC Admin', + 'type': 'administrative', + 'basic': False + } + ) + print(f"✓ PCC Admin designation {'created' if created else 'already exists'}") + + # Check if user already has a working entry to use + existing_holds = HoldsDesignation.objects.filter(user=user).first() + if existing_holds and existing_holds.working_id: + working_id = existing_holds.working_id + print(f"✓ Using existing working_id: {working_id}") + else: + # Try to find any working record + any_working = HoldsDesignation.objects.filter(working_id__isnull=False).first() + if any_working: + working_id = any_working.working_id + print(f"✓ Using working_id from another record: {working_id}") + else: + # If no working_id found, create a default one + working_id = 1 + print(f"✓ Using default working_id: {working_id}") + + # Assign Director role + director_hold, dir_created = HoldsDesignation.objects.get_or_create( + user=user, + designation=director, + defaults={'working_id': working_id} + ) + print(f"✓ Director role {'assigned' if dir_created else 'already assigned'}") + + # Assign PCC Admin role + pcc_hold, pcc_created = HoldsDesignation.objects.get_or_create( + user=user, + designation=pcc_admin, + defaults={'working_id': working_id} + ) + print(f"✓ PCC Admin role {'assigned' if pcc_created else 'already assigned'}") + + print("\n✅ All roles successfully assigned to 23BCS226!") + print(f" - Director: {director.id}") + print(f" - PCC Admin: {pcc_admin.id}") + +except User.DoesNotExist: + print("❌ Error: User 23BCS226 not found") + sys.exit(1) +except Designation.DoesNotExist as e: + print(f"❌ Error: Designation not found - {e}") + sys.exit(1) +except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) diff --git a/docker-compose.yml b/docker-compose.yml index 62dfc8c39..7a72b3810 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: db: - image: postgres:13-alpine + image: postgres:15-alpine restart: always volumes: - ~/private/var/lib/postgresql:/var/lib/postgresql/data diff --git a/tr patent b/tr patent new file mode 100644 index 000000000..1250959dd --- /dev/null +++ b/tr patent @@ -0,0 +1,314 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-m_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + ^O^O Open the currently selected OSC8 hyperlink. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + #_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k _f_i_l_e ... --lesskey-file=_f_i_l_e + Use a compiled lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n ......... --line-numbers + Suppress line numbers in prompts and messages. + -N ......... --LINE-NUMBERS + Display line number at start of each line. + -o [_f_i_l_e] .. --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t _t_a_g .... --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces, tabs and carriage returns. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + + --exit-follow-on-close + Exit F command on a pipe when writer closes pipe. + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --header=[_L[,_C[,_N]]] + Use _L lines (starting at line _N) and _C columns as headers. + --incsearch + Search file as each pattern character is typed in. + --intr=[_C] + Use _C instead of ^X to interrupt a read. + --lesskey-context=_t_e_x_t + Use lesskey source file contents. + --lesskey-src=_f_i_l_e + Use a lesskey source file. + --line-num-width=[_N] + Set the width of the -N line number field to _N characters. + --match-shift=[_N] + Show at least _N characters to the left of a search match. + --modelines=[_N] + Read _N lines from the input file and look for vim modelines. + --mouse + Enable mouse input. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --no-number-headers + Don't give line numbers to header lines. + --no-search-header-lines + Searches do not include header lines. + --no-search-header-columns + Searches do not include header columns. + --no-search-headers + Searches do not include header lines or columns. + --no-vbell + Disable the terminal's visual bell. + --redraw-on-quit + Redraw final screen when quitting. + --rscroll=[_C] + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --search-options=[EFKNRW-] + Set default options for every search. + --show-preproc-errors + Display a message if preprocessor exits with an error status. + --proc-backspace + Process backspaces for bold/underline. + --PROC-BACKSPACE + Treat backspaces as control characters. + --proc-return + Delete carriage returns before newline. + --PROC-RETURN + Treat carriage returns as control characters. + --proc-tab + Expand tabs to spaces. + --PROC-TAB + Treat tabs as control characters. + --status-col-width=[_N] + Set the width of the -J status column to _N characters. + --status-line + Highlight or color the entire line containing a mark. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=[_N] + Each click of the mouse wheel moves _N lines. + --wordwrap + Wrap lines at spaces. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all. From 6fcbc1b7cdd451165e74940cff5c1dbd4a12a021 Mon Sep 17 00:00:00 2001 From: Saumitra-agrahari Date: Sun, 10 May 2026 16:38:58 +0530 Subject: [PATCH 2/2] Update patent module API and middleware --- .../Fusion/middleware/custom_middleware.py | 45 +- FusionIIIT/Fusion/urls.py | 2 +- .../applications/patent_system/admin.py | 138 + .../patent_system/api/__init__.py | 0 .../patent_system/{ => api}/serializers.py | 288 +- .../patent_system/{ => api}/urls.py | 177 +- .../patent_system/{ => api}/views.py | 5851 +++++++++-------- .../0006_application_assigned_director.py | 26 + .../applications/patent_system/models.py | 1 + .../applications/patent_system/selectors.py | 116 + .../applications/patent_system/services.py | 136 + FusionIIIT/create_admin.py | 23 + FusionIIIT/create_admin_extrainfo.py | 38 + FusionIIIT/create_superuser.py | 24 + FusionIIIT/setup_admin_extrainfo.py | 35 + 15 files changed, 3767 insertions(+), 3133 deletions(-) create mode 100644 FusionIIIT/applications/patent_system/admin.py create mode 100644 FusionIIIT/applications/patent_system/api/__init__.py rename FusionIIIT/applications/patent_system/{ => api}/serializers.py (95%) rename FusionIIIT/applications/patent_system/{ => api}/urls.py (98%) rename FusionIIIT/applications/patent_system/{ => api}/views.py (91%) create mode 100644 FusionIIIT/applications/patent_system/migrations/0006_application_assigned_director.py create mode 100644 FusionIIIT/applications/patent_system/selectors.py create mode 100644 FusionIIIT/applications/patent_system/services.py create mode 100644 FusionIIIT/create_admin.py create mode 100644 FusionIIIT/create_admin_extrainfo.py create mode 100644 FusionIIIT/create_superuser.py create mode 100644 FusionIIIT/setup_admin_extrainfo.py diff --git a/FusionIIIT/Fusion/middleware/custom_middleware.py b/FusionIIIT/Fusion/middleware/custom_middleware.py index 74f7d7d72..8aae00a67 100644 --- a/FusionIIIT/Fusion/middleware/custom_middleware.py +++ b/FusionIIIT/Fusion/middleware/custom_middleware.py @@ -19,35 +19,44 @@ def user_logged_in_handler(sender, user, request, **kwargs): design = HoldsDesignation.objects.select_related('user','designation').filter(working=request.user) designation=[] - if str(user.extrainfo.user_type) == "student": - designation.append(str(user.extrainfo.user_type)) + try: + user_type = str(user.extrainfo.user_type) + except ExtraInfo.DoesNotExist: + user_type = None + + if user_type == "student": + designation.append(user_type) for i in design: - if str(i.designation) != str(user.extrainfo.user_type): + if str(i.designation) != str(user_type): print('-------') print(i.designation) - print(user.extrainfo.user_type) + print(user_type) print('') designation.append(str(i.designation)) for i in designation: print(i) - request.session['currentDesignationSelected'] = designation[0] - request.session['allDesignations'] = designation - first_designation = designation[0] - module_access = ModuleAccess.objects.filter(designation=first_designation).first() - - if module_access: - access_rights = {} - - field_names = [field.name for field in ModuleAccess._meta.get_fields() if field.name not in ['id', 'designation']] - - for field_name in field_names: - access_rights[field_name] = getattr(module_access, field_name) - - request.session['moduleAccessRights'] = access_rights + if designation: + request.session['currentDesignationSelected'] = designation[0] + request.session['allDesignations'] = designation + first_designation = designation[0] + else: + request.session['currentDesignationSelected'] = "" + request.session['allDesignations'] = [] + first_designation = None + + access_rights = {} + if first_designation: + module_access = ModuleAccess.objects.filter(designation=first_designation).first() + if module_access: + field_names = [field.name for field in ModuleAccess._meta.get_fields() if field.name not in ['id', 'designation']] + for field_name in field_names: + access_rights[field_name] = getattr(module_access, field_name) + + request.session['moduleAccessRights'] = access_rights print("logged iN") # Set the flag in the session to indicate that the function has bee+n executed diff --git a/FusionIIIT/Fusion/urls.py b/FusionIIIT/Fusion/urls.py index 374a58008..e3e2e5528 100755 --- a/FusionIIIT/Fusion/urls.py +++ b/FusionIIIT/Fusion/urls.py @@ -65,7 +65,7 @@ url(r'^recruitment/', include('applications.recruitment.urls')), url(r'^examination/', include('applications.examination.urls')), url(r'^otheracademic/', include('applications.otheracademic.urls')), - url(r'^patentsystem/', include('applications.patent_system.urls')), + url(r'^patentsystem/', include('applications.patent_system.api.urls')), path( 'password-reset/', diff --git a/FusionIIIT/applications/patent_system/admin.py b/FusionIIIT/applications/patent_system/admin.py new file mode 100644 index 000000000..365a51cba --- /dev/null +++ b/FusionIIIT/applications/patent_system/admin.py @@ -0,0 +1,138 @@ +from django.contrib import admin, messages +from django.db import transaction + +# Ensure globals admin registrations are loaded before we attach actions. +import applications.globals.admin # noqa: F401 +from applications.globals.models import Designation, ExtraInfo, HoldsDesignation + +from .models import ( + Applicant, + Application, + ApplicationSectionI, + ApplicationSectionII, + ApplicationSectionIII, + AssociatedWith, + Attorney, + BudgetApproval, + CommunicationLog, + ConflictDeclaration, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LegalAssessment, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + OfficeActionResponse, +) + + +def _get_or_create_designation(name, full_name, designation_type): + designation = Designation.objects.filter(name__iexact=name).first() + if designation: + updated = False + if not designation.full_name: + designation.full_name = full_name + updated = True + if not designation.type: + designation.type = designation_type + updated = True + if updated: + designation.save(update_fields=["full_name", "type"]) + return designation + + return Designation.objects.create( + name=name, + full_name=full_name, + type=designation_type, + ) + + +def _assign_designation_to_user(user, designation): + # Keep working=user so middleware and role switching stay consistent. + _, created = HoldsDesignation.objects.get_or_create( + user=user, + designation=designation, + defaults={"working": user}, + ) + return created + + +def assign_patent_director_and_pcc_admin(modeladmin, request, queryset): + """Assign Director + PCC Admin role(s) to selected user(s)""" + director_designation = _get_or_create_designation( + name="Director", + full_name="Director", + designation_type="administrative", + ) + pcc_admin_designation = _get_or_create_designation( + name="PCC Admin", + full_name="PCC Admin", + designation_type="administrative", + ) + + users = [obj.user for obj in queryset.select_related("user")] + if not users: + modeladmin.message_user(request, "No users selected.", level=messages.WARNING) + return + + director_created_count = 0 + pcc_created_count = 0 + + with transaction.atomic(): + for user in users: + if _assign_designation_to_user(user, director_designation): + director_created_count += 1 + if _assign_designation_to_user(user, pcc_admin_designation): + pcc_created_count += 1 + + modeladmin.message_user( + request, + ( + f"Processed {len(users)} user(s). " + f"New Director assignments: {director_created_count}. " + f"New PCC Admin assignments: {pcc_created_count}." + ), + level=messages.SUCCESS, + ) + + +assign_patent_director_and_pcc_admin.short_description = "Patent: assign Director + PCC Admin role(s)" + + +# Attach role-assignment action to already-registered ExtraInfo admin safely. +extra_info_admin = admin.site._registry.get(ExtraInfo) +if extra_info_admin: + existing_actions = list(getattr(extra_info_admin, "actions", []) or []) + if assign_patent_director_and_pcc_admin not in existing_actions: + existing_actions.append(assign_patent_director_and_pcc_admin) + extra_info_admin.actions = existing_actions + + +# Register patent-system models for admin visibility. +PATENT_MODELS = [ + Applicant, + Attorney, + Application, + ApplicationSectionI, + ApplicationSectionII, + ApplicationSectionIII, + AssociatedWith, + Document, + CommunicationLog, + ConflictDeclaration, + LegalAssessment, + NotificationEvent, + BudgetApproval, + ExternalFilingRecord, + MaintenanceSchedule, + DocumentVersion, + InventorConsent, + OfficeAction, + OfficeActionResponse, +] + +for model in PATENT_MODELS: + if model not in admin.site._registry: + admin.site.register(model) diff --git a/FusionIIIT/applications/patent_system/api/__init__.py b/FusionIIIT/applications/patent_system/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/patent_system/serializers.py b/FusionIIIT/applications/patent_system/api/serializers.py similarity index 95% rename from FusionIIIT/applications/patent_system/serializers.py rename to FusionIIIT/applications/patent_system/api/serializers.py index a96bda442..7aba81c63 100644 --- a/FusionIIIT/applications/patent_system/serializers.py +++ b/FusionIIIT/applications/patent_system/api/serializers.py @@ -1,145 +1,145 @@ -from rest_framework import serializers -from .models import ( - AppealRequest, - Attorney, - AuditLog, - BudgetApproval, - CommunicationLog, - ConflictDeclaration, - Document, - DocumentVersion, - ExternalFilingRecord, - InventorConsent, - LegalAdviceMemo, - LegalAssessment, - LicensingRequest, - MaintenanceSchedule, - NotificationEvent, - OfficeAction, - OfficeActionResponse, - PriorArtReference, -) - -class AttorneySerializer(serializers.ModelSerializer): - class Meta: - model = Attorney - fields = ['id', 'name', 'email', 'phone', 'firm_name'] - read_only_fields = ['id'] - -class DocumentSerializer(serializers.ModelSerializer): - class Meta: - model = Document - fields = ['id', 'title', 'link', 'created_at', 'updated_at'] - read_only_fields = ['id', 'created_at', 'updated_at'] - - -class CommunicationLogSerializer(serializers.ModelSerializer): - class Meta: - model = CommunicationLog - fields = '__all__' - read_only_fields = ['id', 'application', 'logged_by', 'created_at', 'updated_at'] - - -class ConflictDeclarationSerializer(serializers.ModelSerializer): - class Meta: - model = ConflictDeclaration - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class LegalAssessmentSerializer(serializers.ModelSerializer): - class Meta: - model = LegalAssessment - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class NotificationEventSerializer(serializers.ModelSerializer): - class Meta: - model = NotificationEvent - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class BudgetApprovalSerializer(serializers.ModelSerializer): - class Meta: - model = BudgetApproval - fields = '__all__' - read_only_fields = ['id', 'created_at', 'decided_at'] - - -class ExternalFilingRecordSerializer(serializers.ModelSerializer): - class Meta: - model = ExternalFilingRecord - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class MaintenanceScheduleSerializer(serializers.ModelSerializer): - class Meta: - model = MaintenanceSchedule - fields = '__all__' - read_only_fields = ['id', 'created_at', 'reminder_sent_at', 'paid_at'] - - -class DocumentVersionSerializer(serializers.ModelSerializer): - class Meta: - model = DocumentVersion - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class InventorConsentSerializer(serializers.ModelSerializer): - class Meta: - model = InventorConsent - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class OfficeActionSerializer(serializers.ModelSerializer): - class Meta: - model = OfficeAction - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class OfficeActionResponseSerializer(serializers.ModelSerializer): - class Meta: - model = OfficeActionResponse - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class LicensingRequestSerializer(serializers.ModelSerializer): - class Meta: - model = LicensingRequest - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class AppealRequestSerializer(serializers.ModelSerializer): - class Meta: - model = AppealRequest - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class PriorArtReferenceSerializer(serializers.ModelSerializer): - class Meta: - model = PriorArtReference - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class LegalAdviceMemoSerializer(serializers.ModelSerializer): - class Meta: - model = LegalAdviceMemo - fields = '__all__' - read_only_fields = ['id', 'created_at'] - - -class AuditLogSerializer(serializers.ModelSerializer): - class Meta: - model = AuditLog - fields = '__all__' +from rest_framework import serializers +from ..models import ( + AppealRequest, + Attorney, + AuditLog, + BudgetApproval, + CommunicationLog, + ConflictDeclaration, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LegalAdviceMemo, + LegalAssessment, + LicensingRequest, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + OfficeActionResponse, + PriorArtReference, +) + +class AttorneySerializer(serializers.ModelSerializer): + class Meta: + model = Attorney + fields = ['id', 'name', 'email', 'phone', 'firm_name'] + read_only_fields = ['id'] + +class DocumentSerializer(serializers.ModelSerializer): + class Meta: + model = Document + fields = ['id', 'title', 'link', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class CommunicationLogSerializer(serializers.ModelSerializer): + class Meta: + model = CommunicationLog + fields = '__all__' + read_only_fields = ['id', 'application', 'logged_by', 'created_at', 'updated_at'] + + +class ConflictDeclarationSerializer(serializers.ModelSerializer): + class Meta: + model = ConflictDeclaration + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LegalAssessmentSerializer(serializers.ModelSerializer): + class Meta: + model = LegalAssessment + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class NotificationEventSerializer(serializers.ModelSerializer): + class Meta: + model = NotificationEvent + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class BudgetApprovalSerializer(serializers.ModelSerializer): + class Meta: + model = BudgetApproval + fields = '__all__' + read_only_fields = ['id', 'created_at', 'decided_at'] + + +class ExternalFilingRecordSerializer(serializers.ModelSerializer): + class Meta: + model = ExternalFilingRecord + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class MaintenanceScheduleSerializer(serializers.ModelSerializer): + class Meta: + model = MaintenanceSchedule + fields = '__all__' + read_only_fields = ['id', 'created_at', 'reminder_sent_at', 'paid_at'] + + +class DocumentVersionSerializer(serializers.ModelSerializer): + class Meta: + model = DocumentVersion + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class InventorConsentSerializer(serializers.ModelSerializer): + class Meta: + model = InventorConsent + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class OfficeActionSerializer(serializers.ModelSerializer): + class Meta: + model = OfficeAction + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class OfficeActionResponseSerializer(serializers.ModelSerializer): + class Meta: + model = OfficeActionResponse + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LicensingRequestSerializer(serializers.ModelSerializer): + class Meta: + model = LicensingRequest + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class AppealRequestSerializer(serializers.ModelSerializer): + class Meta: + model = AppealRequest + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class PriorArtReferenceSerializer(serializers.ModelSerializer): + class Meta: + model = PriorArtReference + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class LegalAdviceMemoSerializer(serializers.ModelSerializer): + class Meta: + model = LegalAdviceMemo + fields = '__all__' + read_only_fields = ['id', 'created_at'] + + +class AuditLogSerializer(serializers.ModelSerializer): + class Meta: + model = AuditLog + fields = '__all__' read_only_fields = ['id', 'created_at'] \ No newline at end of file diff --git a/FusionIIIT/applications/patent_system/urls.py b/FusionIIIT/applications/patent_system/api/urls.py similarity index 98% rename from FusionIIIT/applications/patent_system/urls.py rename to FusionIIIT/applications/patent_system/api/urls.py index 282bbbb71..f1a207e43 100644 --- a/FusionIIIT/applications/patent_system/urls.py +++ b/FusionIIIT/applications/patent_system/api/urls.py @@ -1,88 +1,89 @@ -from django.conf import settings -from django.conf.urls.static import static -from django.urls import path -from . import views - -urlpatterns = [ - path("", views.index, name="index"), - - # Applicant-related paths - path("applicant/applications/submit/", views.submit_application, name="submit_application"), - path("applicant/applications/", views.view_applications, name="view_applications"), - path("applicant/applications/details//", views.view_application_details_for_applicant, name="view_application_details"), - path("applicant/drafts/", views.saved_drafts, name="saved_drafts"), - path("applicant/applications//withdraw/", views.withdraw_application, name="withdraw_application"), - path("applicant/applications//resubmit/", views.resubmit_application, name="resubmit_application"), - path("applicant/applications//appeals/", views.submit_appeal, name="submit_appeal"), - path("applicant/insights/", views.get_applicant_insights, name="get_applicant_insights"), - - # PCCAdmin-related paths - path("pccAdmin/applications/new/", views.new_applications, name="new_applications"), - path("pccAdmin/applications/new/review//", views.review_application, name="review_applications"), - path("pccAdmin/applications/new/forward//", views.forward_application, name="forward_application"), - path("pccAdmin/applications/new/requestModification//", views.request_application_modification, name="request_application_modification"), - path("pccAdmin/applications/ongoing/", views.ongoing_applications, name="ongoing_applications"), - path("pccAdmin/applications/ongoing/changeStatus//", views.change_application_status, name="change_application_status"), - path("pccAdmin/applications/past/", views.past_applications, name="past_applications"), - path("pccAdmin/applications/details//", views.view_application_details_for_pccAdmin, name="view_application_details_for_pccAdmin"), - path("pccAdmin/applications//communication-logs/", views.communication_logs, name="communication_logs"), - path("pccAdmin/applications//declare-conflict/", views.declare_conflict, name="declare_conflict"), - path("pccAdmin/applications//legal-assessment/", views.legal_assessment_api, name="legal_assessment_api"), - path("pccAdmin/applications//legal-memos/", views.legal_memos_api, name="legal_memos_api"), - path("pccAdmin/applications//budget/", views.budget_api, name="budget_api"), - path("pccAdmin/budget//decision/", views.budget_decision_by_id, name="budget_decision_by_id"), - path("pccAdmin/applications//external-filing/", views.external_filing_api, name="external_filing_api"), - path("pccAdmin/applications//office-actions/", views.office_actions_api, name="office_actions_api"), - path("pccAdmin/office-actions//respond/", views.respond_office_action, name="respond_office_action"), - path("pccAdmin/applications//prior-art/", views.prior_art_api, name="prior_art_api"), - path("pccAdmin/applications//appeals/", views.appeals_api, name="appeals_api"), - path("pccAdmin/applications//licensing/", views.licensing_api, name="licensing_api"), - path("pccAdmin/applications//inventor-consents/", views.inventor_consents_api, name="inventor_consents_api"), - path("pccAdmin/applications//maintenance/", views.maintenance_api, name="maintenance_api"), - path("pccAdmin/maintenance//mark-paid/", views.mark_maintenance_paid, name="mark_maintenance_paid"), - path("pccAdmin/reviewer-queue/", views.reviewer_queue, name="reviewer_queue"), - path("pccAdmin/queue/prioritized/", views.queue_prioritized, name="queue_prioritized"), - path("pccAdmin/notifications/", views.get_notifications, name="get_notifications"), - path("pccAdmin/notifications//read/", views.mark_notification_read, name="mark_notification_read"), - path("pccAdmin/audit-logs/", views.get_audit_logs, name="get_audit_logs"), - path("pccAdmin/insights/", views.pcc_insights, name="pcc_insights"), - path("pccAdmin/applications/new/resubmit//", views.pcc_resubmit_application, name="pcc_resubmit_application"), - - # Director-related paths - path("director/applications/new/", views.director_new_applications, name="director_new_applications"), - path("director/application/reject", views.director_reject, name="director_reject"), - path("director/application/accept", views.director_accept, name="director_accept"), - path("director/reviewedapplications", views.director_reviewed_applications, name="director_reviewed_applications"), - path("director/active", views.active_applications, name="active_applications"), - path("director/application/details", views.director_application_view, name="director_application_view"), - path("director/notifications/", views.director_notifications, name="director_notifications"), - path("director/applications//budget/decision/", views.director_decide_budget, name="director_decide_budget"), - path("director/insights/", views.director_insights, name="director_insights"), - path("attorney/applications/", views.attorney_applications, name="attorney_applications"), - path("attorney/applications//forward/", views.attorney_forward_to_director, name="attorney_forward_to_director"), - - # Attorney management URLs - path("pccAdmin/attorneys/", views.get_attorney_list, name="get_attorney_list"), - path("pccAdmin/attorneys/add/", views.add_attorney, name="add_attorney"), - path("pccAdmin/attorneys//remove/", views.remove_attorney, name="remove_attorney"), - path("pccAdmin/attorneys//applications/", views.get_attorney_applications, name="get_attorney_applications"), - path("pccAdmin/attorneys//update/", views.update_attorney_details, name="update_attorney_details"), - - # Document Management URLs - path('documents/', views.manage_documents, name='manage_documents'), - path('pccAdmin/documents//delete/', views.delete_document, name='delete_document'), - path('pccAdmin/documents//versions/upload/', views.upload_document_version, name='upload_document_version'), - path('pccAdmin/documents//lock/', views.lock_document, name='lock_document'), - path('documents//versions/upload/', views.upload_document_version, name='upload_document_version_alias'), - path('documents//versions/', views.document_versions_api, name='document_versions_api'), - path('documents//lock/', views.lock_document, name='lock_document_alias'), - - # Global aliases used by imported frontend services - path('notifications/', views.notifications_root, name='notifications_root'), - path('audit-logs/', views.audit_logs_root, name='audit_logs_root'), - path('audit-logs//', views.audit_logs_by_application, name='audit_logs_by_application'), -] - -# Serve media files in development -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +from django.conf import settings +from django.conf.urls.static import static +from django.urls import path +from . import views + +urlpatterns = [ + path("", views.index, name="index"), + + # Applicant-related paths + path("applicant/applications/submit/", views.submit_application, name="submit_application"), + path("applicant/applications/", views.view_applications, name="view_applications"), + path("applicant/applications/details//", views.view_application_details_for_applicant, name="view_application_details"), + path("applicant/drafts/", views.saved_drafts, name="saved_drafts"), + path("applicant/applications//withdraw/", views.withdraw_application, name="withdraw_application"), + path("applicant/applications//resubmit/", views.resubmit_application, name="resubmit_application"), + path("applicant/applications//appeals/", views.submit_appeal, name="submit_appeal"), + path("applicant/insights/", views.get_applicant_insights, name="get_applicant_insights"), + + # PCCAdmin-related paths + path("pccAdmin/applications/new/", views.new_applications, name="new_applications"), + path("pccAdmin/applications/new/review//", views.review_application, name="review_applications"), + path("pccAdmin/applications/new/forward//", views.forward_application, name="forward_application"), + path("pccAdmin/applications/new/requestModification//", views.request_application_modification, name="request_application_modification"), + path("pccAdmin/applications/ongoing/", views.ongoing_applications, name="ongoing_applications"), + path("pccAdmin/applications/ongoing/changeStatus//", views.change_application_status, name="change_application_status"), + path("pccAdmin/applications/past/", views.past_applications, name="past_applications"), + path("pccAdmin/applications/details//", views.view_application_details_for_pccAdmin, name="view_application_details_for_pccAdmin"), + path("pccAdmin/applications//communication-logs/", views.communication_logs, name="communication_logs"), + path("pccAdmin/applications//declare-conflict/", views.declare_conflict, name="declare_conflict"), + path("pccAdmin/applications//legal-assessment/", views.legal_assessment_api, name="legal_assessment_api"), + path("pccAdmin/applications//legal-memos/", views.legal_memos_api, name="legal_memos_api"), + path("pccAdmin/applications//budget/", views.budget_api, name="budget_api"), + path("pccAdmin/budget//decision/", views.budget_decision_by_id, name="budget_decision_by_id"), + path("pccAdmin/applications//external-filing/", views.external_filing_api, name="external_filing_api"), + path("pccAdmin/applications//office-actions/", views.office_actions_api, name="office_actions_api"), + path("pccAdmin/office-actions//respond/", views.respond_office_action, name="respond_office_action"), + path("pccAdmin/applications//prior-art/", views.prior_art_api, name="prior_art_api"), + path("pccAdmin/applications//appeals/", views.appeals_api, name="appeals_api"), + path("pccAdmin/applications//licensing/", views.licensing_api, name="licensing_api"), + path("pccAdmin/applications//inventor-consents/", views.inventor_consents_api, name="inventor_consents_api"), + path("pccAdmin/applications//maintenance/", views.maintenance_api, name="maintenance_api"), + path("pccAdmin/maintenance//mark-paid/", views.mark_maintenance_paid, name="mark_maintenance_paid"), + path("pccAdmin/reviewer-queue/", views.reviewer_queue, name="reviewer_queue"), + path("pccAdmin/queue/prioritized/", views.queue_prioritized, name="queue_prioritized"), + path("pccAdmin/notifications/", views.get_notifications, name="get_notifications"), + path("pccAdmin/notifications//read/", views.mark_notification_read, name="mark_notification_read"), + path("pccAdmin/audit-logs/", views.get_audit_logs, name="get_audit_logs"), + path("pccAdmin/insights/", views.pcc_insights, name="pcc_insights"), + path("pccAdmin/applications/new/resubmit//", views.pcc_resubmit_application, name="pcc_resubmit_application"), + + # Director-related paths + path("director/applications/new/", views.director_new_applications, name="director_new_applications"), + path("director/application/reject", views.director_reject, name="director_reject"), + path("director/application/accept", views.director_accept, name="director_accept"), + path("director/reviewedapplications", views.director_reviewed_applications, name="director_reviewed_applications"), + path("director/active", views.active_applications, name="active_applications"), + path("director/application/details", views.director_application_view, name="director_application_view"), + path("director/notifications/", views.director_notifications, name="director_notifications"), + path("director/applications//budget/decision/", views.director_decide_budget, name="director_decide_budget"), + path("director/insights/", views.director_insights, name="director_insights"), + path("attorney/applications/", views.attorney_applications, name="attorney_applications"), + path("attorney/applications//forward/", views.attorney_forward_to_director, name="attorney_forward_to_director"), + + # Attorney management URLs + path("pccAdmin/attorneys/", views.get_attorney_list, name="get_attorney_list"), + path("pccAdmin/directors/", views.get_director_list, name="get_director_list"), + path("pccAdmin/attorneys/add/", views.add_attorney, name="add_attorney"), + path("pccAdmin/attorneys//remove/", views.remove_attorney, name="remove_attorney"), + path("pccAdmin/attorneys//applications/", views.get_attorney_applications, name="get_attorney_applications"), + path("pccAdmin/attorneys//update/", views.update_attorney_details, name="update_attorney_details"), + + # Document Management URLs + path('documents/', views.manage_documents, name='manage_documents'), + path('pccAdmin/documents//delete/', views.delete_document, name='delete_document'), + path('pccAdmin/documents//versions/upload/', views.upload_document_version, name='upload_document_version'), + path('pccAdmin/documents//lock/', views.lock_document, name='lock_document'), + path('documents//versions/upload/', views.upload_document_version, name='upload_document_version_alias'), + path('documents//versions/', views.document_versions_api, name='document_versions_api'), + path('documents//lock/', views.lock_document, name='lock_document_alias'), + + # Global aliases used by imported frontend services + path('notifications/', views.notifications_root, name='notifications_root'), + path('audit-logs/', views.audit_logs_root, name='audit_logs_root'), + path('audit-logs//', views.audit_logs_by_application, name='audit_logs_by_application'), +] + +# Serve media files in development +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/FusionIIIT/applications/patent_system/views.py b/FusionIIIT/applications/patent_system/api/views.py similarity index 91% rename from FusionIIIT/applications/patent_system/views.py rename to FusionIIIT/applications/patent_system/api/views.py index 1c00c0d3c..ba733e795 100644 --- a/FusionIIIT/applications/patent_system/views.py +++ b/FusionIIIT/applications/patent_system/api/views.py @@ -1,2883 +1,2970 @@ -import os -import json -import logging -from datetime import timedelta -from decimal import Decimal - -from django.http import JsonResponse, HttpResponse -from django.utils.timezone import now -from django.utils import timezone -from django.core.exceptions import ObjectDoesNotExist -from django.core.files.storage import default_storage -from django.shortcuts import get_object_or_404 -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required -from django.views.decorators.csrf import csrf_exempt -from django.db import transaction -from django.db.models import Q - -from rest_framework import status -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.authentication import TokenAuthentication -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from django.db.models import Count - -from .models import ( - Application, - ApplicationSectionI, - ApplicationSectionII, - ApplicationSectionIII, - AssociatedWith, - Applicant, - AppealRequest, - Attorney, - AuditLog, - BudgetApproval, - Document, - DocumentVersion, - ExternalFilingRecord, - InventorConsent, - LegalAdviceMemo, - LegalAssessment, - LicensingRequest, - MaintenanceSchedule, - NotificationEvent, - OfficeAction, - OfficeActionResponse, - PriorArtReference, - CommunicationLog, - ConflictDeclaration, -) - -from applications.globals.models import ( - Designation, - DepartmentInfo, - ExtraInfo, - HoldsDesignation, -) - -from .serializers import ( - AppealRequestSerializer, - AttorneySerializer, - AuditLogSerializer, - BudgetApprovalSerializer, - CommunicationLogSerializer, - ConflictDeclarationSerializer, - DocumentSerializer, - DocumentVersionSerializer, - ExternalFilingRecordSerializer, - InventorConsentSerializer, - LegalAdviceMemoSerializer, - LegalAssessmentSerializer, - LicensingRequestSerializer, - MaintenanceScheduleSerializer, - NotificationEventSerializer, - OfficeActionResponseSerializer, - OfficeActionSerializer, - PriorArtReferenceSerializer, -) - -# Logger setup - used for debugging and logging errors -logger = logging.getLogger(__name__) - - -def index(request): - return JsonResponse( - { - "message": "Patent Management module is running.", - "routes": [ - "/patentsystem/applicant/applications/submit/", - "/patentsystem/applicant/applications/", - "/patentsystem/pccAdmin/applications/new/", - "/patentsystem/pccAdmin/applications/past/", - "/patentsystem/director/applications/new/", - ], - } - ) - -# ----------------------------------------- -# 🔹 Applicant Views -# ----------------------------------------- - -def generate_file_path(folder, filename): - """Helper function to generate a unique file path.""" - base, extension = os.path.splitext(filename) - timestamp = now().strftime("%Y%m%d%H%M%S") - return os.path.join(f"patent/{folder}", f"{base}_{timestamp}{extension}") - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def submit_application(request): - if request.method != "POST": - return JsonResponse({"error": "Invalid request method"}, status=405) - - try: - # Start a transaction - with transaction.atomic(): - json_data = request.POST.get("json_data") - if not json_data: - return JsonResponse({"error": "Missing JSON data"}, status=400) - - data = json.loads(json_data) - - print("Parsed data keys:", data.keys()) - - # Required file fields - poc_file = request.FILES.get("poc_details") - source_file = request.FILES.get("source_file") - mou_file = request.FILES.get("mou_file") - form_iii_file = request.FILES.get("form_iii") - - required_fields = [ - "title", "inventors", "area_of_invention", "problem_statement", "objective", "ip_type", - "novelty", "advantages", "tested_experimentally", "applications", - "funding_details", "funding_source", "publication_details", "mou_details", - "research_details", "company_details", - "development_stage" - ] - - for field in required_fields: - if field not in data: - return JsonResponse({"error": f"Missing required field: {field}"}, status=400) - - # Get the logged-in user - user = request.user - if not _is_authorized_applicant_user(user): - return JsonResponse( - { - "error": ( - "Only authorized applicants, including faculty roles, can submit patent applications." - ) - }, - status=403, - ) - - # Check if the user has an applicant profile, create one if not - applicant, created = Applicant.objects.get_or_create( - user=user, - defaults={ - "email": user.email, - "name": user.get_full_name() or user.username, - "mobile": "", - "address": "", - } - ) - - # Create application entry with the logged-in user as the primary applicant - application = Application.objects.create( - title=data["title"], - status="Submitted", - decision_status="Pending", - submitted_date=now(), - primary_applicant=applicant, - ) - - # Save file uploads and store paths - poc_file_path = None - source_file_path = None - mou_file_path = None - form_iii_file_path = None - - if poc_file: - poc_file_path = default_storage.save( - generate_file_path("Section-I/poc_details", poc_file.name), poc_file - ) - if source_file: - source_file_path = default_storage.save( - generate_file_path("Section-II/source_details", source_file.name), source_file - ) - if mou_file: - mou_file_path = default_storage.save( - generate_file_path("Section-II/mou_details", mou_file.name), mou_file - ) - if form_iii_file: - form_iii_file_path = default_storage.save( - generate_file_path("Section-III/form_iii", form_iii_file.name), form_iii_file - ) - - ApplicationSectionI.objects.create( - application=application, - type_of_ip=data["ip_type"], - area=data["area_of_invention"], - problem=data["problem_statement"], - objective=data["objective"], - novelty=data["novelty"], - advantages=data["advantages"], - is_tested=data["tested_experimentally"], - applications=data["applications"], - poc_details=poc_file_path - ) - - ApplicationSectionII.objects.create( - application=application, - funding_details=data["funding_details"], - funding_source=data["funding_source"], - source_agreement=source_file_path, - publication_details=data["publication_details"], - mou_details=data["mou_details"], - mou_file=mou_file_path, - research_details=data["research_details"] - ) - - # Process multiple companies - company_details = data.get("company_details", []) - if not isinstance(company_details, list): - return JsonResponse({"error": "company_details should be a list"}, status=400) - - for company in company_details: - company_name = company.get("company_name") - contact_person = company.get("contact_person") - contact_no = company.get("contact_no") - - if not (company_name and contact_person and contact_no): - return JsonResponse({"error": "Each company entry must have company_name, contact_person, and contact_no"}, status=400) - - ApplicationSectionIII.objects.create( - application=application, - company_name=company_name, - contact_person=contact_person, - contact_no=contact_no, - development_stage=data["development_stage"], - form_iii=form_iii_file_path - ) - - # Associate inventors with the application - for inventor in data["inventors"]: - email = inventor["institute_mail"] - percentage = inventor["percentage"] - name = inventor.get("name", "") - personal_mail = inventor.get("personal_mail", "") - mobile = inventor.get("mobile", "") - address = inventor.get("address", "") - - try: - user = User.objects.get(email=email) - applicant, created = Applicant.objects.update_or_create( - user=user, - defaults={ - "email": personal_mail, - "name": name, - "mobile": mobile, - "address": address, - } - ) - - AssociatedWith.objects.create( - application=application, - applicant=applicant, - percentage_share=percentage - ) - except User.DoesNotExist: - # This will rollback all database changes made in this transaction - return JsonResponse({"error": f"Inventor {email} not found in auth_user"}, status=404) - - # Generate token - application_id = application.id - application.save() - - return JsonResponse({ - "message": "Application submitted successfully", - "application_id": application_id, - }) - - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON format"}, status=400) - except Exception as e: - return JsonResponse({"error": str(e)}, status=500) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def view_applications(request): - user_id = request.user.id - try: - # Get the applicant based on user_id - applicant = get_object_or_404(Applicant, user_id=user_id) - - # Get all application IDs associated with this applicant - associated_apps = AssociatedWith.objects.filter(applicant=applicant).values_list('application_id', flat=True) - - # Retrieve applications where the user is primary applicant or associated inventor - applications = ( - Application.objects.filter( - Q(primary_applicant=applicant) | Q(id__in=associated_apps) - ) - .select_related("attorney") - .distinct() - .order_by("-last_updated_at") - ) - - # Prepare response data - applications_data = [] - for app in applications: - applications_data.append({ - "application_id": app.id, - "title": app.title, - "token_no": app.token_no, - "application_number": app.token_no, - "attorney_name": app.attorney.name if app.attorney else None, - "submitted_date": app.submitted_date if app.submitted_date else None, - "status": app.status, - "decision_status": app.decision_status, - }) - - return JsonResponse({"applications": applications_data}, safe=False) - - except Applicant.DoesNotExist: - return JsonResponse({"error": "Applicant not found"}, status=404) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def view_application_details_for_applicant(request, application_id): - user = request.user - - # Check if the logged-in user is an applicant - try: - applicant = Applicant.objects.get(user=user) - except Applicant.DoesNotExist: - return JsonResponse({"error": "Unauthorized: User is not an applicant"}, status=403) - - # Fetch application details - application = get_object_or_404(Application, id=application_id) - - # Primary applicant or associated inventor can view details - is_primary_applicant = application.primary_applicant_id == applicant.id - is_associated = AssociatedWith.objects.filter(application_id=application_id, applicant=applicant).exists() - if not (is_primary_applicant or is_associated): - return JsonResponse({"error": "Forbidden: You are not associated with this application"}, status=403) - - handler_name = None - if application.assigned_pcc_admin_id: - handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username - - attorney_name = application.attorney.name if application.attorney else None - - # Fetch associated applicants - associated_applicants = AssociatedWith.objects.filter(application=application) - applicants_data = [ - { - "name": app.applicant.name, - "email": app.applicant.email, - "mobile": app.applicant.mobile, - "address": app.applicant.address, - "percentage_share": app.percentage_share - } - for app in associated_applicants - ] - - # Fetch Section I details - section_i = ApplicationSectionI.objects.filter(application=application).first() - section_i_data = { - "type_of_ip": section_i.type_of_ip if section_i else None, - "type_of_ip": section_i.type_of_ip if section_i else None, - "area": section_i.area if section_i else None, - "problem": section_i.problem if section_i else None, - "objective": section_i.objective if section_i else None, - "novelty": section_i.novelty if section_i else None, - "advantages": section_i.advantages if section_i else None, - "is_tested": section_i.is_tested if section_i else None, - "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, - "applications": section_i.applications if section_i else None, - } - - # Fetch Section II details - section_ii = ApplicationSectionII.objects.filter(application=application).first() - section_ii_data = { - "funding_details": section_ii.funding_details if section_ii else None, - "funding_source": section_ii.funding_source if section_ii else None, - "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, - "publication_details": section_ii.publication_details if section_ii else None, - "mou_details": section_ii.mou_details if section_ii else None, - "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, - "research_details": section_ii.research_details if section_ii else None - } - - # Fetch Section III details - section_iii = ApplicationSectionIII.objects.filter(application=application).first() - section_iii_data = { - "company_name": section_iii.company_name if section_iii else None, - "contact_person": section_iii.contact_person if section_iii else None, - "contact_no": section_iii.contact_no if section_iii else None, - "development_stage": section_iii.development_stage if section_iii else None, - "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None - } - - # Prepare response - response_data = { - "application_id": application.id, - "title": application.title, - "status": application.status, - "token_no": application.token_no if application.token_no else "Token not generated", - "assigned_pcc_admin": handler_name, - "attorney_name": attorney_name, - "dates": { - "submitted_date": application.submitted_date if application.submitted_date else None, - "reviewed_by_pcc_date": application.reviewed_by_pcc_date, - "forwarded_to_director_date": application.forwarded_to_director_date, - "director_approval_date": application.director_approval_date, - "patentability_check_start_date": application.patentability_check_start_date, - "patentability_check_completed_date": application.patentability_check_completed_date, - "search_report_generated_date": application.search_report_generated_date, - "patent_filed_date": application.patent_filed_date, - "patent_published_date": application.patent_published_date, - "decision_date": application.decision_date - }, - "decision_status": application.decision_status, - "comments": application.comments if application.comments else None, - "applicants": applicants_data, - "section_I": section_i_data, - "section_II": section_ii_data, - "section_III": section_iii_data - } - - return JsonResponse(response_data, safe=False) - -def saved_drafts(request): - return JsonResponse({"message": "save drafts"}) - -# ----------------------------------------- -# 🔹 PCC Admin Views -# ----------------------------------------- - -# For new applications tab -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def new_applications(request): - try: - REVIEW_STATUSES = ["Submitted", "Reviewed by PCC Admin"] - - applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") - - application_dict = {} # Using a dictionary instead of a list - - for app in applications: - try: - applicant = app.primary_applicant # Get the Applicant instance - - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None - - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None - designation_name = holds_designation.designation.name if holds_designation else "Unknown" - - # Format response as a dictionary - application_dict[app.id] = { - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown" - } - except Exception as app_error: - logger.error(f"Error processing application {app.id}: {str(app_error)}") - continue - - return JsonResponse({"applications": application_dict}, safe=False) - except Exception as err: - logger.error(f"Error in new_applications: {str(err)}") - return JsonResponse({"error": str(err)}, status=500) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def review_application(request, application_id): - # Check if request method is POST - if request.method == "POST": - try: - # Validate that application_id is provided - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - # Try to fetch the application by its ID - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - # Enforce workflow stage: only Submitted applications can be reviewed. - if application.status == "Reviewed by PCC Admin": - return JsonResponse({"message": "Application already reviewed."}) - if application.status != "Submitted": - return JsonResponse( - { - "error": ( - "Only applications in 'Submitted' state can be reviewed. " - f"Current status: {application.status}" - ) - }, - status=400, - ) - - # Parse JSON body - try: - data = json.loads(request.body) - comments = data.get("comments", "") - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON body."}, status=400) - - # Update application status and review date - application.status = "Reviewed by PCC Admin" - if comments != "": - application.comments = comments - application.assigned_pcc_admin = request.user - application.reviewed_by_pcc_date = now() - application.save() - - # Return success response with updated status and date - return JsonResponse({ - "message": "Application status updated to 'Reviewed by PCC Admin'.", - "application_id": application.id, - "new_status": application.status, - "reviewed_by_pcc_date": application.reviewed_by_pcc_date, - }) - - # Handle invalid JSON (though not used directly here) - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON."}, status=400) - - # Handle non-POST requests - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def forward_application(request, application_id): - if request.method == "POST": - try: - if not _is_pcc_admin_user(request.user): - return JsonResponse( - {"error": "Only PCC Admin can assign attorneys and forward applications."}, - status=403, - ) - - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - # Get the application - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - # Enforce workflow stage and PCC ownership. - if application.status == "Forwarded for Director's Review": - return JsonResponse({"message": "Application is already forwarded for Director's review."}, status=400) - if application.status != "Reviewed by PCC Admin": - return JsonResponse( - { - "error": ( - "Only applications in 'Reviewed by PCC Admin' state can be forwarded. " - f"Current status: {application.status}" - ) - }, - status=400, - ) - if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: - owner = application.assigned_pcc_admin - owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") - return JsonResponse( - { - "error": f"Application is assigned to another PCC Admin: {owner_name}.", - "assigned_pcc_admin": owner_name, - }, - status=403, - ) - - # Parse JSON body - try: - data = json.loads(request.body) - external_attorney_name = data.get("attorney_name", "").strip() - external_attorney_email = data.get("attorney_email", "").strip() - comments = _require_comments(data) - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON body."}, status=400) - except ValueError as exc: - return JsonResponse({"error": str(exc)}, status=400) - - if not external_attorney_name: - return JsonResponse({"error": "attorney_name is required in the request body."}, status=400) - - attorney = Attorney.objects.filter(name__iexact=external_attorney_name).first() - if not attorney: - return JsonResponse( - {"error": f"Attorney with name '{external_attorney_name}' not found."}, - status=404, - ) - - # Optional: Limit comment length - if comments and len(comments) > 1000: - return JsonResponse({"error": "Comments too long. Max 1000 characters allowed."}, status=400) - - # Update the application - application.status = "Forwarded for Director's Review" - application.forwarded_to_director_date = now() - application.assigned_pcc_admin = request.user - application.attorney = attorney - application.comments = comments - application.save() - - if hasattr(attorney, "current_workload"): - attorney.current_workload = Application.objects.filter(attorney=attorney).count() - attorney.save(update_fields=["current_workload"]) - - if comments or external_attorney_name or external_attorney_email: - CommunicationLog.objects.create( - application=application, - logged_by=request.user, - external_attorney_name=external_attorney_name or None, - external_attorney_email=external_attorney_email or None, - message_content=comments or "Application forwarded by PCC Admin", - status_or_notes="Forwarded to Director", - ) - - _create_audit( - "PCC Forwarded Application", - request.user, - application, - f"Forwarded to director with attorney {attorney.name}", - ) - _notify( - application, - f"Application {application.title} has been forwarded to the Director.", - recipient_role="Director", - event_type="Status Update", - ) - - return JsonResponse({ - "message": "Application forwarded to director.", - "application_id": application.id, - "new_status": application.status, - "forwarded_to_director_date": application.forwarded_to_director_date, - "assigned_pcc_admin": request.user.get_full_name() or request.user.username, - "attorney_id": attorney.id, - "attorney_name": attorney.name, - "comments": comments - }) - - except Exception as e: - return JsonResponse({"error": str(e)}, status=500) - - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def request_application_modification(request, application_id): - if request.method == "POST": - try: - if not _is_pcc_admin_user(request.user): - return JsonResponse( - {"error": "Only PCC Admin can request application modification."}, - status=403, - ) - - # Validate if application_id is provided - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - # Fetch the application object from the database - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - # Check if the application is already in Draft status to prevent redundant updates - if application.status == "Draft": - return JsonResponse({"message": "Application is already in Draft status."}, status=400) - - allowed_statuses = [ - "Submitted", - "Reviewed by PCC Admin", - "Forwarded for Director's Review", - "Returned to Director", - ] - if application.status not in allowed_statuses: - return JsonResponse( - { - "error": ( - "Modification can be requested only before detailed patent processing starts. " - f"Current status: {application.status}" - ) - }, - status=400, - ) - - # Parse the request body for comments - try: - data = json.loads(request.body) - comments = _require_comments(data) - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON body."}, status=400) - except ValueError as exc: - return JsonResponse({"error": str(exc)}, status=400) - - # Move to revision state and notify applicant. - application.status = "Needs Revision" - application.revision_requested_at = now() - application.revision_due_date = (now() + timedelta(days=60)).date() - application.is_revision_locked = False - application.comments = comments - application.assigned_pcc_admin = request.user - application.save() - - applicant_user = ( - application.primary_applicant.user - if application.primary_applicant and application.primary_applicant.user_id - else None - ) - _notify( - application, - "PCC Admin requested modifications. Please update and resubmit.", - recipient=applicant_user, - recipient_role="Applicant", - event_type="Status Update", - due_date=application.revision_due_date, - ) - - # Return a success response - return JsonResponse({ - "message": "Application status updated to 'Needs Revision'.", - "application_id": application.id, - "new_status": application.status, - "last_updated_at": application.last_updated_at, - "revision_due_date": application.revision_due_date, - "comments": comments, - }) - - except Exception as e: - # Catch-all for any unexpected exceptions - return JsonResponse({"error": str(e)}, status=500) - - # Return error for methods other than POST - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -# For ongoing applications tab -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def ongoing_applications(request): - try: - REVIEW_STATUSES = [ - "Forwarded for Director's Review", - "Director's Approval Received", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Search Report Generated", - "Patent Filed", - "Patent Published", - "Patent Filed", - "Patent Published", - ] - - applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") - - - application_dict = {} # Using a dictionary instead of a list - - for app in applications: - try: - applicant = app.primary_applicant # Get the Applicant instance - - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None - - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None - designation_name = holds_designation.designation.name if holds_designation else "Unknown" - - # Format response as a dictionary - application_dict[app.id] = { - "token_no": app.token_no if app.token_no else "Token not generated yet", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", - "status": app.status, - } - except Exception as app_error: - logger.error(f"Error processing application {app.id}: {str(app_error)}") - continue - - return JsonResponse({"applications": application_dict}, safe=False) - except Exception as err: - logger.error(f"Error in ongoing_applications: {str(err)}") - return JsonResponse({"error": str(err)}, status=500) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def change_application_status(request, application_id): - REVIEW_STATUSES = [ - "Forwarded for Director's Review", - "Director's Approval Received", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Search Report Generated", - "Patent Filed", - "Patent Published", - "Patent Granted", - "Patent Refused", - ] - # Normalize status strings to protect transitions from stray whitespace in DB/UI payloads. - normalized_statuses = [status.strip() for status in REVIEW_STATUSES] - if request.method == "POST": - try: - # Validate if application_id is provided - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - # Fetch the application object from the database - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - # Parse the request body for the next status - try: - data = json.loads(request.body) - next_status = data.get("next_status", "").strip() # Remove leading/trailing whitespace - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON body."}, status=400) - - # Validate next_status field - if not next_status: - return JsonResponse({"error": "next_status is required."}, status=400) - if next_status not in normalized_statuses: - return JsonResponse({"error": f"Invalid next_status. Allowed statuses: {normalized_statuses}"}, status=400) - - if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: - owner = application.assigned_pcc_admin - owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") - return JsonResponse( - { - "error": f"Application is assigned to another PCC Admin: {owner_name}.", - "assigned_pcc_admin": owner_name, - }, - status=403, - ) - - # Check if the current status allows transitioning to the next status - current_status = (application.status or "").strip() - current_status_index = normalized_statuses.index(current_status) if current_status in normalized_statuses else -1 - next_status_index = normalized_statuses.index(next_status) - - if current_status_index == -1: - return JsonResponse( - { - "error": ( - "Current application status is not in ongoing workflow states. " - f"Current status: {application.status}" - ) - }, - status=400, - ) - - if next_status == "Patent Refused": - if current_status in ["Patent Granted", "Patent Refused"]: - return JsonResponse( - { - "error": ( - f"Invalid status transition from '{current_status}' to '{next_status}'. " - "The application is already in a terminal decision state." - ) - }, - status=400, - ) - - application.status = next_status - application.patent_refused_date = now() - application.decision_status = "Rejected" - application.decision_date = now() - application.save() - - return JsonResponse({ - "message": f"Application status updated to '{next_status}'.", - "application_id": application.id, - "new_status": application.status, - "last_updated_at": application.last_updated_at, - }) - - if next_status_index != current_status_index + 1: - allowed_next = normalized_statuses[current_status_index + 1] if current_status_index + 1 < len(normalized_statuses) else None - return JsonResponse( - { - "error": ( - f"Invalid status transition from '{current_status}' to '{next_status}'. " - f"Allowed next status: '{allowed_next}'." - ) - }, - status=400, - ) - - # Update application status and save - application.status = next_status - if application.status == "Patentability Check Started": - application.patentability_check_start_date = now() - elif application.status == "Patentability Check Completed": - application.patentability_check_completed_date = now() - elif application.status == "Patentability Search Report Generated": - application.search_report_generated_date = now() - elif application.status == "Patent Filed": - application.patent_filed_date = now() - elif application.status == "Patent Published": - application.patent_published_date = now() - elif application.status == "Patent Granted": - application.patent_granted_date = now() - application.decision_status = "Approved" - application.decision_date = now() - elif application.status == "Patent Refused": - application.patent_refused_date = now() - application.decision_status = "Rejected" - application.decision_date = now() - application.save() - - # Return a success response - return JsonResponse({ - "message": f"Application status updated to '{next_status}'.", - "application_id": application.id, - "new_status": application.status, - "last_updated_at": application.last_updated_at, - }) - - except Exception as e: - # Catch-all for any unexpected exceptions - return JsonResponse({"error": str(e)}, status=500) - - # Return error for methods other than POST - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -# For past applications tab -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def past_applications(request): - try: - DECISION_STATUSES = [ - "Approved", - "Rejected", - ] - - applications = Application.objects.filter(decision_status__in=DECISION_STATUSES).select_related("primary_applicant") - - application_dict = {} # Using a dictionary instead of a list - - for app in applications: - try: - applicant = app.primary_applicant # Get the Applicant instance - - # Ensure applicant exists and fetch the linked User - user = applicant.user if applicant else None - - # Fetch extra info (assuming ExtraInfo is linked to User) - extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - - # Fetch department - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - # Fetch designation (get latest held designation) - holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None - designation_name = holds_designation.designation.name if holds_designation else "Unknown" - - # Format response as a dictionary - application_dict[app.id] = { - "token_no": app.token_no if app.token_no else "Token not generated yet", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "designation": designation_name, - "department": department_name, - "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", - "decision_status": app.decision_status, - } - except Exception as app_error: - logger.error(f"Error processing application {app.id}: {str(app_error)}") - continue - - return JsonResponse({"applications": application_dict}, safe=False) - except Exception as err: - logger.error(f"Error in past_applications: {str(err)}") - return JsonResponse({"error": str(err)}, status=500) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def view_application_details_for_pccAdmin(request, application_id): - # Fetch application details - application = get_object_or_404(Application, id=application_id) - - # Fetch primary applicant details using primary_applicant_id - primary_applicant_name = None - if application.primary_applicant_id: - primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() - primary_applicant_name = primary_applicant.name if primary_applicant else None # Get primary applicant name safely - - handler_name = None - if application.assigned_pcc_admin_id: - handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username - - attorney_name = application.attorney.name if application.attorney else None - - # Fetch associated applicants - associated_applicants = AssociatedWith.objects.filter(application=application) - applicants_data = [ - { - "name": app.applicant.name, - "email": app.applicant.email, - "mobile": app.applicant.mobile, - "address": app.applicant.address, - "percentage_share": app.percentage_share - } - for app in associated_applicants - ] - - # Fetch Section I details - section_i = ApplicationSectionI.objects.filter(application=application).first() - section_i_data = { - "type_of_ip": section_i.type_of_ip if section_i else None, - "area": section_i.area if section_i else None, - "problem": section_i.problem if section_i else None, - "objective": section_i.objective if section_i else None, - "novelty": section_i.novelty if section_i else None, - "advantages": section_i.advantages if section_i else None, - "is_tested": section_i.is_tested if section_i else None, - "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, - "applications": section_i.applications if section_i else None, - } - - # Fetch Section II details - section_ii = ApplicationSectionII.objects.filter(application=application).first() - section_ii_data = { - "funding_details": section_ii.funding_details if section_ii else None, - "funding_source": section_ii.funding_source if section_ii else None, - "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, - "publication_details": section_ii.publication_details if section_ii else None, - "mou_details": section_ii.mou_details if section_ii else None, - "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, - "research_details": section_ii.research_details if section_ii else None - } - - # Fetch Section III details - section_iii = ApplicationSectionIII.objects.filter(application=application).first() - section_iii_data = { - "company_name": section_iii.company_name if section_iii else None, - "contact_person": section_iii.contact_person if section_iii else None, - "contact_no": section_iii.contact_no if section_iii else None, - "development_stage": section_iii.development_stage if section_iii else None, - "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None - } - - # Prepare response - response_data = { - "application_id": application.id, - "last_updated_at": application.last_updated_at, - "token_no": application.token_no, - "primary_applicant_name": primary_applicant_name, - "title": application.title, - "status": application.status, - "assigned_pcc_admin": handler_name, - "attorney_name": attorney_name, - "communication_logs": CommunicationLogSerializer(application.communication_logs.all(), many=True).data, - "dates": { - "submitted_date": application.submitted_date if application.submitted_date else None, - "reviewed_by_pcc_date": application.reviewed_by_pcc_date, - "forwarded_to_director_date": application.forwarded_to_director_date, - "director_approval_date": application.director_approval_date, - "patentability_check_start_date": application.patentability_check_start_date, - "patentability_check_completed_date": application.patentability_check_completed_date, - "search_report_generated_date": application.search_report_generated_date, - "patent_filed_date": application.patent_filed_date, - "patent_published_date": application.patent_published_date, - "decision_date": application.decision_date - }, - "decision_status": application.decision_status, - "comments": application.comments if application.comments else None, - "applicants": applicants_data, - "section_I": section_i_data, - "section_II": section_ii_data, - "section_III": section_iii_data - } - - return JsonResponse(response_data, safe=False) - -# ----------------------------------------- -# 🔹 Director Views -# ----------------------------------------- - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def attorney_forward_to_director(request, app_id): - if not _is_attorney_user(request.user): - return JsonResponse({"error": "Only Attorney users can forward applications to Director."}, status=403) - - application = get_object_or_404(Application, id=app_id) - attorney = _get_attorney_for_user(request.user) - if not attorney: - return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) - - if application.attorney_id != attorney.id: - return JsonResponse({"error": "This application is not assigned to the current attorney."}, status=403) - - allowed_statuses = {"Attorney Assigned", "Needs Revision", "Returned to Director"} - if application.status not in allowed_statuses: - return JsonResponse( - { - "error": ( - "Application must be in one of the statuses " - f"{sorted(allowed_statuses)}. Current status: {application.status}" - ) - }, - status=400, - ) - - try: - data = json.loads(request.body) - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON body."}, status=400) - - try: - comments = _require_comments(data, key="comments") - except ValueError as exc: - return JsonResponse({"error": str(exc)}, status=400) - - application.attorney_review_notes = comments - application.attorney_reviewed_at = now() - application.status = "Returned to Director" - application.decision_status = "Reviewed by Attorney" - application.save() - - _create_audit( - "Attorney Forwarded Application", - request.user, - application, - f"Attorney {attorney.name} forwarded application back to director", - ) - _notify( - application, - f"Attorney completed the assessment for {application.title} and returned it to Director.", - recipient_role="Director", - event_type="Status Update", - ) - - return JsonResponse( - { - "message": "Application returned to Director.", - "application_id": application.id, - "new_status": application.status, - "comments": comments, - } - ) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def attorney_applications(request): - if not _is_attorney_user(request.user): - return JsonResponse({"error": "Only Attorney users can access this queue."}, status=403) - - attorney = _get_attorney_for_user(request.user) - if not attorney: - return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) - - applications = ( - Application.objects.filter( - attorney=attorney, - status__in=["Attorney Assigned", "Returned to Director", "Needs Revision"], - ) - .select_related("primary_applicant", "attorney") - .order_by("-last_updated_at") - ) - - payload = [] - for application in applications: - ui_status = ( - "Needs Revision" - if application.status == "Needs Revision" or application.decision_status == "Needs Revision" - else application.status - ) - - payload.append( - { - "application_id": application.id, - "title": application.title, - "status": application.status, - "ui_status": ui_status, - "decision_status": application.decision_status, - "token_no": application.token_no, - "comments": application.comments, - "attorney_review_notes": application.attorney_review_notes, - "attorney_reviewed_at": application.attorney_reviewed_at, - "applicant_name": application.primary_applicant.name if application.primary_applicant else None, - "submitted_date": application.submitted_date, - } - ) - - return JsonResponse({"applications": payload}, safe=False) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_new_applications(request): - try: - applications = Application.objects.filter( - status__in=["Forwarded for Director's Review", "Returned to Director"] - ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") - - application_dict = {} - - for app in applications: - try: - applicant = app.primary_applicant - user = applicant.user if applicant else None - - # Get department name from ExtraInfo - extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" - assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" - - # Unique key for dictionary - key = app.id - - # Build the application summary - application_dict[key] = { - "token_no": app.token_no if app.token_no else "Token not generated", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "department": department_name, - "forwarded_on": app.forwarded_to_director_date.strftime("%Y-%m-%d") if app.forwarded_to_director_date else "Unknown", - "assigned_pcc_admin": assigned_pcc_admin, - "assigned_attorney": assigned_attorney, - "current_status": app.status, - } - except Exception as app_error: - logger.error(f"Error processing application {app.id}: {str(app_error)}") - continue - - return JsonResponse({"applications": application_dict}, safe=False) - except Exception as err: - logger.error(f"Error in director_new_applications: {str(err)}") - return JsonResponse({"error": str(err)}, status=500) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_reject(request): - if request.method == "POST": - try: - data = json.loads(request.body) - application_id = data.get("application_id") - comments = _require_comments(data) - - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: - return JsonResponse( - { - "error": ( - "Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status to reject. " - f"Current status: {application.status}" - ) - }, - status=400, - ) - - application.comments = comments - # Director rejection returns the application to PCC Admin for the next routing decision. - application.status = "Reviewed by PCC Admin" - application.decision_date = now() - application.decision_status = "Needs Revision" - application.is_revision_locked = True - application.save() - - CommunicationLog.objects.create( - application=application, - logged_by=request.user, - message_content=comments, - status_or_notes="Director requested revision", - ) - - _create_audit( - 'Director Rejected Application', - request.user, - application, - f'Director requested PCC Admin revision routing. Comments: {comments}', - ) - _notify( - application, - "Director requested revisions. PCC Admin will route feedback to the applicant.", - recipient_role="PCC Admin", - event_type="Status Update", - ) - - return JsonResponse({ - "message": "Application sent back to PCC Admin for further action", - "application_id": application.id, - "new_status": application.status, - "comments": comments, - }) - - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON."}, status=400) - except ValueError as exc: - return JsonResponse({"error": str(exc)}, status=400) - - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_accept(request): - if request.method == "POST": - try: - data = json.loads(request.body) - application_id = data.get("application_id") - comments = _require_comments(data) - - # Validate required fields - if not application_id: - return JsonResponse({"error": "Application ID is required."}, status=400) - - try: - application = Application.objects.get(id=application_id) - except Application.DoesNotExist: - return JsonResponse({"error": "Application not found."}, status=404) - - # Status check - if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: - return JsonResponse({ - "error": f"Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status. Current status: {application.status}" - }, status=400) - - # Get department name using your provided logic - applicant = application.primary_applicant - user = applicant.user if applicant else None - extra_info = ExtraInfo.objects.filter(user=user).first() if user else None - department_name = ( - extra_info.department.name[:3].upper() - if extra_info and extra_info.department - else "UNK" - ) - - # Retrieving the submission date - submitted_date = application.submitted_date - - # Generate reference number components - app_id_part = f"{application.id:06d}" # 6-digit format - handler_initials = ( - (application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username).replace(" ", "")[:3].upper() - if application.assigned_pcc_admin - else "PCA" - ) - - # Generate serial number (example implementation - adjust as needed) - last_serial = Application.objects.filter( - token_no__isnull=False - ).order_by('-id').first() - serial_number = int(last_serial.token_no.split('/')[-1]) + 1 if last_serial else 104 - - # Construct the complete reference number - token_no = ( - f"IIITDMJ/" - f"{department_name}/" - f"{submitted_date}/" - f"{app_id_part}/" - f"{handler_initials}/" - f"{serial_number:03d}" # 3-digit serial number - ) - - # Update application fields - application.comments = comments - - # After director approval, application returns to PCC Admin workflow stage. - application.status = "Director's Approval Received" - application.director_approval_date = now() - application.decision_status = "Pending" - application.token_no = token_no - application.save() - - CommunicationLog.objects.create( - application=application, - logged_by=request.user, - message_content=comments, - status_or_notes="Director approved and forwarded", - ) - - _create_audit( - 'Director Approved Application', - request.user, - application, - f'Director approved application and returned it to PCC Admin stage. Comments: {comments}', - ) - _notify( - application, - f"Director approved application {application.title}.", - recipient_role="PCC Admin", - event_type="Status Update", - ) - - return JsonResponse({ - "message": "Director's Approval Received", - "application_id": application.id, - "new_status": application.status, - "token_no": token_no, - "assigned_pcc_admin": application.assigned_pcc_admin.get_full_name() if application.assigned_pcc_admin else None, - "attorney_name": application.attorney.name if application.attorney else None, - "comments": comments - }) - - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON."}, status=400) - except ValueError as exc: - return JsonResponse({"error": str(exc)}, status=400) - except Exception as e: - return JsonResponse({"error": str(e)}, status=500) - - return JsonResponse({"error": "Only POST requests are allowed."}, status=405) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_reviewed_applications(request): - # Define the list of statuses to include - reviewed_statuses = [ - "Director's Approval Received", - "Attorney Assigned", - "Returned to Director", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Search Report Generated", - "Patent Filed", - "Patent Published", - "Patent Granted", - "Patent Refused", - ] - - applications = Application.objects.filter( - status__in=reviewed_statuses - ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") - - application_dict = {} - - for app in applications: - applicant = app.primary_applicant - user = applicant.user if applicant else None - - # Get department name from ExtraInfo - extra_info = ExtraInfo.objects.filter(user=user).first() - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" - assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" - - # Unique key for dictionary - key = app.id - - # Build the application summary - application_dict[key] = { - "token_no": app.token_no if app.token_no else "Token not generated", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "department": department_name, - "arrival_date": app.forwarded_to_director_date if app.submitted_date else "Unknown", - "reviewed_date": app.decision_date if app.decision_date else "Unknown", - "assigned_pcc_admin": assigned_pcc_admin, - "assigned_attorney": assigned_attorney, - "current_status": app.status, - } - - return JsonResponse({"applications": application_dict}, safe=False) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def active_applications(request): - # Define statuses relevant to active applications - active_statuses = [ - "Director's Approval Received", - "Attorney Assigned", - "Returned to Director", - "Patentability Check Started", - "Patentability Check Completed", - "Patentability Search Report Generated", - "Patent Filed", - "Patent Published", - "Patent Granted", - "Patent Refused", - ] - - applications = Application.objects.filter( - status__in=active_statuses, - decision_status="Pending" - ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") - - application_dict = {} - - for app in applications: - applicant = app.primary_applicant - user = applicant.user if applicant else None - - # Get department name from ExtraInfo - extra_info = ExtraInfo.objects.filter(user=user).first() - department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" - - assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" - - # Unique key for dictionary - key = str(app.token_no) if app.token_no else f"app_{app.id}" - - # Build the application summary - application_dict[key] = { - "token_no": app.token_no if app.token_no else "Token not generated", - "title": app.title, - "submitted_by": applicant.name if applicant else "Unknown", - "department": department_name, - "submitted_on": app.submitted_date if app.submitted_date else "Unknown", - "assigned_pcc_admin": assigned_pcc_admin, - "assigned_attorney": assigned_pcc_admin, - "current_status": app.status, - } - - return JsonResponse({"applications": application_dict}, safe=False) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_notifications(request): - notifications = NotificationEvent.objects.filter( - Q(recipient=request.user) | Q(recipient__isnull=True) | Q(recipient_role__iexact="Director") - ).order_by("-created_at") - serializer = NotificationEventSerializer(notifications, many=True) - return Response(serializer.data) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_application_view(request): - if request.method != 'POST': - return JsonResponse({'error': 'Only POST method is allowed.'}, status=405) - - try: - data = json.loads(request.body) - application_id = data.get('application_id') - - if not application_id: - return JsonResponse({'error': 'application_id is required in the request body.'}, status=400) - - # Fetch application details - application = get_object_or_404(Application, id=application_id) - - # Fetch primary applicant details - primary_applicant_name = None - if application.primary_applicant_id: - primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() - primary_applicant_name = primary_applicant.name if primary_applicant else None - - handler_name = None - if application.assigned_pcc_admin_id: - handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username - - attorney_name = application.attorney.name if application.attorney else None - - # Fetch associated applicants - associated_applicants = AssociatedWith.objects.filter(application=application) - applicants_data = [ - { - "name": app.applicant.name, - "email": app.applicant.email, - "mobile": app.applicant.mobile, - "address": app.applicant.address, - "percentage_share": app.percentage_share - } - for app in associated_applicants - ] - - # Fetch Section I - section_i = ApplicationSectionI.objects.filter(application=application).first() - section_i_data = { - "type_of_ip": section_i.type_of_ip if section_i else None, - "area": section_i.area if section_i else None, - "problem": section_i.problem if section_i else None, - "objective": section_i.objective if section_i else None, - "novelty": section_i.novelty if section_i else None, - "advantages": section_i.advantages if section_i else None, - "is_tested": section_i.is_tested if section_i else None, - "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, - "applications": section_i.applications if section_i else None, - } - - # Fetch Section II - section_ii = ApplicationSectionII.objects.filter(application=application).first() - section_ii_data = { - "funding_details": section_ii.funding_details if section_ii else None, - "funding_source": section_ii.funding_source if section_ii else None, - "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, - "publication_details": section_ii.publication_details if section_ii else None, - "mou_details": section_ii.mou_details if section_ii else None, - "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, - "research_details": section_ii.research_details if section_ii else None - } - - # Fetch Section III - section_iii = ApplicationSectionIII.objects.filter(application=application).first() - section_iii_data = { - "company_name": section_iii.company_name if section_iii else None, - "contact_person": section_iii.contact_person if section_iii else None, - "contact_no": section_iii.contact_no if section_iii else None, - "development_stage": section_iii.development_stage if section_iii else None, - "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None - } - - # Prepare response - response_data = { - "application_id": application.id, - "last_updated_at": application.last_updated_at, - "token_no": application.token_no, - "primary_applicant_name": primary_applicant_name, - "title": application.title, - "status": application.status, - "assigned_pcc_admin": handler_name, - "attorney_name": attorney_name, - "dates": { - "submitted_date": application.submitted_date, - "reviewed_by_pcc_date": application.reviewed_by_pcc_date, - "forwarded_to_director_date": application.forwarded_to_director_date, - "director_approval_date": application.director_approval_date, - "patentability_check_start_date": application.patentability_check_start_date, - "patentability_check_completed_date": application.patentability_check_completed_date, - "search_report_generated_date": application.search_report_generated_date, - "patent_filed_date": application.patent_filed_date, - "patent_published_date": application.patent_published_date, - "decision_date": application.decision_date, - }, - "decision_status": application.decision_status, - "comments": application.comments, - "applicants": applicants_data, - "section_I": section_i_data, - "section_II": section_ii_data, - "section_III": section_iii_data - } - - return JsonResponse(response_data, safe=False) - - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON in request body.'}, status=400) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def communication_logs(request, application_id): - application = get_object_or_404(Application, id=application_id) - - if request.method == 'GET': - logs = application.communication_logs.select_related('logged_by').all() - serializer = CommunicationLogSerializer(logs, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - payload = request.data.copy() - serializer = CommunicationLogSerializer(data=payload) - if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - serializer.save(application=application, logged_by=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - -# ----------------------------------------- -# 🔹 PCC Admin Attorney Management Views -# ----------------------------------------- - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_attorney_list(request): - try: - # Get all attorneys with their application count - attorneys = Attorney.objects.annotate( - assigned_applications_count=Count('applications') - ).all() - - # Serialize the data - serializer = AttorneySerializer(attorneys, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def add_attorney(request): - try: - serializer = AttorneySerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -def remove_attorney(request, attorney_id): - try: - attorney = Attorney.objects.get(id=attorney_id) - attorney.delete() - return Response({'message': 'Attorney removed successfully'}, status=status.HTTP_200_OK) - except Attorney.DoesNotExist: - return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_attorney_applications(request, attorney_id): - try: - # Get the attorney - attorney = Attorney.objects.get(id=attorney_id) - - # Get all applications assigned to this attorney - applications = Application.objects.filter(attorney=attorney).values('id', 'title', 'status') - - # Get the count of assigned applications - assigned_count = applications.count() - - response_data = { - 'attorney_id': attorney.id, - 'attorney_name': attorney.name, - 'assigned_applications_count': assigned_count, - 'applications': list(applications) - } - - return Response(response_data, status=status.HTTP_200_OK) - except Attorney.DoesNotExist: - return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view(['PUT']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def update_attorney_details(request, attorney_id): - try: - attorney = Attorney.objects.get(id=attorney_id) - serializer = AttorneySerializer(attorney, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Attorney.DoesNotExist: - return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -# ----------------------------------------- -# 🔹 Document Management Views -# ----------------------------------------- - -@api_view(['GET', 'POST']) -@authentication_classes([TokenAuthentication]) -@permission_classes([IsAuthenticated]) -def manage_documents(request): - """ - GET: List all documents - POST: Create a new document - """ - if request.method == 'GET': - try: - documents = Document.objects.all().order_by('-created_at') - serializer = DocumentSerializer(documents, many=True) - return Response(serializer.data) - except Exception as e: - logger.error(f"Error fetching documents: {str(e)}") - return Response( - {'error': 'Failed to fetch documents'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - elif request.method == 'POST': - try: - serializer = DocumentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f"Error creating document: {str(e)}") - return Response( - {'error': 'Failed to create document'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - -@api_view(['DELETE']) -@authentication_classes([TokenAuthentication]) -@permission_classes([IsAuthenticated]) -def delete_document(request, document_id): - """ - Delete a document by ID - """ - try: - document = Document.objects.get(id=document_id) - document.delete() - return Response( - {'message': 'Document deleted successfully'}, - status=status.HTTP_200_OK - ) - except Document.DoesNotExist: - return Response( - {'error': 'Document not found'}, - status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - logger.error(f"Error deleting document: {str(e)}") - return Response( - {'error': 'Failed to delete document'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - -def _role_names(user): - names = set( - HoldsDesignation.objects.filter(user=user).values_list("designation__name", flat=True) - ) - return {name.lower() for name in names if name} - - -def _is_pcc_admin_user(user): - role_names = _role_names(user) - return any("pcc" in role and "admin" in role for role in role_names) - - -def _is_director_user(user): - role_names = _role_names(user) - return any("director" in role for role in role_names) - - -def _is_authorized_applicant_user(user): - role_names = _role_names(user) - allowed_roles = { - "student", - "alumini", - "professor", - "associate professor", - "assistant professor", - "research engineer", - "faculty", - } - return bool(role_names & allowed_roles) or Applicant.objects.filter(user=user).exists() - - -def _get_attorney_for_user(user): - if not user or not user.email: - return None - attorney = Attorney.objects.filter(email__iexact=user.email).first() - if attorney: - return attorney - full_name = user.get_full_name().strip() - if full_name: - attorney = Attorney.objects.filter(name__iexact=full_name).first() - return attorney - - -def _is_attorney_user(user): - role_names = _role_names(user) - if any("attorney" in role for role in role_names): - return True - return _get_attorney_for_user(user) is not None - - -def _require_comments(payload, key="comments"): - comments = (payload.get(key) or "").strip() - if not comments: - raise ValueError("Comments are required.") - if len(comments) > 1000: - raise ValueError("Comments too long. Max 1000 characters allowed.") - return comments - - -def _create_audit(action, actor, application=None, details=""): - AuditLog.objects.create(action=action, actor=actor, application=application, details=details) - - -def _notify(application, message, recipient=None, recipient_role=None, event_type="General", due_date=None): - NotificationEvent.objects.create( - application=application, - recipient=recipient, - recipient_role=recipient_role, - event_type=event_type, - message=message, - due_date=due_date, - ) - - -def _reviewer_workload(user): - return Application.objects.filter(assigned_pcc_admin=user, status__in=["Submitted", "Reviewed by PCC Admin", "Needs Revision"]).count() - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def withdraw_application(request, app_id): - application = get_object_or_404(Application, id=app_id) - if application.primary_applicant and application.primary_applicant.user_id != request.user.id: - return Response({'error': 'Only primary applicant can withdraw.'}, status=status.HTTP_403_FORBIDDEN) - if application.status in ["Patent Granted", "Patent Refused"]: - return Response({'error': 'Cannot withdraw after final decision.'}, status=status.HTTP_400_BAD_REQUEST) - application.status = "Withdrawn" - application.save(update_fields=['status', 'last_updated_at']) - _create_audit('Withdraw Application', request.user, application, 'Applicant withdrew application') - return Response({'message': 'Application withdrawn successfully.'}) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def resubmit_application(request, app_id): - application = get_object_or_404(Application, id=app_id) - is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id - is_pcc = _is_pcc_admin_user(request.user) - if not (is_owner or is_pcc): - return Response({'error': 'Only primary applicant can resubmit.'}, status=status.HTTP_403_FORBIDDEN) - if application.status != "Needs Revision": - return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) - if application.revision_due_date and timezone.now().date() > application.revision_due_date: - application.status = "Revision Expired" - application.save(update_fields=['status', 'last_updated_at']) - return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) - application.status = "Submitted" - application.revised_submitted_at = timezone.now() - application.is_revision_locked = True - application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) - _create_audit('Resubmit Application', request.user, application, 'Applicant resubmitted revised application') - return Response({'message': 'Application resubmitted successfully.'}) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def declare_conflict(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can declare conflict.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = ConflictDeclarationSerializer(data={ - 'application': application.id, - 'reviewer': request.user.id, - 'conflict_type': request.data.get('conflict_type', 'General Conflict'), - 'notes': request.data.get('notes', ''), - 'declaration_status': 'Declared', - }) - if serializer.is_valid(): - serializer.save() - application.status = "Submitted" - application.assigned_pcc_admin = None - application.save(update_fields=['status', 'assigned_pcc_admin', 'last_updated_at']) - _create_audit('Conflict Declared', request.user, application, serializer.validated_data.get('conflict_type', '')) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def submit_legal_assessment(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: - return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) - attorney_id = request.data.get('attorney') - attorney = get_object_or_404(Attorney, id=attorney_id) - serializer = LegalAssessmentSerializer(data={ - 'application': application.id, - 'attorney': attorney.id, - 'opinion': request.data.get('opinion', 'Review Needed'), - 'prior_art_summary': request.data.get('prior_art_summary', ''), - 'recommended_action': request.data.get('recommended_action', ''), - 'comments': request.data.get('comments', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def submit_legal_advice_memo(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = LegalAdviceMemoSerializer(data={ - 'application': application.id, - 'author': request.user.id, - 'summary': request.data.get('summary', ''), - 'recommendation': request.data.get('recommendation', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Legal Memo Submitted', request.user, application, 'Memo added') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def initiate_budget_approval(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - amount = Decimal(str(request.data.get('amount', '0'))) - threshold = Decimal(str(request.data.get('threshold', '50000'))) - serializer = BudgetApprovalSerializer(data={ - 'application': application.id, - 'requested_by': request.user.id, - 'amount': amount, - 'threshold': threshold, - 'status': 'Pending', - 'comments': request.data.get('comments', ''), - }) - if serializer.is_valid(): - serializer.save() - application.budget_status = 'Pending Approval' - application.budget_estimate = amount - application.save(update_fields=['budget_status', 'budget_estimate', 'last_updated_at']) - _notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') - _create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_decide_budget(request, app_id): - if not _is_director_user(request.user): - return Response({'error': 'Only Director can decide budget approval.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - budget = BudgetApproval.objects.filter(application=application).order_by('-created_at').first() - if not budget: - return Response({'error': 'No budget request found.'}, status=status.HTTP_404_NOT_FOUND) - decision = request.data.get('decision', '').strip().lower() - if decision not in ['approve', 'reject']: - return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) - budget.status = 'Approved' if decision == 'approve' else 'Rejected' - budget.decided_by = request.user - budget.decided_at = timezone.now() - budget.comments = request.data.get('comments', budget.comments) - budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) - application.budget_status = budget.status - application.save(update_fields=['budget_status', 'last_updated_at']) - _create_audit('Budget Decision', request.user, application, budget.status) - return Response({'message': f'Budget {budget.status.lower()} successfully.'}) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def initiate_external_filing(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = ExternalFilingRecordSerializer(data={ - 'application': application.id, - 'patent_office': request.data.get('patent_office', ''), - 'filing_reference': request.data.get('filing_reference', ''), - 'communication_notes': request.data.get('communication_notes', ''), - 'filed_by': request.user.id, - 'filing_date': request.data.get('filing_date'), - }) - if serializer.is_valid(): - serializer.save() - application.external_filing_status = 'Filed' - if application.status == 'Patentability Search Report Generated': - application.status = 'Patent Filed' - application.patent_filed_date = timezone.now().date() - application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) - _create_audit('External Filing Initiated', request.user, application, 'External filing recorded') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def add_office_action(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = OfficeActionSerializer(data={ - 'application': application.id, - 'office_name': request.data.get('office_name', ''), - 'action_reference': request.data.get('action_reference', ''), - 'action_summary': request.data.get('action_summary', ''), - 'due_date': request.data.get('due_date'), - 'status': 'Open', - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def respond_office_action(request, office_action_id): - office_action = get_object_or_404(OfficeAction, id=office_action_id) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can respond office action.'}, status=status.HTTP_403_FORBIDDEN) - serializer = OfficeActionResponseSerializer(data={ - 'office_action': office_action.id, - 'responder': request.user.id, - 'response_text': request.data.get('response_text', ''), - 'response_reference': request.data.get('response_reference', ''), - }) - if serializer.is_valid(): - serializer.save() - office_action.status = 'Responded' - office_action.save(update_fields=['status']) - _create_audit('Office Action Responded', request.user, office_action.application, office_action.action_reference) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def add_prior_art_reference(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = PriorArtReferenceSerializer(data={ - 'application': application.id, - 'reference_type': request.data.get('reference_type', ''), - 'citation': request.data.get('citation', ''), - 'notes': request.data.get('notes', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def submit_appeal(request, app_id): - application = get_object_or_404(Application, id=app_id) - if application.primary_applicant and application.primary_applicant.user_id != request.user.id: - return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) - serializer = AppealRequestSerializer(data={ - 'application': application.id, - 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), - 'grounds': request.data.get('grounds', ''), - 'status': 'Open', - }) - if serializer.is_valid(): - serializer.save() - _notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') - _create_audit('Appeal Submitted', request.user, application, 'Appeal opened') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def submit_licensing_request(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - serializer = LicensingRequestSerializer(data={ - 'application': application.id, - 'requester_name': request.data.get('requester_name', ''), - 'requester_org': request.data.get('requester_org', ''), - 'request_details': request.data.get('request_details', ''), - 'status': 'Pending', - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def collect_inventor_consents(request, app_id): - application = get_object_or_404(Application, id=app_id) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) - applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] - created = [] - for applicant in applicants: - if not applicant: - continue - consent, _ = InventorConsent.objects.get_or_create( - application=application, - applicant=applicant, - defaults={ - 'consent_given': False, - 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), - }, - ) - created.append(consent) - serializer = InventorConsentSerializer(created, many=True) - _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') - return Response(serializer.data) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def setup_maintenance_schedule(request, app_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) - application = get_object_or_404(Application, id=app_id) - due_date = request.data.get('due_date') - amount = request.data.get('amount') - serializer = MaintenanceScheduleSerializer(data={ - 'application': application.id, - 'due_date': due_date, - 'amount': amount, - 'status': 'Upcoming', - }) - if serializer.is_valid(): - serializer.save() - application.maintenance_tracking_active = True - application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) - _create_audit('Maintenance Schedule Created', request.user, application, f'Due: {due_date}') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def mark_maintenance_paid(request, schedule_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can mark maintenance paid.'}, status=status.HTTP_403_FORBIDDEN) - schedule = get_object_or_404(MaintenanceSchedule, id=schedule_id) - schedule.status = 'Paid' - schedule.paid_at = timezone.now() - schedule.save(update_fields=['status', 'paid_at']) - _create_audit('Maintenance Paid', request.user, schedule.application, f'Schedule {schedule.id}') - return Response({'message': 'Maintenance marked as paid.'}) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def reviewer_queue(request): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) - apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') - payload = [] - for app in apps: - score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 - app.priority_score = 10 if app.status == 'Needs Revision' else 5 - app.save(update_fields=['priority_score', 'last_updated_at']) - payload.append({ - 'id': app.id, - 'title': app.title, - 'status': app.status, - 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, - 'priority_score': app.priority_score, - 'reviewer_workload': score, - }) - payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) - return Response(payload) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def get_notifications(request): - role = (request.GET.get("role") or "").strip() - notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) - if role: - notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) - notifications = notifications.order_by('-created_at') - serializer = NotificationEventSerializer(notifications, many=True) - return Response(serializer.data) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def mark_notification_read(request, notification_id): - notification = get_object_or_404(NotificationEvent, id=notification_id) - if notification.recipient_id and notification.recipient_id != request.user.id: - return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) - notification.is_read = True - notification.save(update_fields=['is_read']) - return Response({'message': 'Notification marked as read.'}) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def get_audit_logs(request): - if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): - return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) - logs = AuditLog.objects.select_related('actor', 'application').all()[:300] - serializer = AuditLogSerializer(logs, many=True) - return Response(serializer.data) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def get_applicant_insights(request): - applicant = Applicant.objects.filter(user=request.user).first() - if not applicant: - return Response({'error': 'Applicant profile not found.'}, status=status.HTTP_404_NOT_FOUND) - apps = Application.objects.filter(primary_applicant=applicant) - summary = { - 'total': apps.count(), - 'draft': apps.filter(status='Draft').count(), - 'submitted': apps.filter(status='Submitted').count(), - 'under_review': apps.filter(status__in=['Reviewed by PCC Admin', "Forwarded for Director's Review"]).count(), - 'approved': apps.filter(status="Director's Approval Received").count(), - 'granted': apps.filter(status='Patent Granted').count(), - 'refused': apps.filter(status='Patent Refused').count(), - } - return Response(summary) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def director_insights(request): - if not _is_director_user(request.user): - return Response({'error': 'Only Director can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) - return _insights_response(request) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def upload_document_version(request, document_id): - document = get_object_or_404(Document, id=document_id) - if document.is_locked: - return Response({'error': 'Document is locked for new versions.'}, status=status.HTTP_400_BAD_REQUEST) - link = request.data.get('link') - if not link: - return Response({'error': 'link is required.'}, status=status.HTTP_400_BAD_REQUEST) - next_version = (document.versions.first().version_number + 1) if document.versions.exists() else document.current_version + 1 - serializer = DocumentVersionSerializer(data={ - 'document': document.id, - 'version_number': next_version, - 'link': link, - 'uploaded_by': request.user.id, - }) - if serializer.is_valid(): - serializer.save() - document.current_version = next_version - document.link = link - document.save(update_fields=['current_version', 'link', 'updated_at']) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def lock_document(request, document_id): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can lock documents.'}, status=status.HTTP_403_FORBIDDEN) - document = get_object_or_404(Document, id=document_id) - document.is_locked = True - document.save(update_fields=['is_locked', 'updated_at']) - return Response({'message': 'Document locked successfully.'}) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def pcc_insights(request): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) - return _insights_response(request) - - -def _insights_response(request): - base_qs = Application.objects.all() - available_years = sorted( - { - dt.year - for dt in base_qs.exclude(submitted_date__isnull=True).values_list('submitted_date', flat=True) - if dt - } - ) - if not available_years: - available_years = [timezone.now().year] - - requested_year = request.GET.get('year') - if requested_year and requested_year.isdigit() and int(requested_year) in available_years: - selected_year = int(requested_year) - else: - selected_year = available_years[-1] - - apps = base_qs.filter(submitted_date__year=selected_year) - color_map = { - 'Submitted': '#4D96FF', - 'Reviewed by PCC Admin': '#00B894', - "Forwarded for Director's Review": '#F5A623', - "Director's Approval Received": '#2ECC71', - 'Patent Filed': '#8E44AD', - 'Patent Granted': '#16A085', - 'Patent Refused': '#E74C3C', - 'Needs Revision': '#D35400', - 'Withdrawn': '#7F8C8D', - } - status_order = [ - 'Submitted', - 'Reviewed by PCC Admin', - "Forwarded for Director's Review", - "Director's Approval Received", - 'Patent Filed', - 'Patent Granted', - 'Patent Refused', - 'Needs Revision', - 'Withdrawn', - ] - - status_counts = apps.values('status').annotate(count=Count('id')) - status_lookup = {item['status']: item['count'] for item in status_counts} - applications = [ - { - 'label': status_name, - 'count': status_lookup.get(status_name, 0), - 'color': color_map.get(status_name, '#5B7CFA'), - } - for status_name in status_order - ] - - total = sum(item['count'] for item in applications) - payload = { - 'applications': applications, - 'available_years': available_years, - 'selected_year': selected_year, - 'total': total, - } - - if request.GET.get('format') == 'csv': - rows = ['status,count,percentage'] - for item in applications: - percentage = (item['count'] / total * 100) if total else 0 - rows.append(f"{item['label']},{item['count']},{percentage:.2f}") - return HttpResponse('\n'.join(rows), content_type='text/csv') - - return Response(payload) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def pcc_resubmit_application(request, app_id): - application = get_object_or_404(Application, id=app_id) - is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id - is_pcc = _is_pcc_admin_user(request.user) - if not (is_owner or is_pcc): - return Response({'error': 'Only primary applicant or PCC Admin can resubmit.'}, status=status.HTTP_403_FORBIDDEN) - if application.status != "Needs Revision": - return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) - if application.revision_due_date and timezone.now().date() > application.revision_due_date: - application.status = "Revision Expired" - application.save(update_fields=['status', 'last_updated_at']) - return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) - application.status = "Submitted" - application.revised_submitted_at = timezone.now() - application.is_revision_locked = True - application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) - _create_audit('Resubmit Application', request.user, application, 'Application resubmitted') - return Response({'message': 'Application resubmitted successfully.'}) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def legal_assessment_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = LegalAssessmentSerializer(application.legal_assessments.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) - if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: - return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) - attorney = get_object_or_404(Attorney, id=request.data.get('attorney')) - serializer = LegalAssessmentSerializer(data={ - 'application': application.id, - 'attorney': attorney.id, - 'opinion': request.data.get('opinion', 'Review Needed'), - 'prior_art_summary': request.data.get('prior_art_summary', ''), - 'recommended_action': request.data.get('recommended_action', ''), - 'comments': request.data.get('comments', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def budget_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = BudgetApprovalSerializer(application.budget_approvals.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) - amount = Decimal(str(request.data.get('amount', '0'))) - threshold = Decimal(str(request.data.get('threshold', '50000'))) - serializer = BudgetApprovalSerializer(data={ - 'application': application.id, - 'requested_by': request.user.id, - 'amount': amount, - 'threshold': threshold, - 'status': 'Pending', - 'comments': request.data.get('comments', ''), - }) - if serializer.is_valid(): - serializer.save() - application.budget_status = 'Pending Approval' - application.budget_estimate = amount - application.save(update_fields=['budget_status', 'budget_estimate', 'last_updated_at']) - _notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') - _create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def budget_decision_by_id(request, budget_id): - budget = get_object_or_404(BudgetApproval, id=budget_id) - if not (_is_director_user(request.user) or _is_pcc_admin_user(request.user)): - return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) - decision = request.data.get('decision', '').strip().lower() - if decision not in ['approve', 'reject']: - return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) - budget.status = 'Approved' if decision == 'approve' else 'Rejected' - budget.decided_by = request.user - budget.decided_at = timezone.now() - budget.comments = request.data.get('comments', budget.comments) - budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) - budget.application.budget_status = budget.status - budget.application.save(update_fields=['budget_status', 'last_updated_at']) - _create_audit('Budget Decision', request.user, budget.application, budget.status) - return Response({'message': f'Budget {budget.status.lower()} successfully.'}) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def external_filing_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = ExternalFilingRecordSerializer(application.external_filings.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) - serializer = ExternalFilingRecordSerializer(data={ - 'application': application.id, - 'patent_office': request.data.get('patent_office', ''), - 'filing_reference': request.data.get('filing_reference', ''), - 'communication_notes': request.data.get('communication_notes', ''), - 'filed_by': request.user.id, - 'filing_date': request.data.get('filing_date'), - }) - if serializer.is_valid(): - serializer.save() - application.external_filing_status = 'Filed' - if application.status == 'Patentability Search Report Generated': - application.status = 'Patent Filed' - application.patent_filed_date = timezone.now().date() - application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) - _create_audit('External Filing Initiated', request.user, application, 'External filing recorded') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def maintenance_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = MaintenanceScheduleSerializer(application.maintenance_schedules.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) - serializer = MaintenanceScheduleSerializer(data={ - 'application': application.id, - 'due_date': request.data.get('due_date'), - 'amount': request.data.get('amount'), - 'status': 'Upcoming', - }) - if serializer.is_valid(): - serializer.save() - application.maintenance_tracking_active = True - application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) - _create_audit('Maintenance Schedule Created', request.user, application, f"Due: {request.data.get('due_date')}") - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def queue_prioritized(request): - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) - apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') - payload = [] - for app in apps: - score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 - app.priority_score = 10 if app.status == 'Needs Revision' else 5 - app.save(update_fields=['priority_score', 'last_updated_at']) - payload.append({ - 'id': app.id, - 'title': app.title, - 'status': app.status, - 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, - 'priority_score': app.priority_score, - 'reviewer_workload': score, - }) - payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) - return Response(payload) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def notifications_root(request): - role = (request.GET.get("role") or "").strip() - notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) - if role: - notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) - notifications = notifications.order_by('-created_at') - serializer = NotificationEventSerializer(notifications, many=True) - return Response(serializer.data) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def audit_logs_root(request): - if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): - return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) - logs = AuditLog.objects.select_related('actor', 'application').all()[:300] - serializer = AuditLogSerializer(logs, many=True) - return Response(serializer.data) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def audit_logs_by_application(request, application_id): - if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): - return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) - logs = AuditLog.objects.filter(application_id=application_id).select_related('actor', 'application').all() - serializer = AuditLogSerializer(logs, many=True) - return Response(serializer.data) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def office_actions_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = OfficeActionSerializer(application.office_actions.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) - serializer = OfficeActionSerializer(data={ - 'application': application.id, - 'office_name': request.data.get('office_name', ''), - 'action_reference': request.data.get('action_reference', ''), - 'action_summary': request.data.get('action_summary', ''), - 'due_date': request.data.get('due_date'), - 'status': 'Open', - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def prior_art_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - q = request.GET.get('q', '').strip() - refs = application.prior_art_references.all() - if q: - refs = refs.filter(Q(citation__icontains=q) | Q(notes__icontains=q)) - serializer = PriorArtReferenceSerializer(refs, many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) - serializer = PriorArtReferenceSerializer(data={ - 'application': application.id, - 'reference_type': request.data.get('reference_type', ''), - 'citation': request.data.get('citation', ''), - 'notes': request.data.get('notes', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def appeals_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = AppealRequestSerializer(application.appeals.all(), many=True) - return Response(serializer.data) - if application.primary_applicant and application.primary_applicant.user_id != request.user.id: - return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) - serializer = AppealRequestSerializer(data={ - 'application': application.id, - 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), - 'grounds': request.data.get('grounds', ''), - 'status': 'Open', - }) - if serializer.is_valid(): - serializer.save() - _notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') - _create_audit('Appeal Submitted', request.user, application, 'Appeal opened') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def licensing_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = LicensingRequestSerializer(application.licensing_requests.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) - serializer = LicensingRequestSerializer(data={ - 'application': application.id, - 'requester_name': request.data.get('requester_name', ''), - 'requester_org': request.data.get('requester_org', ''), - 'request_details': request.data.get('request_details', ''), - 'status': 'Pending', - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def inventor_consents_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = InventorConsentSerializer(application.inventor_consents.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) - applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] - created = [] - for applicant in applicants: - if not applicant: - continue - consent, _ = InventorConsent.objects.get_or_create( - application=application, - applicant=applicant, - defaults={ - 'consent_given': False, - 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), - }, - ) - created.append(consent) - serializer = InventorConsentSerializer(created, many=True) - _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') - return Response(serializer.data) - - -@api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def legal_memos_api(request, app_id): - application = get_object_or_404(Application, id=app_id) - if request.method == 'GET': - serializer = LegalAdviceMemoSerializer(application.legal_memos.all(), many=True) - return Response(serializer.data) - if not _is_pcc_admin_user(request.user): - return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) - serializer = LegalAdviceMemoSerializer(data={ - 'application': application.id, - 'author': request.user.id, - 'summary': request.data.get('summary', ''), - 'recommendation': request.data.get('recommendation', ''), - }) - if serializer.is_valid(): - serializer.save() - _create_audit('Legal Memo Submitted', request.user, application, 'Memo added') - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([TokenAuthentication]) -def document_versions_api(request, document_id): - document = get_object_or_404(Document, id=document_id) - serializer = DocumentVersionSerializer(document.versions.all(), many=True) +import os +import json +import logging +from datetime import timedelta +from decimal import Decimal + +from django.http import JsonResponse, HttpResponse +from django.utils.timezone import now +from django.utils import timezone +from django.core.exceptions import ObjectDoesNotExist +from django.core.files.storage import default_storage +from django.shortcuts import get_object_or_404 +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import csrf_exempt +from django.db import transaction +from django.db.models import Q + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from django.db.models import Count + +from ..models import ( + Application, + ApplicationSectionI, + ApplicationSectionII, + ApplicationSectionIII, + AssociatedWith, + Applicant, + AppealRequest, + Attorney, + AuditLog, + BudgetApproval, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LegalAdviceMemo, + LegalAssessment, + LicensingRequest, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + OfficeActionResponse, + PriorArtReference, + CommunicationLog, + ConflictDeclaration, +) + +from applications.globals.models import ( + Designation, + DepartmentInfo, + ExtraInfo, + HoldsDesignation, +) + +from .serializers import ( + AppealRequestSerializer, + AttorneySerializer, + AuditLogSerializer, + BudgetApprovalSerializer, + CommunicationLogSerializer, + ConflictDeclarationSerializer, + DocumentSerializer, + DocumentVersionSerializer, + ExternalFilingRecordSerializer, + InventorConsentSerializer, + LegalAdviceMemoSerializer, + LegalAssessmentSerializer, + LicensingRequestSerializer, + MaintenanceScheduleSerializer, + NotificationEventSerializer, + OfficeActionResponseSerializer, + OfficeActionSerializer, + PriorArtReferenceSerializer, +) + +from ..selectors import ( + applicant_applications, + applications_by_status, + applications_by_decision_status, + get_communication_logs, + get_budget_approvals, + get_office_actions, + get_prior_art_references, + get_legal_assessments, + get_legal_advice_memos, + get_licensing_requests, + get_inventor_consents, + get_maintenance_schedules, + get_appeal_requests, + get_external_filing_records, + get_conflict_declarations, + get_documents, + get_document_versions, + get_attorney_applications, + get_pcc_admin_queue, + get_director_queue, + count_by_status, + count_by_decision_status, +) + +from ..services import ( + role_names_for_user as _role_names, + is_pcc_admin_user as _is_pcc_admin_user, + is_director_user as _is_director_user, + get_director_users as _get_director_users, + is_authorized_applicant_user as _is_authorized_applicant_user, + get_attorney_for_user as _get_attorney_for_user, + is_attorney_user as _is_attorney_user, + require_comments as _require_comments, + create_audit as _create_audit, + notify as _notify, + reviewer_workload as _reviewer_workload, + move_application_to_revision as _move_application_to_revision, + record_budget_request as _record_budget_request, +) + +logger = logging.getLogger(__name__) + + +def index(request): + return JsonResponse( + { + "message": "Patent Management module is running.", + "routes": [ + "/patentsystem/applicant/applications/submit/", + "/patentsystem/applicant/applications/", + "/patentsystem/pccAdmin/applications/new/", + "/patentsystem/pccAdmin/applications/past/", + "/patentsystem/director/applications/new/", + ], + } + ) + +# ----------------------------------------- +# 🔹 Applicant Views +# ----------------------------------------- + +def generate_file_path(folder, filename): + """Helper function to generate a unique file path.""" + base, extension = os.path.splitext(filename) + timestamp = now().strftime("%Y%m%d%H%M%S") + return os.path.join(f"patent/{folder}", f"{base}_{timestamp}{extension}") + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_application(request): + if request.method != "POST": + return JsonResponse({"error": "Invalid request method"}, status=405) + + try: + # Start a transaction + with transaction.atomic(): + json_data = request.POST.get("json_data") + if not json_data: + return JsonResponse({"error": "Missing JSON data"}, status=400) + + data = json.loads(json_data) + + print("Parsed data keys:", data.keys()) + + # Required file fields + poc_file = request.FILES.get("poc_details") + source_file = request.FILES.get("source_file") + mou_file = request.FILES.get("mou_file") + form_iii_file = request.FILES.get("form_iii") + + required_fields = [ + "title", "inventors", "area_of_invention", "problem_statement", "objective", "ip_type", + "novelty", "advantages", "tested_experimentally", "applications", + "funding_details", "funding_source", "publication_details", "mou_details", + "research_details", "company_details", + "development_stage" + ] + + for field in required_fields: + if field not in data: + return JsonResponse({"error": f"Missing required field: {field}"}, status=400) + + # Get the logged-in user + user = request.user + if not _is_authorized_applicant_user(user): + return JsonResponse( + { + "error": ( + "Only authorized applicants, including faculty roles, can submit patent applications." + ) + }, + status=403, + ) + + # Check if the user has an applicant profile, create one if not + applicant, created = Applicant.objects.get_or_create( + user=user, + defaults={ + "email": user.email, + "name": user.get_full_name() or user.username, + "mobile": "", + "address": "", + } + ) + + # Create application entry with the logged-in user as the primary applicant + application = Application.objects.create( + title=data["title"], + status="Submitted", + decision_status="Pending", + submitted_date=now(), + primary_applicant=applicant, + ) + + # Save file uploads and store paths + poc_file_path = None + source_file_path = None + mou_file_path = None + form_iii_file_path = None + + if poc_file: + poc_file_path = default_storage.save( + generate_file_path("Section-I/poc_details", poc_file.name), poc_file + ) + if source_file: + source_file_path = default_storage.save( + generate_file_path("Section-II/source_details", source_file.name), source_file + ) + if mou_file: + mou_file_path = default_storage.save( + generate_file_path("Section-II/mou_details", mou_file.name), mou_file + ) + if form_iii_file: + form_iii_file_path = default_storage.save( + generate_file_path("Section-III/form_iii", form_iii_file.name), form_iii_file + ) + + ApplicationSectionI.objects.create( + application=application, + type_of_ip=data["ip_type"], + area=data["area_of_invention"], + problem=data["problem_statement"], + objective=data["objective"], + novelty=data["novelty"], + advantages=data["advantages"], + is_tested=data["tested_experimentally"], + applications=data["applications"], + poc_details=poc_file_path + ) + + ApplicationSectionII.objects.create( + application=application, + funding_details=data["funding_details"], + funding_source=data["funding_source"], + source_agreement=source_file_path, + publication_details=data["publication_details"], + mou_details=data["mou_details"], + mou_file=mou_file_path, + research_details=data["research_details"] + ) + + # Process multiple companies + company_details = data.get("company_details", []) + if not isinstance(company_details, list): + return JsonResponse({"error": "company_details should be a list"}, status=400) + + for company in company_details: + company_name = company.get("company_name") + contact_person = company.get("contact_person") + contact_no = company.get("contact_no") + + if not (company_name and contact_person and contact_no): + return JsonResponse({"error": "Each company entry must have company_name, contact_person, and contact_no"}, status=400) + + ApplicationSectionIII.objects.create( + application=application, + company_name=company_name, + contact_person=contact_person, + contact_no=contact_no, + development_stage=data["development_stage"], + form_iii=form_iii_file_path + ) + + # Associate inventors with the application + for inventor in data["inventors"]: + email = inventor["institute_mail"] + percentage = inventor["percentage"] + name = inventor.get("name", "") + personal_mail = inventor.get("personal_mail", "") + mobile = inventor.get("mobile", "") + address = inventor.get("address", "") + + try: + user = User.objects.get(email=email) + applicant, created = Applicant.objects.update_or_create( + user=user, + defaults={ + "email": personal_mail, + "name": name, + "mobile": mobile, + "address": address, + } + ) + + AssociatedWith.objects.create( + application=application, + applicant=applicant, + percentage_share=percentage + ) + except User.DoesNotExist: + # This will rollback all database changes made in this transaction + return JsonResponse({"error": f"Inventor {email} not found in auth_user"}, status=404) + + # Generate token + application_id = application.id + application.save() + + return JsonResponse({ + "message": "Application submitted successfully", + "application_id": application_id, + }) + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON format"}, status=400) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def view_applications(request): + user_id = request.user.id + try: + # Get the applicant based on user_id + applicant = get_object_or_404(Applicant, user_id=user_id) + + # Get all application IDs associated with this applicant + associated_apps = AssociatedWith.objects.filter(applicant=applicant).values_list('application_id', flat=True) + + # Retrieve applications where the user is primary applicant or associated inventor + applications = ( + Application.objects.filter( + Q(primary_applicant=applicant) | Q(id__in=associated_apps) + ) + .select_related("attorney") + .distinct() + .order_by("-last_updated_at") + ) + + # Prepare response data + applications_data = [] + for app in applications: + applications_data.append({ + "application_id": app.id, + "title": app.title, + "token_no": app.token_no, + "application_number": app.token_no, + "attorney_name": app.attorney.name if app.attorney else None, + "submitted_date": app.submitted_date if app.submitted_date else None, + "status": app.status, + "decision_status": app.decision_status, + }) + + return JsonResponse({"applications": applications_data}, safe=False) + + except Applicant.DoesNotExist: + return JsonResponse({"error": "Applicant not found"}, status=404) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def view_application_details_for_applicant(request, application_id): + user = request.user + + # Check if the logged-in user is an applicant + try: + applicant = Applicant.objects.get(user=user) + except Applicant.DoesNotExist: + return JsonResponse({"error": "Unauthorized: User is not an applicant"}, status=403) + + # Fetch application details + application = get_object_or_404(Application, id=application_id) + + # Primary applicant or associated inventor can view details + is_primary_applicant = application.primary_applicant_id == applicant.id + is_associated = AssociatedWith.objects.filter(application_id=application_id, applicant=applicant).exists() + if not (is_primary_applicant or is_associated): + return JsonResponse({"error": "Forbidden: You are not associated with this application"}, status=403) + + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username + + director_name = None + if application.assigned_director_id: + director_name = application.assigned_director.get_full_name() or application.assigned_director.username + + attorney_name = application.attorney.name if application.attorney else None + + # Fetch associated applicants + associated_applicants = AssociatedWith.objects.filter(application=application) + applicants_data = [ + { + "name": app.applicant.name, + "email": app.applicant.email, + "mobile": app.applicant.mobile, + "address": app.applicant.address, + "percentage_share": app.percentage_share + } + for app in associated_applicants + ] + + # Fetch Section I details + section_i = ApplicationSectionI.objects.filter(application=application).first() + section_i_data = { + "type_of_ip": section_i.type_of_ip if section_i else None, + "type_of_ip": section_i.type_of_ip if section_i else None, + "area": section_i.area if section_i else None, + "problem": section_i.problem if section_i else None, + "objective": section_i.objective if section_i else None, + "novelty": section_i.novelty if section_i else None, + "advantages": section_i.advantages if section_i else None, + "is_tested": section_i.is_tested if section_i else None, + "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, + "applications": section_i.applications if section_i else None, + } + + # Fetch Section II details + section_ii = ApplicationSectionII.objects.filter(application=application).first() + section_ii_data = { + "funding_details": section_ii.funding_details if section_ii else None, + "funding_source": section_ii.funding_source if section_ii else None, + "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, + "publication_details": section_ii.publication_details if section_ii else None, + "mou_details": section_ii.mou_details if section_ii else None, + "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, + "research_details": section_ii.research_details if section_ii else None + } + + # Fetch Section III details + section_iii = ApplicationSectionIII.objects.filter(application=application).first() + section_iii_data = { + "company_name": section_iii.company_name if section_iii else None, + "contact_person": section_iii.contact_person if section_iii else None, + "contact_no": section_iii.contact_no if section_iii else None, + "development_stage": section_iii.development_stage if section_iii else None, + "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None + } + + # Prepare response + response_data = { + "application_id": application.id, + "title": application.title, + "status": application.status, + "token_no": application.token_no if application.token_no else "Token not generated", + "assigned_pcc_admin": handler_name, + "assigned_director": director_name, + "attorney_name": attorney_name, + "dates": { + "submitted_date": application.submitted_date if application.submitted_date else None, + "reviewed_by_pcc_date": application.reviewed_by_pcc_date, + "forwarded_to_director_date": application.forwarded_to_director_date, + "director_approval_date": application.director_approval_date, + "patentability_check_start_date": application.patentability_check_start_date, + "patentability_check_completed_date": application.patentability_check_completed_date, + "search_report_generated_date": application.search_report_generated_date, + "patent_filed_date": application.patent_filed_date, + "patent_published_date": application.patent_published_date, + "decision_date": application.decision_date + }, + "decision_status": application.decision_status, + "comments": application.comments if application.comments else None, + "budget_estimate": str(application.budget_estimate) if application.budget_estimate is not None else None, + "budget_status": application.budget_status, + "applicants": applicants_data, + "section_I": section_i_data, + "section_II": section_ii_data, + "section_III": section_iii_data + } + + return JsonResponse(response_data, safe=False) + +def saved_drafts(request): + return JsonResponse({"message": "save drafts"}) + +# ----------------------------------------- +# 🔹 PCC Admin Views +# ----------------------------------------- + +# For new applications tab +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def new_applications(request): + try: + REVIEW_STATUSES = ["Submitted", "Reviewed by PCC Admin"] + + applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") + + application_dict = {} # Using a dictionary instead of a list + + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance + + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None + + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" + + # Format response as a dictionary + application_dict[app.id] = { + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown" + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue + + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in new_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def review_application(request, application_id): + # Check if request method is POST + if request.method == "POST": + try: + # Validate that application_id is provided + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + # Try to fetch the application by its ID + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + # Enforce workflow stage: only Submitted applications can be reviewed. + if application.status == "Reviewed by PCC Admin": + return JsonResponse({"message": "Application already reviewed."}) + if application.status != "Submitted": + return JsonResponse( + { + "error": ( + "Only applications in 'Submitted' state can be reviewed. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + + # Parse JSON body + try: + data = json.loads(request.body) + comments = data.get("comments", "") + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + + # Update application status and review date + application.status = "Reviewed by PCC Admin" + if comments != "": + application.comments = comments + application.assigned_pcc_admin = request.user + application.reviewed_by_pcc_date = now() + application.save() + + # Return success response with updated status and date + return JsonResponse({ + "message": "Application status updated to 'Reviewed by PCC Admin'.", + "application_id": application.id, + "new_status": application.status, + "reviewed_by_pcc_date": application.reviewed_by_pcc_date, + }) + + # Handle invalid JSON (though not used directly here) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON."}, status=400) + + # Handle non-POST requests + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def forward_application(request, application_id): + if request.method == "POST": + try: + if not _is_pcc_admin_user(request.user): + return JsonResponse( + {"error": "Only PCC Admin can assign attorneys and forward applications."}, + status=403, + ) + + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + # Get the application + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + # Enforce workflow stage and PCC ownership. + if application.status == "Forwarded for Director's Review": + return JsonResponse({"message": "Application is already forwarded for Director's review."}, status=400) + if application.status != "Reviewed by PCC Admin": + return JsonResponse( + { + "error": ( + "Only applications in 'Reviewed by PCC Admin' state can be forwarded. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: + owner = application.assigned_pcc_admin + owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") + return JsonResponse( + { + "error": f"Application is assigned to another PCC Admin: {owner_name}.", + "assigned_pcc_admin": owner_name, + }, + status=403, + ) + + # Parse JSON body + try: + data = json.loads(request.body) + external_attorney_name = data.get("attorney_name", "").strip() + external_attorney_email = data.get("attorney_email", "").strip() + director_user_id = data.get("director_user_id") + budget_estimate = data.get("budget_estimate") + comments = _require_comments(data) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + if not external_attorney_name: + return JsonResponse({"error": "attorney_name is required in the request body."}, status=400) + + attorney = Attorney.objects.filter(name__iexact=external_attorney_name).first() + if not attorney: + return JsonResponse( + {"error": f"Attorney with name '{external_attorney_name}' not found."}, + status=404, + ) + + director_users = list(_get_director_users()) + if not director_users: + return JsonResponse( + {"error": "No Director user is configured. Please assign Director designation first."}, + status=400, + ) + + director_user = None + if director_user_id not in [None, ""]: + try: + director_user_id = int(director_user_id) + except (TypeError, ValueError): + return JsonResponse({"error": "director_user_id must be an integer."}, status=400) + + director_user = next((u for u in director_users if u.id == director_user_id), None) + if not director_user: + return JsonResponse( + {"error": "Selected director is invalid or does not hold Director role."}, + status=400, + ) + elif application.assigned_director_id: + director_user = application.assigned_director + elif len(director_users) == 1: + director_user = director_users[0] + else: + return JsonResponse( + { + "error": "director_user_id is required because multiple directors are available.", + "available_directors": [ + { + "id": user.id, + "username": user.username, + "full_name": (user.get_full_name() or user.username), + } + for user in director_users + ], + }, + status=400, + ) + + # Optional: Limit comment length + if comments and len(comments) > 1000: + return JsonResponse({"error": "Comments too long. Max 1000 characters allowed."}, status=400) + + if budget_estimate not in [None, ""]: + try: + budget_estimate = Decimal(str(budget_estimate)) + except Exception: + return JsonResponse({"error": "budget_estimate must be a valid number."}, status=400) + if budget_estimate < 0: + return JsonResponse({"error": "budget_estimate cannot be negative."}, status=400) + + application.budget_estimate = budget_estimate + application.budget_status = "Pending Approval" + else: + application.budget_status = application.budget_status or "Not Initiated" + + # Update the application + application.status = "Forwarded for Director's Review" + application.forwarded_to_director_date = now() + application.assigned_pcc_admin = request.user + application.assigned_director = director_user + application.attorney = attorney + application.comments = comments + application.save() + + if hasattr(attorney, "current_workload"): + attorney.current_workload = Application.objects.filter(attorney=attorney).count() + attorney.save(update_fields=["current_workload"]) + + if comments or external_attorney_name or external_attorney_email: + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + external_attorney_name=external_attorney_name or None, + external_attorney_email=external_attorney_email or None, + message_content=comments or "Application forwarded by PCC Admin", + status_or_notes="Forwarded to Director", + ) + + _create_audit( + "PCC Forwarded Application", + request.user, + application, + f"Forwarded to director with attorney {attorney.name}", + ) + _notify( + application, + f"Application {application.title} has been forwarded to Director {(director_user.get_full_name() or director_user.username)}.", + recipient=director_user, + event_type="Status Update", + ) + + return JsonResponse({ + "message": "Application forwarded to director.", + "application_id": application.id, + "new_status": application.status, + "forwarded_to_director_date": application.forwarded_to_director_date, + "assigned_pcc_admin": request.user.get_full_name() or request.user.username, + "assigned_director": director_user.get_full_name() or director_user.username, + "assigned_director_id": director_user.id, + "budget_estimate": str(application.budget_estimate) if application.budget_estimate is not None else None, + "budget_status": application.budget_status, + "attorney_id": attorney.id, + "attorney_name": attorney.name, + "comments": comments + }) + + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def request_application_modification(request, application_id): + if request.method == "POST": + try: + if not _is_pcc_admin_user(request.user): + return JsonResponse( + {"error": "Only PCC Admin can request application modification."}, + status=403, + ) + + # Validate if application_id is provided + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + # Fetch the application object from the database + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + # Check if the application is already in Draft status to prevent redundant updates + if application.status == "Draft": + return JsonResponse({"message": "Application is already in Draft status."}, status=400) + + allowed_statuses = [ + "Submitted", + "Reviewed by PCC Admin", + "Forwarded for Director's Review", + "Returned to Director", + ] + if application.status not in allowed_statuses: + return JsonResponse( + { + "error": ( + "Modification can be requested only before detailed patent processing starts. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + + # Parse the request body for comments + try: + data = json.loads(request.body) + comments = _require_comments(data) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + # Move to revision state and notify applicant. + application.status = "Needs Revision" + application.revision_requested_at = now() + application.revision_due_date = (now() + timedelta(days=60)).date() + application.is_revision_locked = False + application.comments = comments + application.assigned_pcc_admin = request.user + application.save() + + applicant_user = ( + application.primary_applicant.user + if application.primary_applicant and application.primary_applicant.user_id + else None + ) + _notify( + application, + "PCC Admin requested modifications. Please update and resubmit.", + recipient=applicant_user, + recipient_role="Applicant", + event_type="Status Update", + due_date=application.revision_due_date, + ) + + # Return a success response + return JsonResponse({ + "message": "Application status updated to 'Needs Revision'.", + "application_id": application.id, + "new_status": application.status, + "last_updated_at": application.last_updated_at, + "revision_due_date": application.revision_due_date, + "comments": comments, + }) + + except Exception as e: + # Catch-all for any unexpected exceptions + return JsonResponse({"error": str(e)}, status=500) + + # Return error for methods other than POST + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +# For ongoing applications tab +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def ongoing_applications(request): + try: + REVIEW_STATUSES = [ + "Forwarded for Director's Review", + "Director's Approval Received", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Search Report Generated", + "Patent Filed", + "Patent Published", + "Patent Filed", + "Patent Published", + ] + + applications = Application.objects.filter(status__in=REVIEW_STATUSES).select_related("primary_applicant") + + + application_dict = {} # Using a dictionary instead of a list + + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance + + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None + + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" + + # Format response as a dictionary + application_dict[app.id] = { + "token_no": app.token_no if app.token_no else "Token not generated yet", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", + "status": app.status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue + + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in ongoing_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def change_application_status(request, application_id): + REVIEW_STATUSES = [ + "Forwarded for Director's Review", + "Director's Approval Received", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Search Report Generated", + "Patent Filed", + "Patent Published", + "Patent Granted", + "Patent Refused", + ] + # Normalize status strings to protect transitions from stray whitespace in DB/UI payloads. + normalized_statuses = [status.strip() for status in REVIEW_STATUSES] + if request.method == "POST": + try: + # Validate if application_id is provided + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + # Fetch the application object from the database + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + # Parse the request body for the next status + try: + data = json.loads(request.body) + next_status = data.get("next_status", "").strip() # Remove leading/trailing whitespace + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + + # Validate next_status field + if not next_status: + return JsonResponse({"error": "next_status is required."}, status=400) + if next_status not in normalized_statuses: + return JsonResponse({"error": f"Invalid next_status. Allowed statuses: {normalized_statuses}"}, status=400) + + if application.assigned_pcc_admin_id and application.assigned_pcc_admin_id != request.user.id: + owner = application.assigned_pcc_admin + owner_name = owner.get_full_name().strip() if owner and owner.get_full_name() else (owner.username if owner else "Unknown") + return JsonResponse( + { + "error": f"Application is assigned to another PCC Admin: {owner_name}.", + "assigned_pcc_admin": owner_name, + }, + status=403, + ) + + # Check if the current status allows transitioning to the next status + current_status = (application.status or "").strip() + current_status_index = normalized_statuses.index(current_status) if current_status in normalized_statuses else -1 + next_status_index = normalized_statuses.index(next_status) + + if current_status_index == -1: + return JsonResponse( + { + "error": ( + "Current application status is not in ongoing workflow states. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + + if next_status == "Patent Refused": + if current_status in ["Patent Granted", "Patent Refused"]: + return JsonResponse( + { + "error": ( + f"Invalid status transition from '{current_status}' to '{next_status}'. " + "The application is already in a terminal decision state." + ) + }, + status=400, + ) + + application.status = next_status + application.patent_refused_date = now() + application.decision_status = "Rejected" + application.decision_date = now() + application.save() + + return JsonResponse({ + "message": f"Application status updated to '{next_status}'.", + "application_id": application.id, + "new_status": application.status, + "last_updated_at": application.last_updated_at, + }) + + if next_status_index != current_status_index + 1: + allowed_next = normalized_statuses[current_status_index + 1] if current_status_index + 1 < len(normalized_statuses) else None + return JsonResponse( + { + "error": ( + f"Invalid status transition from '{current_status}' to '{next_status}'. " + f"Allowed next status: '{allowed_next}'." + ) + }, + status=400, + ) + + # Update application status and save + application.status = next_status + if application.status == "Patentability Check Started": + application.patentability_check_start_date = now() + elif application.status == "Patentability Check Completed": + application.patentability_check_completed_date = now() + elif application.status == "Patentability Search Report Generated": + application.search_report_generated_date = now() + elif application.status == "Patent Filed": + application.patent_filed_date = now() + elif application.status == "Patent Published": + application.patent_published_date = now() + elif application.status == "Patent Granted": + application.patent_granted_date = now() + application.decision_status = "Approved" + application.decision_date = now() + elif application.status == "Patent Refused": + application.patent_refused_date = now() + application.decision_status = "Rejected" + application.decision_date = now() + application.save() + + # Return a success response + return JsonResponse({ + "message": f"Application status updated to '{next_status}'.", + "application_id": application.id, + "new_status": application.status, + "last_updated_at": application.last_updated_at, + }) + + except Exception as e: + # Catch-all for any unexpected exceptions + return JsonResponse({"error": str(e)}, status=500) + + # Return error for methods other than POST + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +# For past applications tab +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def past_applications(request): + try: + DECISION_STATUSES = [ + "Approved", + "Rejected", + ] + + applications = Application.objects.filter(decision_status__in=DECISION_STATUSES).select_related("primary_applicant") + + application_dict = {} # Using a dictionary instead of a list + + for app in applications: + try: + applicant = app.primary_applicant # Get the Applicant instance + + # Ensure applicant exists and fetch the linked User + user = applicant.user if applicant else None + + # Fetch extra info (assuming ExtraInfo is linked to User) + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + + # Fetch department + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + # Fetch designation (get latest held designation) + holds_designation = HoldsDesignation.objects.filter(user=user).select_related("designation").first() if user else None + designation_name = holds_designation.designation.name if holds_designation else "Unknown" + + # Format response as a dictionary + application_dict[app.id] = { + "token_no": app.token_no if app.token_no else "Token not generated yet", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "designation": designation_name, + "department": department_name, + "submitted_on": app.submitted_date.strftime("%Y-%m-%d") if app.submitted_date else "Unknown", + "decision_status": app.decision_status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue + + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in past_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def view_application_details_for_pccAdmin(request, application_id): + # Fetch application details + application = get_object_or_404(Application, id=application_id) + + # Fetch primary applicant details using primary_applicant_id + primary_applicant_name = None + if application.primary_applicant_id: + primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() + primary_applicant_name = primary_applicant.name if primary_applicant else None # Get primary applicant name safely + + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username + + director_name = None + if application.assigned_director_id: + director_name = application.assigned_director.get_full_name() or application.assigned_director.username + + attorney_name = application.attorney.name if application.attorney else None + + # Fetch associated applicants + associated_applicants = AssociatedWith.objects.filter(application=application) + applicants_data = [ + { + "name": app.applicant.name, + "email": app.applicant.email, + "mobile": app.applicant.mobile, + "address": app.applicant.address, + "percentage_share": app.percentage_share + } + for app in associated_applicants + ] + + # Fetch Section I details + section_i = ApplicationSectionI.objects.filter(application=application).first() + section_i_data = { + "type_of_ip": section_i.type_of_ip if section_i else None, + "area": section_i.area if section_i else None, + "problem": section_i.problem if section_i else None, + "objective": section_i.objective if section_i else None, + "novelty": section_i.novelty if section_i else None, + "advantages": section_i.advantages if section_i else None, + "is_tested": section_i.is_tested if section_i else None, + "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, + "applications": section_i.applications if section_i else None, + } + + # Fetch Section II details + section_ii = ApplicationSectionII.objects.filter(application=application).first() + section_ii_data = { + "funding_details": section_ii.funding_details if section_ii else None, + "funding_source": section_ii.funding_source if section_ii else None, + "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, + "publication_details": section_ii.publication_details if section_ii else None, + "mou_details": section_ii.mou_details if section_ii else None, + "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, + "research_details": section_ii.research_details if section_ii else None + } + + # Fetch Section III details + section_iii = ApplicationSectionIII.objects.filter(application=application).first() + section_iii_data = { + "company_name": section_iii.company_name if section_iii else None, + "contact_person": section_iii.contact_person if section_iii else None, + "contact_no": section_iii.contact_no if section_iii else None, + "development_stage": section_iii.development_stage if section_iii else None, + "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None + } + + # Prepare response + response_data = { + "application_id": application.id, + "last_updated_at": application.last_updated_at, + "token_no": application.token_no, + "primary_applicant_name": primary_applicant_name, + "title": application.title, + "status": application.status, + "assigned_pcc_admin": handler_name, + "assigned_director": director_name, + "attorney_name": attorney_name, + "communication_logs": CommunicationLogSerializer(application.communication_logs.all(), many=True).data, + "dates": { + "submitted_date": application.submitted_date if application.submitted_date else None, + "reviewed_by_pcc_date": application.reviewed_by_pcc_date, + "forwarded_to_director_date": application.forwarded_to_director_date, + "director_approval_date": application.director_approval_date, + "patentability_check_start_date": application.patentability_check_start_date, + "patentability_check_completed_date": application.patentability_check_completed_date, + "search_report_generated_date": application.search_report_generated_date, + "patent_filed_date": application.patent_filed_date, + "patent_published_date": application.patent_published_date, + "decision_date": application.decision_date + }, + "decision_status": application.decision_status, + "comments": application.comments if application.comments else None, + "budget_estimate": str(application.budget_estimate) if application.budget_estimate is not None else None, + "budget_status": application.budget_status, + "applicants": applicants_data, + "section_I": section_i_data, + "section_II": section_ii_data, + "section_III": section_iii_data + } + + return JsonResponse(response_data, safe=False) + +# ----------------------------------------- +# 🔹 Director Views +# ----------------------------------------- + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def attorney_forward_to_director(request, app_id): + if not _is_attorney_user(request.user): + return JsonResponse({"error": "Only Attorney users can forward applications to Director."}, status=403) + + application = get_object_or_404(Application, id=app_id) + attorney = _get_attorney_for_user(request.user) + if not attorney: + return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) + + if application.attorney_id != attorney.id: + return JsonResponse({"error": "This application is not assigned to the current attorney."}, status=403) + + if application.status != "Attorney Assigned": + return JsonResponse( + {"error": f"Application must be in 'Attorney Assigned' status. Current status: {application.status}"}, + status=400, + ) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON body."}, status=400) + + try: + comments = _require_comments(data, key="comments") + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + application.attorney_review_notes = comments + application.attorney_reviewed_at = now() + application.status = "Returned to Director" + application.decision_status = "Reviewed by Attorney" + application.save() + + _create_audit( + "Attorney Forwarded Application", + request.user, + application, + f"Attorney {attorney.name} forwarded application back to director", + ) + _notify( + application, + f"Attorney completed the assessment for {application.title} and returned it to Director.", + recipient_role="Director", + event_type="Status Update", + ) + + return JsonResponse( + { + "message": "Application returned to Director.", + "application_id": application.id, + "new_status": application.status, + "comments": comments, + } + ) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def attorney_applications(request): + if not _is_attorney_user(request.user): + return JsonResponse({"error": "Only Attorney users can access this queue."}, status=403) + + attorney = _get_attorney_for_user(request.user) + if not attorney: + return JsonResponse({"error": "No attorney profile is linked to this account."}, status=403) + + applications = ( + Application.objects.filter(attorney=attorney, status__in=["Attorney Assigned", "Returned to Director"]) + .select_related("primary_applicant", "attorney") + .order_by("-last_updated_at") + ) + + payload = [] + for application in applications: + payload.append( + { + "application_id": application.id, + "title": application.title, + "status": application.status, + "token_no": application.token_no, + "comments": application.comments, + "attorney_review_notes": application.attorney_review_notes, + "attorney_reviewed_at": application.attorney_reviewed_at, + "applicant_name": application.primary_applicant.name if application.primary_applicant else None, + "submitted_date": application.submitted_date, + } + ) + + return JsonResponse({"applications": payload}, safe=False) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_new_applications(request): + try: + if not _is_director_user(request.user): + return JsonResponse({"error": "Only Director can access this queue."}, status=403) + + applications = Application.objects.filter( + status__in=["Forwarded for Director's Review", "Returned to Director"] + ).filter( + Q(assigned_director=request.user) | Q(assigned_director__isnull=True) + ).select_related("primary_applicant", "assigned_pcc_admin", "assigned_director", "attorney") + + application_dict = {} + + for app in applications: + try: + applicant = app.primary_applicant + user = applicant.user if applicant else None + + # Get department name from ExtraInfo + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" + assigned_director = app.assigned_director.get_full_name() if app.assigned_director else "Not Assigned" + assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" + + # Unique key for dictionary + key = app.id + + # Build the application summary + application_dict[key] = { + "token_no": app.token_no if app.token_no else "Token not generated", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "department": department_name, + "forwarded_on": app.forwarded_to_director_date.strftime("%Y-%m-%d") if app.forwarded_to_director_date else "Unknown", + "assigned_pcc_admin": assigned_pcc_admin, + "assigned_director": assigned_director, + "assigned_attorney": assigned_attorney, + "budget_estimate": str(app.budget_estimate) if app.budget_estimate is not None else None, + "budget_status": app.budget_status, + "current_status": app.status, + } + except Exception as app_error: + logger.error(f"Error processing application {app.id}: {str(app_error)}") + continue + + return JsonResponse({"applications": application_dict}, safe=False) + except Exception as err: + logger.error(f"Error in director_new_applications: {str(err)}") + return JsonResponse({"error": str(err)}, status=500) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_reject(request): + if request.method == "POST": + try: + if not _is_director_user(request.user): + return JsonResponse({"error": "Only Director can reject applications."}, status=403) + + data = json.loads(request.body) + application_id = data.get("application_id") + comments = _require_comments(data) + + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: + return JsonResponse( + { + "error": ( + "Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status to reject. " + f"Current status: {application.status}" + ) + }, + status=400, + ) + + if application.assigned_director_id and application.assigned_director_id != request.user.id: + assigned_name = application.assigned_director.get_full_name() or application.assigned_director.username + return JsonResponse( + {"error": f"Application is assigned to another director: {assigned_name}."}, + status=403, + ) + if not application.assigned_director_id: + application.assigned_director = request.user + + application.comments = comments + # Director rejection returns the application to PCC Admin for the next routing decision. + application.status = "Reviewed by PCC Admin" + application.decision_date = now() + application.decision_status = "Needs Revision" + application.is_revision_locked = True + application.save() + + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + message_content=comments, + status_or_notes="Director requested revision", + ) + + _create_audit( + 'Director Rejected Application', + request.user, + application, + f'Director requested PCC Admin revision routing. Comments: {comments}', + ) + _notify( + application, + "Director requested revisions. PCC Admin will route feedback to the applicant.", + recipient_role="PCC Admin", + event_type="Status Update", + ) + + return JsonResponse({ + "message": "Application sent back to PCC Admin for further action", + "application_id": application.id, + "new_status": application.status, + "comments": comments, + }) + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_accept(request): + if request.method == "POST": + try: + if not _is_director_user(request.user): + return JsonResponse({"error": "Only Director can approve applications."}, status=403) + + data = json.loads(request.body) + application_id = data.get("application_id") + comments = _require_comments(data) + + # Validate required fields + if not application_id: + return JsonResponse({"error": "Application ID is required."}, status=400) + + try: + application = Application.objects.get(id=application_id) + except Application.DoesNotExist: + return JsonResponse({"error": "Application not found."}, status=404) + + # Status check + if application.status not in ["Forwarded for Director's Review", "Returned to Director"]: + return JsonResponse({ + "error": f"Application must be in 'Forwarded for Director's Review' or 'Returned to Director' status. Current status: {application.status}" + }, status=400) + + if application.assigned_director_id and application.assigned_director_id != request.user.id: + assigned_name = application.assigned_director.get_full_name() or application.assigned_director.username + return JsonResponse( + {"error": f"Application is assigned to another director: {assigned_name}."}, + status=403, + ) + if not application.assigned_director_id: + application.assigned_director = request.user + + # Get department name using your provided logic + applicant = application.primary_applicant + user = applicant.user if applicant else None + extra_info = ExtraInfo.objects.filter(user=user).first() if user else None + department_name = ( + extra_info.department.name[:3].upper() + if extra_info and extra_info.department + else "UNK" + ) + + # Retrieving the submission date + submitted_date = application.submitted_date + + # Generate reference number components + app_id_part = f"{application.id:06d}" # 6-digit format + handler_initials = ( + (application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username).replace(" ", "")[:3].upper() + if application.assigned_pcc_admin + else "PCA" + ) + + # Generate serial number (example implementation - adjust as needed) + last_serial = Application.objects.filter( + token_no__isnull=False + ).order_by('-id').first() + serial_number = int(last_serial.token_no.split('/')[-1]) + 1 if last_serial else 104 + + # Construct the complete reference number + token_no = ( + f"IIITDMJ/" + f"{department_name}/" + f"{submitted_date}/" + f"{app_id_part}/" + f"{handler_initials}/" + f"{serial_number:03d}" # 3-digit serial number + ) + + # Update application fields + application.comments = comments + + # After director approval, application returns to PCC Admin workflow stage. + application.status = "Director's Approval Received" + application.director_approval_date = now() + application.decision_status = "Pending" + application.token_no = token_no + application.save() + + CommunicationLog.objects.create( + application=application, + logged_by=request.user, + message_content=comments, + status_or_notes="Director approved and forwarded", + ) + + _create_audit( + 'Director Approved Application', + request.user, + application, + f'Director approved application and returned it to PCC Admin stage. Comments: {comments}', + ) + _notify( + application, + f"Director approved application {application.title}.", + recipient_role="PCC Admin", + event_type="Status Update", + ) + + return JsonResponse({ + "message": "Director's Approval Received", + "application_id": application.id, + "new_status": application.status, + "token_no": token_no, + "assigned_pcc_admin": application.assigned_pcc_admin.get_full_name() if application.assigned_pcc_admin else None, + "attorney_name": application.attorney.name if application.attorney else None, + "comments": comments + }) + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON."}, status=400) + except ValueError as exc: + return JsonResponse({"error": str(exc)}, status=400) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + + return JsonResponse({"error": "Only POST requests are allowed."}, status=405) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_reviewed_applications(request): + if not _is_director_user(request.user): + return JsonResponse({"error": "Only Director can access this queue."}, status=403) + + # Define the list of statuses to include + reviewed_statuses = [ + "Director's Approval Received", + "Attorney Assigned", + "Returned to Director", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Search Report Generated", + "Patent Filed", + "Patent Published", + "Patent Granted", + "Patent Refused", + ] + + applications = Application.objects.filter( + status__in=reviewed_statuses + ).filter( + Q(assigned_director=request.user) | Q(assigned_director__isnull=True) + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") + + application_dict = {} + + for app in applications: + applicant = app.primary_applicant + user = applicant.user if applicant else None + + # Get department name from ExtraInfo + extra_info = ExtraInfo.objects.filter(user=user).first() + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" + assigned_attorney = app.attorney.name if app.attorney else "Attorney not assigned" + + # Unique key for dictionary + key = app.id + + # Build the application summary + application_dict[key] = { + "token_no": app.token_no if app.token_no else "Token not generated", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "department": department_name, + "arrival_date": app.forwarded_to_director_date if app.submitted_date else "Unknown", + "reviewed_date": app.decision_date if app.decision_date else "Unknown", + "assigned_pcc_admin": assigned_pcc_admin, + "assigned_attorney": assigned_attorney, + "budget_estimate": str(app.budget_estimate) if app.budget_estimate is not None else None, + "budget_status": app.budget_status, + "current_status": app.status, + } + + return JsonResponse({"applications": application_dict}, safe=False) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def active_applications(request): + if not _is_director_user(request.user): + return JsonResponse({"error": "Only Director can access this queue."}, status=403) + + # Define statuses relevant to active applications + active_statuses = [ + "Director's Approval Received", + "Attorney Assigned", + "Returned to Director", + "Patentability Check Started", + "Patentability Check Completed", + "Patentability Search Report Generated", + "Patent Filed", + "Patent Published", + "Patent Granted", + "Patent Refused", + ] + + applications = Application.objects.filter( + status__in=active_statuses, + decision_status="Pending" + ).filter( + Q(assigned_director=request.user) | Q(assigned_director__isnull=True) + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") + + application_dict = {} + + for app in applications: + applicant = app.primary_applicant + user = applicant.user if applicant else None + + # Get department name from ExtraInfo + extra_info = ExtraInfo.objects.filter(user=user).first() + department_name = extra_info.department.name if extra_info and extra_info.department else "Unknown" + + assigned_pcc_admin = app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else "PCC Admin" + + # Unique key for dictionary + key = str(app.token_no) if app.token_no else f"app_{app.id}" + + # Build the application summary + application_dict[key] = { + "token_no": app.token_no if app.token_no else "Token not generated", + "title": app.title, + "submitted_by": applicant.name if applicant else "Unknown", + "department": department_name, + "submitted_on": app.submitted_date if app.submitted_date else "Unknown", + "assigned_pcc_admin": assigned_pcc_admin, + "assigned_attorney": assigned_pcc_admin, + "budget_estimate": str(app.budget_estimate) if app.budget_estimate is not None else None, + "budget_status": app.budget_status, + "current_status": app.status, + } + + return JsonResponse({"applications": application_dict}, safe=False) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_notifications(request): + notifications = NotificationEvent.objects.filter( + Q(recipient=request.user) | Q(recipient__isnull=True) | Q(recipient_role__iexact="Director") + ).order_by("-created_at") + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_application_view(request): + if request.method != 'POST': + return JsonResponse({'error': 'Only POST method is allowed.'}, status=405) + + try: + if not _is_director_user(request.user): + return JsonResponse({'error': 'Only Director can access this view.'}, status=403) + + data = json.loads(request.body) + application_id = data.get('application_id') + + if not application_id: + return JsonResponse({'error': 'application_id is required in the request body.'}, status=400) + + # Fetch application details + application = get_object_or_404(Application, id=application_id) + + if application.assigned_director_id and application.assigned_director_id != request.user.id: + assigned_name = application.assigned_director.get_full_name() or application.assigned_director.username + return JsonResponse({'error': f'Application is assigned to another director: {assigned_name}.'}, status=403) + + # Fetch primary applicant details + primary_applicant_name = None + if application.primary_applicant_id: + primary_applicant = Applicant.objects.filter(id=application.primary_applicant_id).first() + primary_applicant_name = primary_applicant.name if primary_applicant else None + + handler_name = None + if application.assigned_pcc_admin_id: + handler_name = application.assigned_pcc_admin.get_full_name() or application.assigned_pcc_admin.username + + director_name = None + if application.assigned_director_id: + director_name = application.assigned_director.get_full_name() or application.assigned_director.username + + attorney_name = application.attorney.name if application.attorney else None + + # Fetch associated applicants + associated_applicants = AssociatedWith.objects.filter(application=application) + applicants_data = [ + { + "name": app.applicant.name, + "email": app.applicant.email, + "mobile": app.applicant.mobile, + "address": app.applicant.address, + "percentage_share": app.percentage_share + } + for app in associated_applicants + ] + + # Fetch Section I + section_i = ApplicationSectionI.objects.filter(application=application).first() + section_i_data = { + "type_of_ip": section_i.type_of_ip if section_i else None, + "area": section_i.area if section_i else None, + "problem": section_i.problem if section_i else None, + "objective": section_i.objective if section_i else None, + "novelty": section_i.novelty if section_i else None, + "advantages": section_i.advantages if section_i else None, + "is_tested": section_i.is_tested if section_i else None, + "poc_details": section_i.poc_details.url if section_i and section_i.poc_details else None, + "applications": section_i.applications if section_i else None, + } + + # Fetch Section II + section_ii = ApplicationSectionII.objects.filter(application=application).first() + section_ii_data = { + "funding_details": section_ii.funding_details if section_ii else None, + "funding_source": section_ii.funding_source if section_ii else None, + "source_agreement": section_ii.source_agreement.url if section_ii and section_ii.source_agreement else None, + "publication_details": section_ii.publication_details if section_ii else None, + "mou_details": section_ii.mou_details if section_ii else None, + "mou_file": section_ii.mou_file.url if section_ii and section_ii.mou_file else None, + "research_details": section_ii.research_details if section_ii else None + } + + # Fetch Section III + section_iii = ApplicationSectionIII.objects.filter(application=application).first() + section_iii_data = { + "company_name": section_iii.company_name if section_iii else None, + "contact_person": section_iii.contact_person if section_iii else None, + "contact_no": section_iii.contact_no if section_iii else None, + "development_stage": section_iii.development_stage if section_iii else None, + "form_iii": section_iii.form_iii.url if section_iii and section_iii.form_iii else None + } + + # Prepare response + response_data = { + "application_id": application.id, + "last_updated_at": application.last_updated_at, + "token_no": application.token_no, + "primary_applicant_name": primary_applicant_name, + "title": application.title, + "status": application.status, + "assigned_pcc_admin": handler_name, + "assigned_director": director_name, + "attorney_name": attorney_name, + "dates": { + "submitted_date": application.submitted_date, + "reviewed_by_pcc_date": application.reviewed_by_pcc_date, + "forwarded_to_director_date": application.forwarded_to_director_date, + "director_approval_date": application.director_approval_date, + "patentability_check_start_date": application.patentability_check_start_date, + "patentability_check_completed_date": application.patentability_check_completed_date, + "search_report_generated_date": application.search_report_generated_date, + "patent_filed_date": application.patent_filed_date, + "patent_published_date": application.patent_published_date, + "decision_date": application.decision_date, + }, + "decision_status": application.decision_status, + "comments": application.comments, + "applicants": applicants_data, + "section_I": section_i_data, + "section_II": section_ii_data, + "section_III": section_iii_data + } + + return JsonResponse(response_data, safe=False) + + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON in request body.'}, status=400) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def communication_logs(request, application_id): + application = get_object_or_404(Application, id=application_id) + + if request.method == 'GET': + logs = application.communication_logs.select_related('logged_by').all() + serializer = CommunicationLogSerializer(logs, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + payload = request.data.copy() + serializer = CommunicationLogSerializer(data=payload) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + serializer.save(application=application, logged_by=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + +# ----------------------------------------- +# 🔹 PCC Admin Attorney Management Views +# ----------------------------------------- + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_director_list(request): + try: + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can access director list.'}, status=status.HTTP_403_FORBIDDEN) + + directors = _get_director_users().order_by('first_name', 'username') + payload = [ + { + 'id': user.id, + 'username': user.username, + 'full_name': user.get_full_name() or user.username, + 'email': user.email, + } + for user in directors + ] + return Response(payload, status=status.HTTP_200_OK) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_attorney_list(request): + try: + # Get all attorneys with their application count + attorneys = Attorney.objects.annotate( + assigned_applications_count=Count('applications') + ).all() + + # Serialize the data + serializer = AttorneySerializer(attorneys, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def add_attorney(request): + try: + serializer = AttorneySerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['DELETE']) +@permission_classes([IsAuthenticated]) +def remove_attorney(request, attorney_id): + try: + attorney = Attorney.objects.get(id=attorney_id) + attorney.delete() + return Response({'message': 'Attorney removed successfully'}, status=status.HTTP_200_OK) + except Attorney.DoesNotExist: + return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_attorney_applications(request, attorney_id): + try: + # Get the attorney + attorney = Attorney.objects.get(id=attorney_id) + + # Get all applications assigned to this attorney + applications = Application.objects.filter(attorney=attorney).values('id', 'title', 'status') + + # Get the count of assigned applications + assigned_count = applications.count() + + response_data = { + 'attorney_id': attorney.id, + 'attorney_name': attorney.name, + 'assigned_applications_count': assigned_count, + 'applications': list(applications) + } + + return Response(response_data, status=status.HTTP_200_OK) + except Attorney.DoesNotExist: + return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['PUT']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def update_attorney_details(request, attorney_id): + try: + attorney = Attorney.objects.get(id=attorney_id) + serializer = AttorneySerializer(attorney, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Attorney.DoesNotExist: + return Response({'error': 'Attorney not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +# ----------------------------------------- +# 🔹 Document Management Views +# ----------------------------------------- + +@api_view(['GET', 'POST']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def manage_documents(request): + """ + GET: List all documents + POST: Create a new document + """ + if request.method == 'GET': + try: + documents = Document.objects.all().order_by('-created_at') + serializer = DocumentSerializer(documents, many=True) + return Response(serializer.data) + except Exception as e: + logger.error(f"Error fetching documents: {str(e)}") + return Response( + {'error': 'Failed to fetch documents'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + elif request.method == 'POST': + try: + serializer = DocumentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"Error creating document: {str(e)}") + return Response( + {'error': 'Failed to create document'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['DELETE']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def delete_document(request, document_id): + """ + Delete a document by ID + """ + try: + document = Document.objects.get(id=document_id) + document.delete() + return Response( + {'message': 'Document deleted successfully'}, + status=status.HTTP_200_OK + ) + except Document.DoesNotExist: + return Response( + {'error': 'Document not found'}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"Error deleting document: {str(e)}") + return Response( + {'error': 'Failed to delete document'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def withdraw_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only primary applicant can withdraw.'}, status=status.HTTP_403_FORBIDDEN) + if application.status in ["Patent Granted", "Patent Refused"]: + return Response({'error': 'Cannot withdraw after final decision.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Withdrawn" + application.save(update_fields=['status', 'last_updated_at']) + create_audit('Withdraw Application', request.user, application, 'Applicant withdrew application') + return Response({'message': 'Application withdrawn successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def resubmit_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id + is_pcc = is_pcc_admin_user(request.user) + if not (is_owner or is_pcc): + return Response({'error': 'Only primary applicant can resubmit.'}, status=status.HTTP_403_FORBIDDEN) + if application.status != "Needs Revision": + return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) + if application.revision_due_date and timezone.now().date() > application.revision_due_date: + application.status = "Revision Expired" + application.save(update_fields=['status', 'last_updated_at']) + return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Submitted" + application.revised_submitted_at = timezone.now() + application.is_revision_locked = True + application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) + create_audit('Resubmit Application', request.user, application, 'Applicant resubmitted revised application') + return Response({'message': 'Application resubmitted successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def declare_conflict(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can declare conflict.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = ConflictDeclarationSerializer(data={ + 'application': application.id, + 'reviewer': request.user.id, + 'conflict_type': request.data.get('conflict_type', 'General Conflict'), + 'notes': request.data.get('notes', ''), + 'declaration_status': 'Declared', + }) + if serializer.is_valid(): + serializer.save() + application.status = "Submitted" + application.assigned_pcc_admin = None + application.save(update_fields=['status', 'assigned_pcc_admin', 'last_updated_at']) + create_audit('Conflict Declared', request.user, application, serializer.validated_data.get('conflict_type', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_legal_assessment(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: + return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) + attorney_id = request.data.get('attorney') + attorney = get_object_or_404(Attorney, id=attorney_id) + serializer = LegalAssessmentSerializer(data={ + 'application': application.id, + 'attorney': attorney.id, + 'opinion': request.data.get('opinion', 'Review Needed'), + 'prior_art_summary': request.data.get('prior_art_summary', ''), + 'recommended_action': request.data.get('recommended_action', ''), + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_legal_advice_memo(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = LegalAdviceMemoSerializer(data={ + 'application': application.id, + 'author': request.user.id, + 'summary': request.data.get('summary', ''), + 'recommendation': request.data.get('recommendation', ''), + }) + if serializer.is_valid(): + serializer.save() + create_audit('Legal Memo Submitted', request.user, application, 'Memo added') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def initiate_budget_approval(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + amount = Decimal(str(request.data.get('amount', '0'))) + threshold = Decimal(str(request.data.get('threshold', '50000'))) + budget = record_budget_request(application, request.user, amount, threshold, request.data.get('comments', '')) + serializer = BudgetApprovalSerializer(budget) + create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') + notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_decide_budget(request, app_id): + if not is_director_user(request.user): + return Response({'error': 'Only Director can decide budget approval.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + budget = BudgetApproval.objects.filter(application=application).order_by('-created_at').first() + if not budget: + return Response({'error': 'No budget request found.'}, status=status.HTTP_404_NOT_FOUND) + decision = request.data.get('decision', '').strip().lower() + if decision not in ['approve', 'reject']: + return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) + budget.status = 'Approved' if decision == 'approve' else 'Rejected' + budget.decided_by = request.user + budget.decided_at = timezone.now() + budget.comments = request.data.get('comments', budget.comments) + budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) + application.budget_status = budget.status + application.save(update_fields=['budget_status', 'last_updated_at']) + create_audit('Budget Decision', request.user, application, budget.status) + return Response({'message': f'Budget {budget.status.lower()} successfully.'}) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def initiate_external_filing(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = ExternalFilingRecordSerializer(data={ + 'application': application.id, + 'patent_office': request.data.get('patent_office', ''), + 'filing_reference': request.data.get('filing_reference', ''), + 'communication_notes': request.data.get('communication_notes', ''), + 'filed_by': request.user.id, + 'filing_date': request.data.get('filing_date'), + }) + if serializer.is_valid(): + serializer.save() + application.external_filing_status = 'Filed' + if application.status == 'Patentability Search Report Generated': + application.status = 'Patent Filed' + application.patent_filed_date = timezone.now().date() + application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) + create_audit('External Filing Initiated', request.user, application, 'External filing recorded') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def add_office_action(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = OfficeActionSerializer(data={ + 'application': application.id, + 'office_name': request.data.get('office_name', ''), + 'action_reference': request.data.get('action_reference', ''), + 'action_summary': request.data.get('action_summary', ''), + 'due_date': request.data.get('due_date'), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def respond_office_action(request, office_action_id): + office_action = get_object_or_404(OfficeAction, id=office_action_id) + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can respond office action.'}, status=status.HTTP_403_FORBIDDEN) + serializer = OfficeActionResponseSerializer(data={ + 'office_action': office_action.id, + 'responder': request.user.id, + 'response_text': request.data.get('response_text', ''), + 'response_reference': request.data.get('response_reference', ''), + }) + if serializer.is_valid(): + serializer.save() + office_action.status = 'Responded' + office_action.save(update_fields=['status']) + create_audit('Office Action Responded', request.user, office_action.application, office_action.action_reference) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def add_prior_art_reference(request, app_id): + if not is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = PriorArtReferenceSerializer(data={ + 'application': application.id, + 'reference_type': request.data.get('reference_type', ''), + 'citation': request.data.get('citation', ''), + 'notes': request.data.get('notes', ''), + }) + if serializer.is_valid(): + serializer.save() + create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_appeal(request, app_id): + application = get_object_or_404(Application, id=app_id) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) + serializer = AppealRequestSerializer(data={ + 'application': application.id, + 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), + 'grounds': request.data.get('grounds', ''), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') + create_audit('Appeal Submitted', request.user, application, 'Appeal opened') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def submit_licensing_request(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + serializer = LicensingRequestSerializer(data={ + 'application': application.id, + 'requester_name': request.data.get('requester_name', ''), + 'requester_org': request.data.get('requester_org', ''), + 'request_details': request.data.get('request_details', ''), + 'status': 'Pending', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def collect_inventor_consents(request, app_id): + application = get_object_or_404(Application, id=app_id) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) + applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] + created = [] + for applicant in applicants: + if not applicant: + continue + consent, _ = InventorConsent.objects.get_or_create( + application=application, + applicant=applicant, + defaults={ + 'consent_given': False, + 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), + }, + ) + created.append(consent) + serializer = InventorConsentSerializer(created, many=True) + _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') + return Response(serializer.data) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def setup_maintenance_schedule(request, app_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) + application = get_object_or_404(Application, id=app_id) + due_date = request.data.get('due_date') + amount = request.data.get('amount') + serializer = MaintenanceScheduleSerializer(data={ + 'application': application.id, + 'due_date': due_date, + 'amount': amount, + 'status': 'Upcoming', + }) + if serializer.is_valid(): + serializer.save() + application.maintenance_tracking_active = True + application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) + _create_audit('Maintenance Schedule Created', request.user, application, f'Due: {due_date}') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def mark_maintenance_paid(request, schedule_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can mark maintenance paid.'}, status=status.HTTP_403_FORBIDDEN) + schedule = get_object_or_404(MaintenanceSchedule, id=schedule_id) + schedule.status = 'Paid' + schedule.paid_at = timezone.now() + schedule.save(update_fields=['status', 'paid_at']) + _create_audit('Maintenance Paid', request.user, schedule.application, f'Schedule {schedule.id}') + return Response({'message': 'Maintenance marked as paid.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def reviewer_queue(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) + apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') + payload = [] + for app in apps: + score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 + app.priority_score = 10 if app.status == 'Needs Revision' else 5 + app.save(update_fields=['priority_score', 'last_updated_at']) + payload.append({ + 'id': app.id, + 'title': app.title, + 'status': app.status, + 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, + 'priority_score': app.priority_score, + 'reviewer_workload': score, + }) + payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) + return Response(payload) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_notifications(request): + role = (request.GET.get("role") or "").strip() + notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) + if role: + notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) + notifications = notifications.order_by('-created_at') + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def mark_notification_read(request, notification_id): + notification = get_object_or_404(NotificationEvent, id=notification_id) + if notification.recipient_id and notification.recipient_id != request.user.id: + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + notification.is_read = True + notification.save(update_fields=['is_read']) + return Response({'message': 'Notification marked as read.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_audit_logs(request): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.select_related('actor', 'application').all()[:300] + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def get_applicant_insights(request): + applicant = Applicant.objects.filter(user=request.user).first() + if not applicant: + return Response({'error': 'Applicant profile not found.'}, status=status.HTTP_404_NOT_FOUND) + apps = Application.objects.filter(primary_applicant=applicant) + summary = { + 'total': apps.count(), + 'draft': apps.filter(status='Draft').count(), + 'submitted': apps.filter(status='Submitted').count(), + 'under_review': apps.filter(status__in=['Reviewed by PCC Admin', "Forwarded for Director's Review"]).count(), + 'approved': apps.filter(status="Director's Approval Received").count(), + 'granted': apps.filter(status='Patent Granted').count(), + 'refused': apps.filter(status='Patent Refused').count(), + } + return Response(summary) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def director_insights(request): + if not _is_director_user(request.user): + return Response({'error': 'Only Director can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) + return _insights_response(request) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def upload_document_version(request, document_id): + document = get_object_or_404(Document, id=document_id) + if document.is_locked: + return Response({'error': 'Document is locked for new versions.'}, status=status.HTTP_400_BAD_REQUEST) + link = request.data.get('link') + if not link: + return Response({'error': 'link is required.'}, status=status.HTTP_400_BAD_REQUEST) + next_version = (document.versions.first().version_number + 1) if document.versions.exists() else document.current_version + 1 + serializer = DocumentVersionSerializer(data={ + 'document': document.id, + 'version_number': next_version, + 'link': link, + 'uploaded_by': request.user.id, + }) + if serializer.is_valid(): + serializer.save() + document.current_version = next_version + document.link = link + document.save(update_fields=['current_version', 'link', 'updated_at']) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def lock_document(request, document_id): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can lock documents.'}, status=status.HTTP_403_FORBIDDEN) + document = get_object_or_404(Document, id=document_id) + document.is_locked = True + document.save(update_fields=['is_locked', 'updated_at']) + return Response({'message': 'Document locked successfully.'}) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def pcc_insights(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can access this endpoint.'}, status=status.HTTP_403_FORBIDDEN) + return _insights_response(request) + + +def _insights_response(request): + base_qs = Application.objects.all() + available_years = sorted( + { + dt.year + for dt in base_qs.exclude(submitted_date__isnull=True).values_list('submitted_date', flat=True) + if dt + } + ) + if not available_years: + available_years = [timezone.now().year] + + requested_year = request.GET.get('year') + if requested_year and requested_year.isdigit() and int(requested_year) in available_years: + selected_year = int(requested_year) + else: + selected_year = available_years[-1] + + apps = base_qs.filter(submitted_date__year=selected_year) + color_map = { + 'Submitted': '#4D96FF', + 'Reviewed by PCC Admin': '#00B894', + "Forwarded for Director's Review": '#F5A623', + "Director's Approval Received": '#2ECC71', + 'Patent Filed': '#8E44AD', + 'Patent Granted': '#16A085', + 'Patent Refused': '#E74C3C', + 'Needs Revision': '#D35400', + 'Withdrawn': '#7F8C8D', + } + status_order = [ + 'Submitted', + 'Reviewed by PCC Admin', + "Forwarded for Director's Review", + "Director's Approval Received", + 'Patent Filed', + 'Patent Granted', + 'Patent Refused', + 'Needs Revision', + 'Withdrawn', + ] + + status_counts = apps.values('status').annotate(count=Count('id')) + status_lookup = {item['status']: item['count'] for item in status_counts} + applications = [ + { + 'label': status_name, + 'count': status_lookup.get(status_name, 0), + 'color': color_map.get(status_name, '#5B7CFA'), + } + for status_name in status_order + ] + + total = sum(item['count'] for item in applications) + payload = { + 'applications': applications, + 'available_years': available_years, + 'selected_year': selected_year, + 'total': total, + } + + if request.GET.get('format') == 'csv': + rows = ['status,count,percentage'] + for item in applications: + percentage = (item['count'] / total * 100) if total else 0 + rows.append(f"{item['label']},{item['count']},{percentage:.2f}") + return HttpResponse('\n'.join(rows), content_type='text/csv') + + return Response(payload) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def pcc_resubmit_application(request, app_id): + application = get_object_or_404(Application, id=app_id) + is_owner = application.primary_applicant and application.primary_applicant.user_id == request.user.id + is_pcc = _is_pcc_admin_user(request.user) + if not (is_owner or is_pcc): + return Response({'error': 'Only primary applicant or PCC Admin can resubmit.'}, status=status.HTTP_403_FORBIDDEN) + if application.status != "Needs Revision": + return Response({'error': 'Application is not in revision stage.'}, status=status.HTTP_400_BAD_REQUEST) + if application.revision_due_date and timezone.now().date() > application.revision_due_date: + application.status = "Revision Expired" + application.save(update_fields=['status', 'last_updated_at']) + return Response({'error': 'Revision deadline expired.'}, status=status.HTTP_400_BAD_REQUEST) + application.status = "Submitted" + application.revised_submitted_at = timezone.now() + application.is_revision_locked = True + application.save(update_fields=['status', 'revised_submitted_at', 'is_revision_locked', 'last_updated_at']) + _create_audit('Resubmit Application', request.user, application, 'Application resubmitted') + return Response({'message': 'Application resubmitted successfully.'}) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def legal_assessment_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LegalAssessmentSerializer(application.legal_assessments.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can submit legal assessment.'}, status=status.HTTP_403_FORBIDDEN) + if application.status not in ["Director's Approval Received", "Patentability Check Started", "Patentability Check Completed"]: + return Response({'error': 'Legal assessment not allowed in current status.'}, status=status.HTTP_400_BAD_REQUEST) + attorney = get_object_or_404(Attorney, id=request.data.get('attorney')) + serializer = LegalAssessmentSerializer(data={ + 'application': application.id, + 'attorney': attorney.id, + 'opinion': request.data.get('opinion', 'Review Needed'), + 'prior_art_summary': request.data.get('prior_art_summary', ''), + 'recommended_action': request.data.get('recommended_action', ''), + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Assessment Submitted', request.user, application, 'Legal assessment saved') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def budget_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = BudgetApprovalSerializer(application.budget_approvals.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate budget approval.'}, status=status.HTTP_403_FORBIDDEN) + amount = Decimal(str(request.data.get('amount', '0'))) + threshold = Decimal(str(request.data.get('threshold', '50000'))) + serializer = BudgetApprovalSerializer(data={ + 'application': application.id, + 'requested_by': request.user.id, + 'amount': amount, + 'threshold': threshold, + 'status': 'Pending', + 'comments': request.data.get('comments', ''), + }) + if serializer.is_valid(): + serializer.save() + application.budget_status = 'Pending Approval' + application.budget_estimate = amount + application.save(update_fields=['budget_status', 'budget_estimate', 'last_updated_at']) + _notify(application, f'Budget approval requested for {amount}.', recipient_role='Director', event_type='Budget Approval') + _create_audit('Budget Approval Initiated', request.user, application, f'Amount: {amount}') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def budget_decision_by_id(request, budget_id): + budget = get_object_or_404(BudgetApproval, id=budget_id) + if not (_is_director_user(request.user) or _is_pcc_admin_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + decision = request.data.get('decision', '').strip().lower() + if decision not in ['approve', 'reject']: + return Response({'error': "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST) + budget.status = 'Approved' if decision == 'approve' else 'Rejected' + budget.decided_by = request.user + budget.decided_at = timezone.now() + budget.comments = request.data.get('comments', budget.comments) + budget.save(update_fields=['status', 'decided_by', 'decided_at', 'comments']) + budget.application.budget_status = budget.status + budget.application.save(update_fields=['budget_status', 'last_updated_at']) + _create_audit('Budget Decision', request.user, budget.application, budget.status) + return Response({'message': f'Budget {budget.status.lower()} successfully.'}) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def external_filing_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = ExternalFilingRecordSerializer(application.external_filings.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can initiate external filing.'}, status=status.HTTP_403_FORBIDDEN) + serializer = ExternalFilingRecordSerializer(data={ + 'application': application.id, + 'patent_office': request.data.get('patent_office', ''), + 'filing_reference': request.data.get('filing_reference', ''), + 'communication_notes': request.data.get('communication_notes', ''), + 'filed_by': request.user.id, + 'filing_date': request.data.get('filing_date'), + }) + if serializer.is_valid(): + serializer.save() + application.external_filing_status = 'Filed' + if application.status == 'Patentability Search Report Generated': + application.status = 'Patent Filed' + application.patent_filed_date = timezone.now().date() + application.save(update_fields=['external_filing_status', 'status', 'patent_filed_date', 'last_updated_at']) + _create_audit('External Filing Initiated', request.user, application, 'External filing recorded') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def maintenance_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = MaintenanceScheduleSerializer(application.maintenance_schedules.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can configure maintenance schedule.'}, status=status.HTTP_403_FORBIDDEN) + serializer = MaintenanceScheduleSerializer(data={ + 'application': application.id, + 'due_date': request.data.get('due_date'), + 'amount': request.data.get('amount'), + 'status': 'Upcoming', + }) + if serializer.is_valid(): + serializer.save() + application.maintenance_tracking_active = True + application.save(update_fields=['maintenance_tracking_active', 'last_updated_at']) + _create_audit('Maintenance Schedule Created', request.user, application, f"Due: {request.data.get('due_date')}") + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def queue_prioritized(request): + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can view reviewer queue.'}, status=status.HTTP_403_FORBIDDEN) + apps = Application.objects.filter(status__in=['Submitted', 'Needs Revision']).select_related('primary_applicant', 'assigned_pcc_admin') + payload = [] + for app in apps: + score = _reviewer_workload(app.assigned_pcc_admin) if app.assigned_pcc_admin else 0 + app.priority_score = 10 if app.status == 'Needs Revision' else 5 + app.save(update_fields=['priority_score', 'last_updated_at']) + payload.append({ + 'id': app.id, + 'title': app.title, + 'status': app.status, + 'assigned_to': app.assigned_pcc_admin.get_full_name() if app.assigned_pcc_admin else None, + 'priority_score': app.priority_score, + 'reviewer_workload': score, + }) + payload.sort(key=lambda x: (x['priority_score'], -x['reviewer_workload']), reverse=True) + return Response(payload) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def notifications_root(request): + role = (request.GET.get("role") or "").strip() + notifications = NotificationEvent.objects.filter(Q(recipient=request.user) | Q(recipient__isnull=True)) + if role: + notifications = notifications.filter(Q(recipient=request.user) | Q(recipient_role__iexact=role)) + notifications = notifications.order_by('-created_at') + serializer = NotificationEventSerializer(notifications, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def audit_logs_root(request): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.select_related('actor', 'application').all()[:300] + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def audit_logs_by_application(request, application_id): + if not (_is_pcc_admin_user(request.user) or _is_director_user(request.user)): + return Response({'error': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + logs = AuditLog.objects.filter(application_id=application_id).select_related('actor', 'application').all() + serializer = AuditLogSerializer(logs, many=True) + return Response(serializer.data) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def office_actions_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = OfficeActionSerializer(application.office_actions.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add office action.'}, status=status.HTTP_403_FORBIDDEN) + serializer = OfficeActionSerializer(data={ + 'application': application.id, + 'office_name': request.data.get('office_name', ''), + 'action_reference': request.data.get('action_reference', ''), + 'action_summary': request.data.get('action_summary', ''), + 'due_date': request.data.get('due_date'), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Office Action Added', request.user, application, serializer.validated_data.get('action_reference', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def prior_art_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + q = request.GET.get('q', '').strip() + refs = application.prior_art_references.all() + if q: + refs = refs.filter(Q(citation__icontains=q) | Q(notes__icontains=q)) + serializer = PriorArtReferenceSerializer(refs, many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can add prior-art reference.'}, status=status.HTTP_403_FORBIDDEN) + serializer = PriorArtReferenceSerializer(data={ + 'application': application.id, + 'reference_type': request.data.get('reference_type', ''), + 'citation': request.data.get('citation', ''), + 'notes': request.data.get('notes', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Prior Art Added', request.user, application, serializer.validated_data.get('citation', '')) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def appeals_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = AppealRequestSerializer(application.appeals.all(), many=True) + return Response(serializer.data) + if application.primary_applicant and application.primary_applicant.user_id != request.user.id: + return Response({'error': 'Only applicant can submit appeal.'}, status=status.HTTP_403_FORBIDDEN) + serializer = AppealRequestSerializer(data={ + 'application': application.id, + 'appellant': request.data.get('appellant', request.user.get_full_name() or request.user.username), + 'grounds': request.data.get('grounds', ''), + 'status': 'Open', + }) + if serializer.is_valid(): + serializer.save() + _notify(application, 'Appeal submitted by applicant.', recipient_role='Director', event_type='Appeal') + _create_audit('Appeal Submitted', request.user, application, 'Appeal opened') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def licensing_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LicensingRequestSerializer(application.licensing_requests.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create licensing requests.'}, status=status.HTTP_403_FORBIDDEN) + serializer = LicensingRequestSerializer(data={ + 'application': application.id, + 'requester_name': request.data.get('requester_name', ''), + 'requester_org': request.data.get('requester_org', ''), + 'request_details': request.data.get('request_details', ''), + 'status': 'Pending', + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Licensing Request Submitted', request.user, application, 'Licensing workflow started') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def inventor_consents_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = InventorConsentSerializer(application.inventor_consents.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can collect consents.'}, status=status.HTTP_403_FORBIDDEN) + applicants = [application.primary_applicant] + [rel.applicant for rel in AssociatedWith.objects.filter(application=application).select_related('applicant')] + created = [] + for applicant in applicants: + if not applicant: + continue + consent, _ = InventorConsent.objects.get_or_create( + application=application, + applicant=applicant, + defaults={ + 'consent_given': False, + 'agreement_reference': request.data.get('agreement_reference', f'CONSENT-{application.id}-{applicant.id}'), + }, + ) + created.append(consent) + serializer = InventorConsentSerializer(created, many=True) + _create_audit('Inventor Consents Collected', request.user, application, f'{len(created)} consent records ensured') + return Response(serializer.data) + + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def legal_memos_api(request, app_id): + application = get_object_or_404(Application, id=app_id) + if request.method == 'GET': + serializer = LegalAdviceMemoSerializer(application.legal_memos.all(), many=True) + return Response(serializer.data) + if not _is_pcc_admin_user(request.user): + return Response({'error': 'Only PCC Admin can create legal memo.'}, status=status.HTTP_403_FORBIDDEN) + serializer = LegalAdviceMemoSerializer(data={ + 'application': application.id, + 'author': request.user.id, + 'summary': request.data.get('summary', ''), + 'recommendation': request.data.get('recommendation', ''), + }) + if serializer.is_valid(): + serializer.save() + _create_audit('Legal Memo Submitted', request.user, application, 'Memo added') + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +def document_versions_api(request, document_id): + document = get_object_or_404(Document, id=document_id) + serializer = DocumentVersionSerializer(document.versions.all(), many=True) return Response(serializer.data) \ No newline at end of file diff --git a/FusionIIIT/applications/patent_system/migrations/0006_application_assigned_director.py b/FusionIIIT/applications/patent_system/migrations/0006_application_assigned_director.py new file mode 100644 index 000000000..0df38601c --- /dev/null +++ b/FusionIIIT/applications/patent_system/migrations/0006_application_assigned_director.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.5 on 2026-04-21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('patent_system', '0005_application_attorney_review_fields_and_statuses'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='assigned_director', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='patent_director_applications', + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/FusionIIIT/applications/patent_system/models.py b/FusionIIIT/applications/patent_system/models.py index ec5f7f695..121585ac3 100644 --- a/FusionIIIT/applications/patent_system/models.py +++ b/FusionIIIT/applications/patent_system/models.py @@ -68,6 +68,7 @@ class Application(models.Model): status = models.CharField(max_length=50, choices=STATUS_CHOICES, default = "Draft") attorney = models.ForeignKey(Attorney, on_delete=models.CASCADE, related_name="applications", blank=True, null=True) assigned_pcc_admin = models.ForeignKey(User, on_delete=models.SET_NULL, related_name="patent_assigned_applications", blank=True, null=True) + assigned_director = models.ForeignKey(User, on_delete=models.SET_NULL, related_name="patent_director_applications", blank=True, null=True) submitted_date = models.DateField(blank=True, null=True) reviewed_by_pcc_date = models.DateField(blank=True, null=True) forwarded_to_director_date = models.DateField(blank=True, null=True) diff --git a/FusionIIIT/applications/patent_system/selectors.py b/FusionIIIT/applications/patent_system/selectors.py new file mode 100644 index 000000000..feb0ad985 --- /dev/null +++ b/FusionIIIT/applications/patent_system/selectors.py @@ -0,0 +1,116 @@ +from django.db.models import Count, Q + +from .models import ( + Application, + AuditLog, + BudgetApproval, + CommunicationLog, + Document, + DocumentVersion, + ExternalFilingRecord, + InventorConsent, + LicensingRequest, + MaintenanceSchedule, + NotificationEvent, + OfficeAction, + PriorArtReference, + AppealRequest, + LegalAdviceMemo, + LegalAssessment, + Attorney, +) + + +def applicant_applications(applicant): + return ( + Application.objects.filter(Q(primary_applicant=applicant) | Q(associatedwith__applicant=applicant)) + .select_related("attorney") + .distinct() + .order_by("-last_updated_at") + ) + + +def applications_by_status(statuses): + return Application.objects.filter(status__in=statuses).select_related("primary_applicant") + + +def applications_by_decision_status(statuses): + return Application.objects.filter(decision_status__in=statuses).select_related("primary_applicant") + + +def get_communication_logs(application): + return application.communication_logs.select_related("logged_by").all() + + +def get_budget_approvals(application): + return application.budget_approvals.all() + + +def get_office_actions(application): + return application.office_actions.all() + + +def get_prior_art_references(application): + return application.prior_art_references.all() + + +def get_legal_assessments(application): + return application.legal_assessments.all() + + +def get_legal_advice_memos(application): + return application.legal_memos.all() + + +def get_licensing_requests(application): + return application.licensing_requests.all() + + +def get_inventor_consents(application): + return application.inventor_consents.all() + + +def get_maintenance_schedules(application): + return application.maintenance_schedules.all() + + +def get_appeal_requests(application): + return application.appeals.all() + + +def get_external_filing_records(application): + return application.external_filings.all() + + +def get_conflict_declarations(application): + return application.conflict_declarations.all() + + +def get_documents(application): + return application.documents.all() + + +def get_document_versions(document): + return document.versions.all() + + +def get_attorney_applications(attorney): + return Application.objects.filter(attorney=attorney).values("id", "title", "status") + + +def get_pcc_admin_queue(): + return Application.objects.filter(status__in=["Submitted", "Needs Revision"]).select_related("primary_applicant", "assigned_pcc_admin") + + +def get_director_queue(): + return Application.objects.filter( + status__in=["Forwarded for Director's Review", "Returned to Director"] + ).select_related("primary_applicant", "assigned_pcc_admin", "attorney") + + +def count_by_status(statuses): + return Application.objects.filter(status__in=statuses).values("status").annotate(count=Count("id")) + + +def count_by_decision_status(statuses): + return Application.objects.filter(decision_status__in=statuses).values("decision_status").annotate(count=Count("id")) diff --git a/FusionIIIT/applications/patent_system/services.py b/FusionIIIT/applications/patent_system/services.py new file mode 100644 index 000000000..d60b22f9c --- /dev/null +++ b/FusionIIIT/applications/patent_system/services.py @@ -0,0 +1,136 @@ +import logging +from decimal import Decimal +from datetime import timedelta + +from django.db.models import Count, Q +from django.utils import timezone +from django.utils.timezone import now +from django.contrib.auth.models import User + +from applications.globals.models import HoldsDesignation + +from .models import ( + Applicant, + Application, + AuditLog, + Attorney, + BudgetApproval, + NotificationEvent, +) + +logger = logging.getLogger(__name__) + + +def role_names_for_user(user): + names = set( + HoldsDesignation.objects.filter(user=user).values_list("designation__name", flat=True) + ) + return {name.lower() for name in names if name} + + +def is_pcc_admin_user(user): + role_names = role_names_for_user(user) + return any("pcc" in role and "admin" in role for role in role_names) + + +def is_director_user(user): + role_names = role_names_for_user(user) + return any("director" in role for role in role_names) + + +def get_director_users(): + return User.objects.filter( + holds_designations__designation__name__icontains="director", + is_active=True, + ).distinct() + + +def is_authorized_applicant_user(user): + role_names = role_names_for_user(user) + allowed_roles = { + "student", + "alumini", + "professor", + "associate professor", + "assistant professor", + "research engineer", + "faculty", + } + return bool(role_names & allowed_roles) or Applicant.objects.filter(user=user).exists() + + +def get_attorney_for_user(user): + if not user or not user.email: + return None + attorney = Attorney.objects.filter(email__iexact=user.email).first() + if attorney: + return attorney + full_name = user.get_full_name().strip() + if full_name: + attorney = Attorney.objects.filter(name__iexact=full_name).first() + return attorney + + +def is_attorney_user(user): + role_names = role_names_for_user(user) + if any("attorney" in role for role in role_names): + return True + return get_attorney_for_user(user) is not None + + +def require_comments(payload, key="comments"): + comments = (payload.get(key) or "").strip() + if not comments: + raise ValueError("Comments are required.") + if len(comments) > 1000: + raise ValueError("Comments too long. Max 1000 characters allowed.") + return comments + + +def create_audit(action, actor, application=None, details=""): + AuditLog.objects.create(action=action, actor=actor, application=application, details=details) + + +def notify(application, message, recipient=None, recipient_role=None, event_type="General", due_date=None): + NotificationEvent.objects.create( + application=application, + recipient=recipient, + recipient_role=recipient_role, + event_type=event_type, + message=message, + due_date=due_date, + ) + + +def reviewer_workload(user): + return Application.objects.filter( + assigned_pcc_admin=user, + status__in=["Submitted", "Reviewed by PCC Admin", "Needs Revision"], + ).count() + + +def move_application_to_revision(application, comments, actor): + application.status = "Needs Revision" + application.revision_requested_at = now() + application.revision_due_date = (now() + timedelta(days=60)).date() + application.is_revision_locked = False + application.comments = comments + application.assigned_pcc_admin = actor + application.save() + return application + + +def record_budget_request(application, requested_by, amount, threshold, comments=""): + serializer_data = { + "application": application.id, + "requested_by": requested_by.id, + "amount": Decimal(str(amount)), + "threshold": Decimal(str(threshold)), + "status": "Pending", + "comments": comments, + } + budget = BudgetApproval.objects.create(**serializer_data) + application.budget_status = "Pending Approval" + application.budget_estimate = serializer_data["amount"] + application.save(update_fields=["budget_status", "budget_estimate", "last_updated_at"]) + return budget diff --git a/FusionIIIT/create_admin.py b/FusionIIIT/create_admin.py new file mode 100644 index 000000000..ee04ccfda --- /dev/null +++ b/FusionIIIT/create_admin.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Fusion.settings') +django.setup() + +from django.contrib.auth.models import User + +# Check if superuser already exists +if User.objects.filter(username='admin').exists(): + print("⚠️ Superuser 'admin' already exists") +else: + User.objects.create_superuser('admin', 'admin@example.com', 'admin123') + print("✅ Superuser created successfully!") + +print("\n" + "="*50) +print("Django Admin Credentials:") +print("="*50) +print("Username: admin") +print("Password: admin123") +print("URL: http://localhost:8000/admin/") +print("="*50) diff --git a/FusionIIIT/create_admin_extrainfo.py b/FusionIIIT/create_admin_extrainfo.py new file mode 100644 index 000000000..aab63a5b4 --- /dev/null +++ b/FusionIIIT/create_admin_extrainfo.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import os +import sys +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Fusion.settings.development') +sys.path.insert(0, r'C:\Users\saumi\OneDrive\Desktop\FUSION-FINAL\fusion\Fusion\FusionIIIT') + +django.setup() + +from django.contrib.auth.models import User +from applications.globals.models import ExtraInfo + +# Get the admin user +admin_user = User.objects.get(username='admin') + +# Create ExtraInfo for admin user if it doesn't exist +extra_info, created = ExtraInfo.objects.get_or_create( + user=admin_user, + defaults={ + 'id': 'admin', # Required: primary key + 'title': 'Dr.', + 'sex': 'M', + 'user_type': 'staff', # Required field + 'phone_no': 9999999999, + 'user_status': 'PRESENT' + } +) + +if created: + print("✅ ExtraInfo created for admin user") +else: + print("✅ ExtraInfo already exists for admin user") + +print(f"Admin user: {admin_user.username}") +print(f"Email: {admin_user.email}") +print(f"Is Staff: {admin_user.is_staff}") +print(f"Is Superuser: {admin_user.is_superuser}") diff --git a/FusionIIIT/create_superuser.py b/FusionIIIT/create_superuser.py new file mode 100644 index 000000000..8c024cbab --- /dev/null +++ b/FusionIIIT/create_superuser.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +import os +import sys +import django + +# Set Django settings module +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Fusion.settings.development') + +# Add project to path +sys.path.insert(0, r'C:\Users\saumi\OneDrive\Desktop\FUSION-FINAL\fusion\Fusion\FusionIIIT') + +# Setup Django +django.setup() + +from django.contrib.auth.models import User + +# Delete existing admin user if it exists +User.objects.filter(username='admin').delete() + +# Create superuser +user = User.objects.create_superuser('admin', 'admin@test.com', 'admin123') +print("✅ Superuser created successfully!") +print("Username: admin") +print("Password: admin123") diff --git a/FusionIIIT/setup_admin_extrainfo.py b/FusionIIIT/setup_admin_extrainfo.py new file mode 100644 index 000000000..8ba88cade --- /dev/null +++ b/FusionIIIT/setup_admin_extrainfo.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +import os +import sys +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Fusion.settings.development') +sys.path.insert(0, r'C:\Users\saumi\OneDrive\Desktop\FUSION-FINAL\fusion\Fusion\FusionIIIT') + +django.setup() + +from django.contrib.auth.models import User +from applications.globals.models import ExtraInfo + +# Get the admin user +admin_user = User.objects.get(username='admin') + +# Create ExtraInfo for admin user if it doesn't exist +extra_info, created = ExtraInfo.objects.get_or_create( + user=admin_user, + defaults={ + 'roll_no': 'admin', + 'phone_number': '0000000000', + 'sex': 'U' # Unknown + } +) + +if created: + print("✅ ExtraInfo created for admin user") +else: + print("✅ ExtraInfo already exists for admin user") + +print(f"Admin user: {admin_user.username}") +print(f"Email: {admin_user.email}") +print(f"Is Staff: {admin_user.is_staff}") +print(f"Is Superuser: {admin_user.is_superuser}")