Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 133 additions & 57 deletions education/education/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@
from frappe.utils import cstr, flt, getdate, today
from frappe.utils.dateutils import get_dates_from_timegrain

PORTAL_STUDENT_FIELDS = [
"name",
"student_name",
"student_email_id",
"student_mobile_number",
"image",
"gender",
"blood_group",
"nationality",
"date_of_birth",
"joining_date",
"address_line_1",
"address_line_2",
"city",
"state",
"pincode",
"country",
]

PROGRAM_ENROLLMENT_FIELDS = [
"name as program_enrollment",
"program",
"student_name",
"student_batch_name as student_batch",
"student_category",
"academic_term",
"academic_year",
]


def get_course(program):
"""Return list of courses for a particular program
Expand Down Expand Up @@ -557,41 +586,93 @@ def get_user_role():
return None


@frappe.whitelist()
def get_student_info():
email = frappe.session.user
if email == "Administrator":
return
def _resolve_portal_student(student=None):
if student:
return student

student = frappe.db.get_value("Student", {"user": email}, "name")
if not student:
user = frappe.session.user
if user in ("Guest", "Administrator"):
return None

return get_student_context(student)
return frappe.db.get_value("Student", {"user": user}, "name")


@frappe.whitelist()
def get_student_context(student):
"""Returns the full portal context for a single student.
def _get_submitted_program_enrollments(student):
return frappe.db.get_list(
"Program Enrollment",
filters={"student": student, "docstatus": 1},
fields=PROGRAM_ENROLLMENT_FIELDS,
order_by="creation desc",
)

Same shape as get_student_info (student record + current_program +
student_groups) but for an explicit student, authorized for the student
themselves or a linked guardian.

:param student: Student.
"""
def _resolve_default_program(student, programs):
if not programs:
return None

program_names = {row["program"] for row in programs}
active_enrollment = get_current_enrollment(student)
if active_enrollment and active_enrollment.get("program") in program_names:
return active_enrollment["program"]

return programs[0]["program"]


def _get_program_enrollment(student, program):
enrollments = frappe.db.get_list(
"Program Enrollment",
filters={"student": student, "program": program, "docstatus": 1},
fields=PROGRAM_ENROLLMENT_FIELDS,
order_by="creation desc",
limit=1,
)
return enrollments[0] if enrollments else None


@frappe.whitelist()
def get_student_profile(student=None):
"""Portal bootstrap: student profile and all submitted program enrollments."""
student = _resolve_portal_student(student)
if not student:
return None

check_permission(student, "profile")

student_info = frappe.db.get_value("Student", student, "*", as_dict=True)
student_info = frappe.db.get_value(
"Student", student, PORTAL_STUDENT_FIELDS, as_dict=True
)
if not student_info:
return None

current_program = get_current_enrollment(student_info.name)
if current_program:
student_groups = get_student_groups(student_info.name, current_program.program)
student_info["student_groups"] = student_groups
student_info["current_program"] = current_program
return student_info
programs = _get_submitted_program_enrollments(student)
return {
"student": student_info,
"programs": programs,
"suggested_program": _resolve_default_program(student, programs),
}


@frappe.whitelist()
def get_program_context(student=None, program=None):
"""Portal context for the selected program: enrollment + student groups."""
student = _resolve_portal_student(student)
if not student:
return None

if not program:
frappe.throw(_("Program is required"))

check_permission(student, "profile")

enrollment = _get_program_enrollment(student, program)
if not enrollment:
frappe.throw(_("Program enrollment not found"))

return {
"program": program,
"enrollment": enrollment,
"student_groups": get_student_groups(student, program),
}


@frappe.whitelist()
Expand Down Expand Up @@ -659,9 +740,6 @@ def get_portal_context():
if role == "Guardian":
context["guardian"] = get_guardian_info()
context["students"] = get_guardian_students()
elif role == "Student":
context["student"] = get_student_info()

return context


Expand Down Expand Up @@ -737,12 +815,12 @@ def apply_leave(leave_data, program_name):
"Education Settings", "attendance_based_on_course_schedule"
)
if attendance_based_on_course_schedule:
apply_leave_based_on_course_schedule(leave_data, program_name)
create_student_leave_application_based_on_course_schedule(leave_data, program_name)
else:
apply_leave_based_on_student_group(leave_data, program_name)
create_student_leave_application_based_on_student_group(leave_data, program_name)


def apply_leave_based_on_course_schedule(leave_data, program_name):
def create_student_leave_application_based_on_course_schedule(leave_data, program_name):
course_schedule_in_leave_period = frappe.db.get_list(
"Course Schedule",
fields=["name", "schedule_date"],
Expand All @@ -757,37 +835,35 @@ def apply_leave_based_on_course_schedule(leave_data, program_name):
)
if not course_schedule_in_leave_period:
frappe.throw(_("No classes found in the leave period"))
for course_schedule in course_schedule_in_leave_period:
# check if attendance record does not exist for the student on the course schedule
if not frappe.db.exists(
"Student Attendance",
{"course_schedule": course_schedule.get("name"), "docstatus": 1},
):
make_attendance_records(
leave_data.get("student"),
leave_data.get("student_name"),
"Leave",
course_schedule.get("name"),
None,
course_schedule.get("schedule_date"),
)

student_leave_application = frappe.new_doc("Student Leave Application")
student_leave_application.student = leave_data.get("student")
student_leave_application.student_name = leave_data.get("student_name")
student_leave_application.attendance_based_on = "Course Schedule"
student_leave_application.course_schedule = course_schedule_in_leave_period[0].get(
"name"
)
student_leave_application.from_date = leave_data.get("from_date")
student_leave_application.to_date = leave_data.get("to_date")
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()


def apply_leave_based_on_student_group(leave_data, program_name):
def create_student_leave_application_based_on_student_group(leave_data, program_name):
Comment on lines +848 to +852

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Leave application silently never applied — save() leaves the document in Draft (docstatus=0) and update_attendance() only runs from on_submit. Attendance records are never created, so the student's leave has no effect.

Suggested change
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()
def apply_leave_based_on_student_group(leave_data, program_name):
def create_student_leave_application_based_on_student_group(leave_data, program_name):
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()
student_leave_application.submit()
def create_student_leave_application_based_on_student_group(leave_data, program_name):

student_groups = get_student_groups(leave_data.get("student"), program_name)
leave_dates = get_dates_from_timegrain(
leave_data.get("from_date"), leave_data.get("to_date")
)
for student_group in student_groups:
for leave_date in leave_dates:
make_attendance_records(
leave_data.get("student"),
leave_data.get("student_name"),
"Leave",
None,
student_group.get("label"),
leave_date,
)

if not student_groups:
frappe.throw(_("No student groups found for the student"))

student_leave_application = frappe.new_doc("Student Leave Application")
student_leave_application.student = leave_data.get("student")
student_leave_application.student_name = leave_data.get("student_name")
student_leave_application.attendance_based_on = "Student Group"
student_leave_application.student_group = student_groups[0].get("label")
student_leave_application.from_date = leave_data.get("from_date")
student_leave_application.to_date = leave_data.get("to_date")
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()
Comment on lines +862 to +866

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Same .save() without .submit() issue as the course-schedule path — the leave application is saved as a Draft but on_submit → update_attendance() is never triggered, so no attendance records are created.

Suggested change
student_leave_application.student_group = student_groups[0].get("label")
student_leave_application.from_date = leave_data.get("from_date")
student_leave_application.to_date = leave_data.get("to_date")
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()
student_leave_application.student_group = student_groups[0].get("label")
student_leave_application.from_date = leave_data.get("from_date")
student_leave_application.to_date = leave_data.get("to_date")
student_leave_application.reason = leave_data.get("reason")
student_leave_application.save()
student_leave_application.submit()



@frappe.whitelist()
Expand Down
4 changes: 2 additions & 2 deletions education/education/doctype/student_group/student_group.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
}
],
"links": [],
"modified": "2022-12-05 10:50:08.798494",
"modified": "2026-07-01 15:04:37.649308",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Group",
Expand All @@ -157,7 +157,7 @@
"write": 1
}
],
"restrict_to_domain": "",
"row_format": "Dynamic",
"search_fields": "program, batch, course",
"sort_field": "modified",
"sort_order": "DESC",
Expand Down
4 changes: 1 addition & 3 deletions education/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@
# home_page = "login"

# website user home page (by Role)
# role_home_page = {
# "Role": "home_page"
# }
role_home_page = {"Student": "edu-portal", "Guardian": "edu-portal"}

# Generators
# ----------
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/components/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
<header
class="flex justify-between gap-3 items-center px-5 py-2.5 h-12 border-b"
>
<div class="flex gap-2 items-center cursor-pointer">
<h3 class="font-medium text-lg text-gray-900">
<div class="flex gap-3 items-center cursor-pointer min-w-0">
<h3 class="font-medium text-lg text-gray-900 shrink-0">
{{ currentRoute }}
</h3>
<ProgramSelector v-if="showProgramSelector" />
</div>
<div v-if="isStudent" class="flex flex-row gap-2">
<div v-if="isStudent" class="flex flex-row gap-2 shrink-0">
<Button
v-if="currentRoute === 'Attendance'"
variant="solid"
Expand All @@ -21,12 +22,22 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { leaveStore } from '@/stores/leave'
import { portalStore } from '@/stores/portal'
import { studentStore } from '@/stores/student'
import ProgramSelector from '@/components/ProgramSelector.vue'

const router = useRouter()
const currentRoute = computed(() => router.currentRoute.value.name)

const { setIsAttendancePage } = leaveStore()
const { isStudent } = portalStore()
const { isStudent, isGuardian, activeStudentId } = storeToRefs(portalStore())
const { programs } = storeToRefs(studentStore())

const showProgramSelector = computed(() => {
if (currentRoute.value === 'Students') return false
if (isGuardian.value && !activeStudentId.value) return false
return programs.value.length > 0
})
</script>
34 changes: 34 additions & 0 deletions frontend/src/components/ProgramSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<Dropdown v-if="programOptions.length > 1" :options="programOptions">
<template #default="{ open }">
<Button :label="activeProgram || 'Select Program'">
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4 text-gray-600"
/>
</template>
</Button>
</template>
</Dropdown>
<span v-else-if="activeProgram" class="text-m font-medium text-gray-700">
{{ activeProgram }}
</span>
</template>

<script setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { Dropdown, FeatherIcon, Button } from 'frappe-ui'
import { studentStore } from '@/stores/student'

const store = studentStore()
const { programs, activeProgram } = storeToRefs(store)

const programOptions = computed(() =>
programs.value.map((row) => ({
label: row.program,
onClick: () => store.setActiveProgram(row.program),
}))
)
</script>
Loading
Loading