All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.iu.uits.lms.externaltoolsreport.controller.ToolController Maven / Gradle / Ivy

package edu.iu.uits.lms.externaltoolsreport.controller;

/*-
 * #%L
 * external-tools-report
 * %%
 * Copyright (C) 2022 - 2023 Indiana University
 * %%
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the Indiana University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import edu.iu.uits.lms.externaltoolsreport.config.ToolConfig;
import edu.iu.uits.lms.externaltoolsreport.model.ExternalToolsData;
import edu.iu.uits.lms.externaltoolsreport.model.TermData;
import edu.iu.uits.lms.externaltoolsreport.service.ExternalToolsReportService;
import edu.iu.uits.lms.externaltoolsreport.util.ExternalToolsReportUtils;
import edu.iu.uits.lms.lti.LTIConstants;
import edu.iu.uits.lms.lti.controller.OidcTokenAwareController;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.authentication.OidcAuthenticationToken;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@Controller
@RequestMapping("/app")
@Slf4j
public class ToolController extends OidcTokenAwareController {

   @Autowired
   private ToolConfig toolConfig = null;

   @Autowired
   private ResourceBundleMessageSource messageSource = null;
   
   @Autowired
   private ExternalToolsReportService externalToolsReportService;

   public enum RequiredHeadings {
      canvas_id,
      sis_course_id,
      campus,
      course_code,
      term,
      term_id,
      tool,
      placement,
      counts;
   }

   @RequestMapping({"/index", "/launch"})
   @Secured(LTIConstants.INSTRUCTOR_AUTHORITY)
   public ModelAndView index(Model model, HttpServletRequest request) {
      log.debug("in /index");
      OidcAuthenticationToken token = getTokenWithoutContext();
      
      List terms = externalToolsReportService.getDistinctTerms();
      model.addAttribute("terms", terms);

      return new ModelAndView("index");
   }
   

   @RequestMapping(value = "/delete", method = RequestMethod.POST)
   @Secured(LTIConstants.INSTRUCTOR_AUTHORITY)
   public ModelAndView delete(Model model, 
                              HttpServletRequest request,
                              @RequestParam("submittedTermToDelete") String submittedTermToDelete) {
      
      if (submittedTermToDelete == null || submittedTermToDelete.isBlank()) {
         model.addAttribute("error", messageSource.getMessage("externaltools.delete.error", new Object[] {}, Locale.getDefault()));
      } else {
         String termName = externalToolsReportService.getTermName(submittedTermToDelete);
         externalToolsReportService.deleteAllRecordsForTerm(submittedTermToDelete);
         model.addAttribute("successMsg", messageSource.getMessage("externaltools.delete.success", new Object[]{termName, submittedTermToDelete}, Locale.getDefault()));
      }
      
      return index(model, request);
   }

   @RequestMapping(value = "/upload", method = RequestMethod.POST)
   @Secured(LTIConstants.INSTRUCTOR_AUTHORITY)
   public ModelAndView uploadNewTerm(Model model, 
                                     HttpServletRequest request,
                                     @RequestParam(value = "newTermFile") MultipartFile newTermFile) {
      
      if (newTermFile.isEmpty()) {
         model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.noFile", new Object[] {}, Locale.getDefault()));
         return index(model, request);
      } 
      
      try {
         CSVReader csvReader = new CSVReader(new InputStreamReader(newTermFile.getInputStream()));
         List rawContents = csvReader.readAll();

         // make sure the file isn't empty and has at least 2 rows (one for header, one for term data)
         if (rawContents == null || rawContents.isEmpty() || rawContents.size() < 2) {
            model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.noContent", new Object[] {}, Locale.getDefault()));
            return index(model, request);
         } 

         List headers = new ArrayList(Arrays.asList(rawContents.get(0)));
         
         // check for required header columns
         String errorMsg = "";
         for (RequiredHeadings heading : RequiredHeadings.values()) {
            if (!headers.stream().anyMatch(s -> s.equalsIgnoreCase(heading.toString()))) {
               errorMsg += "CSV is missing required column: " + heading + System.lineSeparator();
            }
         }
         
         if (errorMsg.length() > 0) {
            model.addAttribute("error", errorMsg);
            return index(model, request);
         }
         
         List existingTerms = externalToolsReportService.getDistinctTerms();

         int numRows = rawContents.size(); 
         log.debug("Total rows: " + numRows);
         
         String fileTermId = null;
         String fileTermName = null;
         
         List newData = new ArrayList<>();
         int currRow = 1;
         while (currRow < numRows) {
            ExternalToolsData data = new ExternalToolsData();
            Map csvContent = ExternalToolsReportUtils.createCsvLineDataMap(rawContents, currRow);

            // Ensure the canvas id is numeric
            String canvasIdString = csvContent.get(RequiredHeadings.canvas_id.name());
            if (!NumberUtils.isCreatable(canvasIdString)) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.nonNumeric.canvasId", new Object[] {currRow, canvasIdString}, Locale.getDefault()));
               return index(model, request);
            }
            data.setCanvasId(Long.parseLong(canvasIdString));
            
            data.setSisCourseId(csvContent.get(RequiredHeadings.sis_course_id.name())); 
            data.setCampus(csvContent.get(RequiredHeadings.campus.name()));
            data.setCourseCode(csvContent.get(RequiredHeadings.course_code.name()));

            // Make sure term name is populated since we are using it for the label in the UI
            String termName = csvContent.get(RequiredHeadings.term.name());
            if (termName == null || termName.isBlank()) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.missingTermName", new Object[] {currRow, canvasIdString}, Locale.getDefault()));
               return index(model, request);
            }
            
            if (fileTermName == null) {
               fileTermName = termName;
            }
            
            // make sure the termName is consistent in the file
            if (!fileTermName.equals(termName)) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.differentTermNames", new Object[] {}, Locale.getDefault()));
               return index(model, request);
            }
            data.setTerm(termName);

            // Make sure the termId is populated
            String termId = csvContent.get(RequiredHeadings.term_id.name());
            if (termId == null || termId.isBlank()) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.missingTermId", new Object[] {currRow, canvasIdString}, Locale.getDefault()));
               return index(model, request);
            }
            
            if (fileTermId == null) {
               // We need to make sure there aren't multiple terms in the file.
               // Ensure this term isn't already in the database
               fileTermId = termId;
               if (existingTerms.stream().anyMatch(s -> s.getTermId().equals(termId))) {
                  model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.existingTerm", new Object[] {termName, termId}, Locale.getDefault()));
                  return index(model, request);
               }
            }
            
            if (!fileTermId.equals(termId)) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.differentTerms", new Object[] {}, Locale.getDefault()));
               return index(model, request);
            }
            
            data.setTermId(termId);
            
            data.setTool(csvContent.get(RequiredHeadings.tool.name()));
            data.setPlacement(csvContent.get(RequiredHeadings.placement.name()));

            // Counts must be populated
            String countsString = csvContent.get(RequiredHeadings.counts.name());
            if (countsString == null || countsString.trim().isBlank()) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.missingCounts", new Object[] {currRow, canvasIdString}, Locale.getDefault()));
               return index(model, request);
            }

            // Ensure the count is an integer
            try {
               data.setCounts(Integer.parseInt(countsString));
            } catch (NumberFormatException nfe) {
               model.addAttribute("error", messageSource.getMessage("externaltools.upload.error.nonNumeric.counts", new Object[] {currRow, countsString}, Locale.getDefault()));
               return index(model, request);
            }
            
            newData.add(data);
            
            currRow++;
         }
         
         log.debug("Finished processing csv. About to save...");
         
         externalToolsReportService.saveToolData(newData);
         
         log.debug("Data saved successfully");

         model.addAttribute("successMsg",  messageSource.getMessage("externaltools.upload.success", new Object[] {}, Locale.getDefault()));
      
      } catch (IOException | CsvException e) {
            log.error("Exception uploading csv file for External Tools Data tool", e);
            model.addAttribute("error", messageSource.getMessage("exernaltools.upload.error.unexpected", new Object[] {}, Locale.getDefault()));
      } 
      
      return index(model, request);
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy