/** * Copyright © 2002 Instituto Superior Técnico * * This file is part of FenixEdu Academic. * * FenixEdu Academic is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu Academic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu Academic. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.academic.ui.spring.controller.teacher.authorization; import java.io.IOException; import java.io.OutputStream; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.fenixedu.academic.domain.Department; import org.fenixedu.academic.domain.ExecutionSemester; import org.fenixedu.academic.domain.Teacher; import org.fenixedu.academic.domain.TeacherAuthorization; import org.fenixedu.academic.domain.TeacherCategory; import org.fenixedu.bennu.core.domain.Bennu; import org.fenixedu.bennu.core.domain.User; import org.fenixedu.commons.i18n.I18N; import org.fenixedu.commons.i18n.LocalizedString; import org.fenixedu.commons.spreadsheet.SheetData; import org.fenixedu.commons.spreadsheet.SpreadsheetBuilder; import org.fenixedu.commons.spreadsheet.WorkbookExportFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import pt.ist.fenixframework.Atomic; import pt.ist.fenixframework.Atomic.TxMode; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Strings; /*** * Teacher authorization and categories service * * This service provides methods to manage teacher authorizations and categories. * * @author Sérgio Silva (sergio.silva@tecnico.ulisboa.pt) * */ @Service public class AuthorizationService { @Autowired private CsvService csvService; @Autowired private MessageSource messageSource; /*** * Helper method to get message from message source easily * * @param code the key * @param args the arguments if necessary * @return */ private String message(String code, Object... args) { return messageSource.getMessage(code, args, I18N.getLocale()); } /*** * Get all active deparments * * @return {@link Department} */ public List<Department> getDepartments() { return Department.readActiveDepartments(); } /*** * Get all teacher categories * * @return */ public List<TeacherCategory> getCategories() { return Bennu.getInstance().getTeacherCategorySet().stream().distinct().sorted().collect(Collectors.toList()); } /*** * Get all execution periods order by semester and year in descending order (most recent to oldest) * * @return */ public List<ExecutionSemester> getExecutionPeriods() { return Bennu.getInstance().getExecutionPeriodsSet().stream().distinct() .sorted(ExecutionSemester.COMPARATOR_BY_SEMESTER_AND_YEAR.reversed()).collect(Collectors.toList()); } /*** * Get all execution periods that the teacher is authorized to * * @param teacher * @return */ public List<ExecutionSemester> getExecutionPeriods(Teacher teacher) { return teacher.getTeacherAuthorizationStream().map(TeacherAuthorization::getExecutionSemester).distinct() .sorted(ExecutionSemester.COMPARATOR_BY_SEMESTER_AND_YEAR.reversed()).collect(Collectors.toList()); } /*** * Get the current execution period * * @return */ public ExecutionSemester getCurrentPeriod() { return ExecutionSemester.readActualExecutionSemester(); } /*** * Create new teacher authorization using the bean * * @param bean * @return */ public TeacherAuthorization createTeacherAuthorization(FormBean bean) { return createTeacherAuthorization(bean.getUser(), bean.getDepartment(), bean.getPeriod(), bean.getCategory(), bean.getContracted(), bean.getLessonHours()); } /*** * Create new teacher authorization in one atomic operation. * Creates the teacher if the user doesn't have one * * @param user The user * @param department The associated deparment * @param semester The period * @param category The teacher category * @param contracted true if the teacher is contracted, false if otherwise * @param lessonHours the number of hours the teacher must teacher * @return the created {@link TeacherAuthorization} object * */ @Atomic(mode = TxMode.WRITE) public TeacherAuthorization createTeacherAuthorization(User user, Department department, ExecutionSemester semester, TeacherCategory category, Boolean contracted, Double lessonHours) { Teacher teacher; if (user.getPerson().getTeacher() == null) { teacher = new Teacher(user.getPerson()); } else { teacher = user.getPerson().getTeacher(); } return TeacherAuthorization.createOrUpdate(teacher, department, semester, category, contracted, lessonHours); } /*** * Creates a new teacher category. * * @param code * @param name * @param weight The degree of importance (used by the comparator) * @return */ @Atomic(mode = TxMode.WRITE) public TeacherCategory createTeacherCategory(String code, LocalizedString name, Integer weight) { return new TeacherCategory(code, name, weight); } private Stream<TeacherAuthorization> getAuthorizations(Department department) { if (department == null) { return Bennu.getInstance().getTeacherAuthorizationSet().stream(); } return department.getTeacherAuthorizationStream(); } /*** * Get all revoked teacher authorizations ordered descendingly by revoke time. * * @return */ public List<TeacherAuthorization> getRevokedAuthorizations() { Comparator<TeacherAuthorization> byRevokeTime = (a1, a2) -> { return a1.getRevokeTime().compareTo(a2.getRevokeTime()); }; return Bennu.getInstance().getRevokedTeacherAuthorizationSet().stream().distinct().sorted(byRevokeTime.reversed()) .collect(Collectors.toList()); } /** * Get all valid teacher authorizations for the specific {@link Department} and {@link ExecutionSemester} * * @param search {@link Department} and {@link ExecutionSemester} filter * @return */ public List<TeacherAuthorization> searchAuthorizations(final SearchBean search) { return getAuthorizations(search.getDepartment()).filter(t -> t.getExecutionSemester().equals(search.getPeriod())) .distinct().collect(Collectors.toList()); } /*** * Revokes the teacher authorization * * @param authorization */ @Atomic(mode = TxMode.WRITE) public void revoke(TeacherAuthorization authorization) { authorization.revoke(); } /*** * Creates a new {@link TeacherCategory} * * @param form */ @Atomic(mode = TxMode.WRITE) public void createCategory(CategoryBean form) { new TeacherCategory(form.getCode(), form.getName(), form.getWeight()); } /*** * Edits {@link TeacherCategory} instance * * @param category * @param form */ @Atomic(mode = TxMode.WRITE) public void editCategory(TeacherCategory category, CategoryBean form) { category.setCode(form.getCode()); category.setName(form.getName()); category.setWeight(form.getWeight()); } /*** * Write to outputstream csv file with {@link TeacherAuthorization} specified by the filter bean * * @param search filter bean * @param out the outputstream to dump the CSV to * @throws IOException if write to the outputstream fails */ public void dumpCSV(SearchBean search, OutputStream out) throws IOException { SpreadsheetBuilder builder = new SpreadsheetBuilder(); builder.addSheet(getSheetName(search), new SheetData<TeacherAuthorization>(searchAuthorizations(search)) { @Override protected void makeLine(TeacherAuthorization item) { final User user = item.getTeacher().getPerson().getUser(); addCell(message("teacher.authorizations.csv.column.1.username"), user.getUsername()); addCell(message("teacher.authorizations.csv.column.2.categoryCode"), item.getTeacherCategory().getCode()); addCell(message("teacher.authorizations.csv.column.3.departmentAcronym"), item.getDepartment().getAcronym()); addCell(message("teacher.authorizations.csv.column.4.lessonHours"), item.getLessonHours()); addCell(message("teacher.authorizations.csv.column.5.contracted"), item.isContracted() ? "Y" : "N"); addCell(message("teacher.authorizations.displayname"), user.getProfile().getDisplayName()); addCell(message("teacher.authorizations.category"), item.getTeacherCategory().getName().getContent()); addCell(message("teacher.authorizations.department"), item.getDepartment().getNameI18n().getContent()); addCell(message("teacher.authorizations.period"), item.getExecutionSemester().getQualifiedName()); addCell(message("teacher.authorizations.authorized"), String.format("%s (%s)", item.getAuthorizer().getProfile() .getDisplayName(), item.getAuthorizer().getUsername())); } }); builder.build(WorkbookExportFormat.CSV, out); } public String getCsvFilename(SearchBean search) { return getSheetName(search) + ".csv"; }; /** * get the CSV sheet name when exporting existing teacher authorizations * * @param search bean * @return */ public String getSheetName(SearchBean search) { String department = search.getDepartment() == null ? message("label.all") : search.getDepartment().getAcronym(); String period = search.getPeriod().getQualifiedName().replace(" ", "_"); return Joiner.on("_").join("teacherAuthorizations", department, period); } /*** * Importation of teacher authorizations from a CSV file * * @param period the period where to import * @param partFile the CSV file * @return */ public List<TeacherAuthorization> importCSV(ExecutionSemester period, MultipartFile partFile) { try { return importAuthorizations(period, csvService.readCsvFile(partFile.getInputStream(), ",", Charsets.UTF_8.toString())); } catch (IOException e) { throw new RuntimeException(message("teacher.authorizations.upload.parsing.failed")); } } /*** * Batch import of teacher authorizations * Ignores empty lines. * * @param period the {@link ExecutionSemester} where to import * @param authorizationEntries list where each element is a map of the column and the value * @throws RuntimeException if can't convert a specified value for a specific element * @return the list of {@link TeacherAuthorization} created objects */ @Atomic(mode = TxMode.WRITE) private List<TeacherAuthorization> importAuthorizations(ExecutionSemester period, List<Map<String, String>> authorizationEntries) { return authorizationEntries .stream() .map(auth -> { String username = auth.get(message("teacher.authorizations.csv.column.1.username")); String categoryCode = auth.get(message("teacher.authorizations.csv.column.2.categoryCode")); String departmentAcronym = auth.get(message("teacher.authorizations.csv.column.3.departmentAcronym")); String hoursValue = auth.get(message("teacher.authorizations.csv.column.4.lessonHours")); String contractedValue = auth.get(message("teacher.authorizations.csv.column.5.contracted")); if (Strings.isNullOrEmpty(username) && Strings.isNullOrEmpty(categoryCode) && Strings.isNullOrEmpty(departmentAcronym) && Strings.isNullOrEmpty(hoursValue) && Strings.isNullOrEmpty(contractedValue)) { // ignore empty line return Optional.<TeacherAuthorization> empty(); } User user = User.findByUsername(username); if (user == null) { throw new RuntimeException(message("teacher.authorizations.csv.column.1.username.error", username)); } Optional<TeacherCategory> category = TeacherCategory.findByCode(categoryCode); if (!category.isPresent()) { throw new RuntimeException( message("teacher.authorizations.csv.column.2.categoryCode.error", categoryCode)); } Department department = Department.find(departmentAcronym); if (department == null) { throw new RuntimeException(message("teacher.authorizations.csv.column.3.departmentAcronym.error", departmentAcronym)); } Double lessonHours; try { lessonHours = Double.parseDouble(hoursValue); } catch (NumberFormatException nfe) { throw new RuntimeException(message("teacher.authorizations.csv.column.4.lessonHours.error", hoursValue)); } if (!"Y".equals(contractedValue) && !"N".equals(contractedValue)) { throw new RuntimeException(message("teacher.authorizations.csv.column.5.contracted.error", contractedValue)); } Boolean contracted = new Boolean("Y".equals(contractedValue)); return Optional.<TeacherAuthorization> of(createTeacherAuthorization(user, department, period, category.get(), contracted, lessonHours)); }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()); } }