/* * Copyright 2012 Shared Learning Collaborative, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.slc.sli.dashboard.manager.impl; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.slc.sli.dashboard.entity.Config; import org.slc.sli.dashboard.entity.GenericEntity; import org.slc.sli.dashboard.entity.util.GenericEntityEnhancer; import org.slc.sli.dashboard.manager.EntityManager; import org.slc.sli.dashboard.manager.StudentProgressManager; import org.slc.sli.dashboard.util.Constants; /** * Gathers and provides information needed for the student progress view * @author srupasinghe * */ public class StudentProgressManagerImpl implements StudentProgressManager { private static Logger log = LoggerFactory.getLogger(StudentProgressManagerImpl.class); public static final String TRANSCRIPT_HISTORY = "transcriptHistory"; public static final String GRADE = "grade"; public static final String SUBJECT = "subject"; public static final String COURSE = "course"; private static final DecimalFormat GRADE_FORMATTER = new DecimalFormat("0.0"); @Autowired private EntityManager entityManager; @Override @SuppressWarnings("unchecked") public GenericEntity getTranscript(String token, Object studentIdObj, Config.Data config) { SortedMap<GenericEntity, List<GenericEntity>> transcripts = new TreeMap<GenericEntity, List<GenericEntity>>(new SessionComparator()); String studentId = studentIdObj.toString(); List<String> optionalFields = new LinkedList<String>(); optionalFields.add(Constants.ATTR_TRANSCRIPT); GenericEntity studentWithTranscript = entityManager.getStudentWithOptionalFields(token, studentId, optionalFields); if (studentWithTranscript == null) { return new GenericEntity(); } Map<String, Object> studentTranscript = (Map<String, Object>) studentWithTranscript.get(Constants.ATTR_TRANSCRIPT); if (studentTranscript == null) { return new GenericEntity(); } List<Map<String, Object>> courseTranscripts = (List<Map<String, Object>>) studentTranscript.get(Constants.ATTR_COURSE_TRANSCRIPTS); List<Map<String, Object>> studentSectionAssociations = (List<Map<String, Object>>) studentTranscript.get(Constants.ATTR_STUDENT_SECTION_ASSOC); if (studentSectionAssociations == null || courseTranscripts == null) { return new GenericEntity(); } for (Map<String, Object> studentSectionAssociation : studentSectionAssociations) { Map<String, Object> courseTranscript = getCourseTranscriptForSection(studentSectionAssociation, courseTranscripts); // skip this course if we can't find previous info if (courseTranscript == null) { continue; } Map<String, Object> section = getGenericEntity(studentSectionAssociation, Constants.ATTR_SECTIONS); Map<String, Object> course = getGenericEntity(section, Constants.ATTR_COURSES); Map<String, Object> session = getGenericEntity(section, Constants.ATTR_SESSIONS); GenericEntity term = new GenericEntity(); term.put(Constants.ATTR_TERM, getValue(session, Constants.ATTR_TERM)); term.put(Constants.ATTR_GRADE_LEVEL, getValue(courseTranscript, Constants.ATTR_GRADE_LEVEL_WHEN_TAKEN)); term.put(Constants.ATTR_SCHOOL, getSchoolName(section, token)); term.put(Constants.ATTR_SCHOOL_YEAR, getValue(session, Constants.ATTR_SCHOOL_YEAR)); term.put(Constants.ATTR_CUMULATIVE_GPA, getGPA(session, studentId, token)); term.put(Constants.ATTR_SESSION_BEGIN_DATE, getValue(session, Constants.ATTR_SESSION_BEGIN_DATE)); GenericEntityEnhancer.convertGradeLevel(term, Constants.ATTR_GRADE_LEVEL); // This isn't a new term if (transcripts.containsKey(term)) { List<GenericEntity> courses = transcripts.get(term); GenericEntity courseData = getCourseData(courseTranscript, course); courses.add(courseData); } else { // this is the first time the term has been encountered List<GenericEntity> courses = new ArrayList<GenericEntity>(); GenericEntity courseData = getCourseData(courseTranscript, course); courses.add(courseData); transcripts.put(term, courses); } } List<GenericEntity> transcriptData = new ArrayList<GenericEntity>(); for (Map.Entry<GenericEntity, List<GenericEntity>> entry : transcripts.entrySet()) { GenericEntity term = new GenericEntity(); term.putAll(entry.getKey()); term.put(Constants.ATTR_COURSES, entry.getValue()); transcriptData.add(term); } GenericEntity ret = new GenericEntity(); ret.put(TRANSCRIPT_HISTORY, transcriptData); return ret; } private String getGPA(Map<String, Object> session, String studentId, String token) { String sessionId = getValue(session, Constants.ATTR_ID); Map<String, String> params = new HashMap<String, String>(); params.put(Constants.ATTR_SESSION_ID, sessionId); params.put(Constants.ATTR_STUDENT_ID, studentId); GenericEntity academicRecord = entityManager.getAcademicRecord(token, studentId, params); String gpa = ""; if (academicRecord != null) { gpa = GRADE_FORMATTER.format(academicRecord.get(Constants.ATTR_CUMULATIVE_GPA)); } return gpa; } private GenericEntity getCourseData(Map<String, Object> courseTranscript, Map<String, Object> course) { GenericEntity courseData = new GenericEntity(); courseData.put(GRADE, getGrade(courseTranscript)); courseData.put(SUBJECT, getValue(course, Constants.ATTR_SUBJECTAREA)); courseData.put(COURSE, getValue(course, Constants.ATTR_COURSE_TITLE)); return courseData; } private String getGrade(Map<String, Object> courseTranscript) { String grade = ""; // Try to get numeric grade first grade = getValue(courseTranscript, Constants.ATTR_FINAL_NUMERIC_GRADE); if (grade.equals("")) { grade = getValue(courseTranscript, Constants.ATTR_FINAL_LETTER_GRADE); } return grade; } private String getSchoolName(Map<String, Object> section, String token) { String schoolId = getValue(section, Constants.ATTR_SCHOOL_ID); GenericEntity school = entityManager.getEntity(token, Constants.ATTR_SCHOOLS, schoolId, new HashMap<String, String>()); String schoolName = ""; if (school != null) { schoolName = school.getString(Constants.ATTR_NAME_OF_INST); } return schoolName; } /** * Returns a list of historical data for a given subject area * @param token Security token * @param studentIds List of student ids * @param selectedCourse The course to get information for * @return */ @Override @SuppressWarnings("unchecked") public Map<String, List<GenericEntity>> getStudentHistoricalAssessments(final String token, List<String> studentIds, String selectedCourse, String selectedSection) { Map<String, List<GenericEntity>> results = new HashMap<String, List<GenericEntity>>(); //get the subject area String subjectArea = getSubjectArea(token, selectedCourse); Map<String, String> params = new HashMap<String, String>(); List<GenericEntity> students = entityManager.getCourses(token, selectedSection, params); if (students == null || students.isEmpty()) { return results; } for (GenericEntity student : students) { String studentId = student.getString(Constants.ATTR_ID); List<GenericEntity> transcriptData = new ArrayList<GenericEntity>(); Map<String, Object> transcript = (Map<String, Object>) student.get(Constants.ATTR_TRANSCRIPT); List<Map<String, Object>> courseTranscripts = (List<Map<String, Object>>) transcript.get(Constants.ATTR_COURSE_TRANSCRIPTS); List<Map<String, Object>> studentSectionAssociations = (List<Map<String, Object>>) transcript.get(Constants.ATTR_STUDENT_SECTION_ASSOC); // skip if we have no associations or we have no previous transcripts if (studentSectionAssociations == null || courseTranscripts == null) { continue; } studentSectionAssociations = filterBySubjectArea(studentSectionAssociations, subjectArea); for (Map<String, Object> studentSectionAssoc : studentSectionAssociations) { // Get the course transcript for a particular section Map<String, Object> courseTranscript = getCourseTranscriptForSection(studentSectionAssoc, courseTranscripts); // skip this course if we can't find previous info if (courseTranscript == null) { continue; } Map<String, Object> section = getGenericEntity(studentSectionAssoc, Constants.ATTR_SECTIONS); Map<String, Object> course = getGenericEntity(section, Constants.ATTR_COURSES); Map<String, Object> session = getGenericEntity(section, Constants.ATTR_SESSIONS); GenericEntity data = new GenericEntity(); data.put(Constants.ATTR_FINAL_LETTER_GRADE, courseTranscript.get(Constants.ATTR_FINAL_LETTER_GRADE).toString()); data.put(Constants.ATTR_COURSE_TITLE, getValue(course, Constants.ATTR_COURSE_TITLE)); data.put(Constants.ATTR_SCHOOL_YEAR, getValue(session, Constants.ATTR_SCHOOL_YEAR)); data.put(Constants.ATTR_TERM, getValue(session, Constants.ATTR_TERM)); transcriptData.add(data); } results.put(studentId, transcriptData); } return results; } @SuppressWarnings("unchecked") private Map<String, Object> getGenericEntity(Map<String, Object> map, String field) { if (map == null) { return null; } return (Map<String, Object>) map.get(field); } private String getValue(Map<String, Object> map, String field) { String ret = ""; if (map.containsKey(field)) { ret = map.get(field).toString(); } return ret; } /** * Get the subject area for the selected course * @param token Security token * @param selectedCourse The id for the selected course * @return */ protected String getSubjectArea(final String token, String selectedCourse) { String subjectArea = null; Map<String, String> params = new HashMap<String, String>(); GenericEntity entity = entityManager.getEntity(token, Constants.ATTR_COURSES, selectedCourse, params); if (entity != null) { subjectArea = entity.getString(Constants.ATTR_SUBJECTAREA); } return subjectArea; } /** * Return the course transcript for a given section * @param studentSectionAssoc The student section association we are looking at * @param courseTranscripts a set of transcripts for a given student * @return The transcript that applies to a given section */ private Map<String, Object> getCourseTranscriptForSection(Map<String, Object> studentSectionAssoc, List<Map<String, Object>> courseTranscripts) { String courseId = ""; Map<String, Object> section = getGenericEntity(studentSectionAssoc, Constants.ATTR_SECTIONS); Map<String, Object> course = getGenericEntity(section, Constants.ATTR_COURSES); if (course != null) { courseId = course.get(Constants.ATTR_ID).toString(); } for (Map<String, Object> courseTranscript : courseTranscripts) { if (courseId.equals(courseTranscript.get(Constants.ATTR_COURSE_ID).toString())) { return courseTranscript; } } return null; } /** * Filters a list of student section associations down by a subject area * @param studentSectionAssociations The list of student section associations * @param subjectArea The filter to look at * @return Filtered list of student section associations */ private List<Map<String, Object>> filterBySubjectArea(List<Map<String, Object>> studentSectionAssociations, String subjectArea) { if (subjectArea == null) { log.warn("Subject Area to match is null!"); return studentSectionAssociations; } List<Map<String, Object>> filteredAssociations = new ArrayList<Map<String, Object>>(); for (Map<String, Object> studentSectionAssociation : studentSectionAssociations) { Map<String, Object> section = getGenericEntity(studentSectionAssociation, Constants.ATTR_SECTIONS); Map<String, Object> course = getGenericEntity(section, Constants.ATTR_COURSES); if (course != null) { Object ssaSubjectArea = course.get(Constants.ATTR_SUBJECTAREA); if (ssaSubjectArea != null && ssaSubjectArea.toString().equals(subjectArea)) { filteredAssociations.add(studentSectionAssociation); } } } return filteredAssociations; } /** * Set the transcript and session information for the given sections * @param token Security token * @param historicalData The historical data * @return */ @Override public SortedSet<String> getSchoolYears(final String token, Map<String, List<GenericEntity>> historicalData) { SortedSet<String> results = new TreeSet<String>(); for (List<GenericEntity> studentTranscripts : historicalData.values()) { for (GenericEntity transcript : studentTranscripts) { String schoolYear = transcript.getString(Constants.ATTR_SCHOOL_YEAR) + " " + transcript.get(Constants.ATTR_TERM); results.add(schoolYear); } } return results; } /** * Returns all the gradebook entries for all the students in the given section * @param token Security token * @param studentIds The students in the section * @param selectedSection The section to look for * @return */ @Override public Map<String, Map<String, GenericEntity>> getCurrentProgressForStudents(final String token, List<String> studentIds, String selectedSection) { Map<String, Map<String, GenericEntity>> results = new HashMap<String, Map<String, GenericEntity>>(); List<GenericEntity> students = entityManager.getStudentsWithGradebookEntries(token, selectedSection); for (GenericEntity student : students) { String studentId = student.getString(Constants.ATTR_ID); @SuppressWarnings("unchecked") List<Map<String, Object>> studentGradebookEntries = (List<Map<String, Object>>) student.get(Constants.ATTR_STUDENT_GRADEBOOK_ENTRIES); if (studentGradebookEntries == null) { continue; } Map<String, GenericEntity> gradebookEntries = new HashMap<String, GenericEntity>(); for (Map<String, Object> studentGradebookEntry : studentGradebookEntries) { // This doesn't cast well - have to manually create Generic Entity from Map<String, Object> GenericEntity geStudentGradebookEntry = new GenericEntity(); geStudentGradebookEntry.putAll(studentGradebookEntry); @SuppressWarnings("unchecked") Map<String, Object> entries = (Map<String, Object>) geStudentGradebookEntry.get(Constants.ATTR_GRADEBOOK_ENTRIES); if (entries == null) { continue; } geStudentGradebookEntry.put(Constants.ATTR_GRADEBOOK_ENTRY_TYPE, entries.get(Constants.ATTR_GRADEBOOK_ENTRY_TYPE)); gradebookEntries.put(studentGradebookEntry.get(Constants.ATTR_GRADEBOOK_ENTRY_ID).toString(), geStudentGradebookEntry); log.debug("Progress data [studentGradebookEntry] {}", studentGradebookEntry); } results.put(studentId, gradebookEntries); } return results; } /** * Parses a numeric grade to a double * @param numericGrade The numeric grade as a string * @return */ protected double parseNumericGrade(Object numericGrade) { if (numericGrade != null && numericGrade instanceof Double) { return ((Double) numericGrade).doubleValue(); } else { return 0; } } /** * Returns a sorted unique set of gradebook entries(tests) * @param gradebookEntryData The gradebook entry data for a section * @return */ @Override public SortedSet<GenericEntity> retrieveSortedGradebookEntryList(Map<String, Map<String, GenericEntity>> gradebookEntryData) { SortedSet<GenericEntity> list = new TreeSet<GenericEntity>(new DateFulFilledComparator()); //Sorting by entity to be able to handle the introduction of GradebookEntry/type in the future //Can be sorted by the keyset if GradebookEntry/type will not be used //go through and add the tests into one list for (Map<String, GenericEntity> map : gradebookEntryData.values()) { list.addAll(map.values()); } return list; } public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } /** * Compare two GenericEntities by the dateFulFilled * @author srupasinghe * */ public class DateFulFilledComparator implements Comparator<GenericEntity> { @Override public int compare(GenericEntity o1, GenericEntity o2) { DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); if (o1.getString(Constants.ATTR_DATE_FULFILLED) != null && o2.getString(Constants.ATTR_DATE_FULFILLED) != null) { try { Date date1 = formatter.parse(o1.getString(Constants.ATTR_DATE_FULFILLED)); Date date2 = formatter.parse(o2.getString(Constants.ATTR_DATE_FULFILLED)); return date1.compareTo(date2); } catch (ParseException e) { return 0; } } return 0; } } /** * Compare two GenericEntities by the Session * @author jshort * */ public class SessionComparator implements Comparator<GenericEntity> { @Override public int compare(GenericEntity ge0, GenericEntity ge1) { DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); if (ge0.getString(Constants.ATTR_SESSION_BEGIN_DATE) != null && ge1.getString(Constants.ATTR_SESSION_BEGIN_DATE) != null) { try { Date date1 = formatter.parse(ge0.getString(Constants.ATTR_SESSION_BEGIN_DATE)); Date date2 = formatter.parse(ge1.getString(Constants.ATTR_SESSION_BEGIN_DATE)); return date2.compareTo(date1); } catch (ParseException e) { return 0; } } return 0; } } }