/*
* ELW : e-learning workspace
* Copyright (C) 2010 Anton Kraievoy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package elw.web;
import com.google.common.base.Strings;
import elw.dao.Auth;
import elw.dao.Ctx;
import elw.dao.Queries;
import elw.dao.ctx.CtxSolution;
import elw.miniweb.Message;
import elw.miniweb.ViewJackson;
import elw.vo.*;
import elw.web.core.Core;
import elw.web.core.IndexRow;
import elw.web.core.LogFilter;
import elw.web.core.W;
import org.akraievoy.base.Parse;
import org.apache.commons.fileupload.FileUploadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import static elw.ElwPackage.*;
@Controller
@RequestMapping("/a/**/*")
public class AdminController extends ControllerElw {
private static final Logger log = LoggerFactory.getLogger(AdminController.class);
private final Queries queries;
public AdminController(
Queries queries,
Core core,
ElwServerConfig elwServerConfig
) {
super(core, elwServerConfig);
this.queries = queries;
}
protected HashMap<String, Object> auth(
final HttpServletRequest req,
final HttpServletResponse resp,
final boolean page,
final boolean verified
) throws IOException {
final HashMap<String, Object> model =
super.auth(req, resp, page, verified);
if (model == null) {
return null;
}
model.put(
R_CTX,
Ctx.fromString(req.getParameter(R_CTX)).resolve(queries)
);
return model;
}
@Override
protected String extraAuthValidations(Auth auth) {
if (!auth.isAdm()) {
return "Admin Auth Required";
}
return null;
}
@RequestMapping(value = "logout", method = RequestMethod.GET)
public ModelAndView do_logout(
final HttpServletRequest req,
final HttpServletResponse resp
) throws IOException {
req.getSession(true).invalidate();
resp.sendRedirect("index");
return null;
}
@RequestMapping(
value = "SessionMessage",
method = RequestMethod.GET
)
public ModelAndView do_SessionMessageGet(
final HttpServletRequest req,
final HttpServletResponse resp
) throws IOException {
return new ModelAndView(ViewJackson.data(Message.getMessages(req)));
}
@RequestMapping(
value = "SessionMessage/{stamp}",
method = RequestMethod.DELETE
)
public ModelAndView do_SessionMessageDelete(
final HttpServletRequest req,
final HttpServletResponse resp,
@PathVariable("stamp") final String stamp
) throws IOException {
Message.delete(req, stamp);
return null;
}
@RequestMapping(value = "index", method = RequestMethod.GET)
public ModelAndView do_index(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final HashMap<String, Object> model = auth(req, resp, true, false);
if (model == null) {
return null;
}
return new ModelAndView("a/index", model);
}
@RequestMapping(value = "rest/index", method = RequestMethod.GET)
public ModelAndView do_restIndex(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final HashMap<String, Object> model = auth(req, resp, false, false);
if (model == null) {
return null;
}
final List<Enrollment> enrolls = core.getQueries().enrollments();
final List<IndexRow> indexData = core.index(enrolls);
return new ModelAndView(ViewJackson.success(indexData));
}
@RequestMapping(value = "summary", method = RequestMethod.GET)
public ModelAndView do_summary(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmECG(req, resp, true, false, new WebMethodCtx() {
@Override
public ModelAndView handleCtx() throws IOException {
W.storeFilter(req, model);
W.filterDefault(model, "f_mode", "a");
final LogFilter filter = W.parseFilter(req);
final SortedMap<String, Map<String, List<Solution>>> fileMetas =
new TreeMap<String, Map<String, List<Solution>>>();
final Map<String, Double> ctxToScore = new HashMap<String, Double>();
core.tasksData(ctx, filter, true, fileMetas, ctxToScore, null);
model.put("fileMetas", fileMetas);
model.put("ctxToScore", ctxToScore);
model.put("totalBudget", ctx.getEnr().cmpTotalBudget());
return new ModelAndView("a/summary", model);
}
});
}
@RequestMapping(value = "log", method = RequestMethod.GET)
public ModelAndView do_log(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmECG(req, resp, true, false, new WebMethodCtx() {
public ModelAndView handleCtx() throws IOException {
W.storeFilter(req, model);
return new ModelAndView("a/log", model);
}
});
}
@RequestMapping(value = "rest/log", method = RequestMethod.GET)
public ModelAndView do_restLog(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmECG(req, resp, false, false, new WebMethodCtx() {
public ModelAndView handleCtx() throws IOException {
final Format format = (Format) model.get(FormatTool.MODEL_KEY);
final List<Object[]> logData = core.log(
ctx, format, W.parseFilter(req), true
);
return new ModelAndView(ViewJackson.success(logData));
}
});
}
@RequestMapping(value = "tasks", method = RequestMethod.GET)
public ModelAndView do_tasks(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmECG(req, resp, true, false, new WebMethodCtx() {
public ModelAndView handleCtx() throws IOException {
W.storeFilter(req, model);
return new ModelAndView("a/tasks", model);
}
});
}
@RequestMapping(value = "rest/tasks", method = RequestMethod.GET)
public ModelAndView do_restTasks(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmECG(req, resp, false, false, new WebMethodCtx() {
public ModelAndView handleCtx() throws IOException {
final Format format = (Format) model.get(FormatTool.MODEL_KEY);
final List<Object[]> logData = core.tasks(
ctx, W.parseFilter(req), format, true
);
return new ModelAndView(ViewJackson.success(logData));
}
});
}
@RequestMapping(value = "approve", method = RequestMethod.GET)
public ModelAndView do_approve(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmScore(req, resp, true, false, new WebMethodScore() {
public ModelAndView handleScore(
HttpServletRequest req,
HttpServletResponse resp,
Ctx ctx,
FileSlot slot,
Solution file,
Long stamp,
Map<String, Object> model
) {
final SortedMap<Long, Score> allScores =
core.getQueries().scores(ctx, slot, file);
final long stampEff =
stamp == null ? allScores.lastKey() : stamp;
final Score score = allScores.get(stampEff);
model.put("stamp", stamp);
model.put("score", score);
model.put("slot", slot);
model.put("file", file);
return new ModelAndView("a/approve", model);
}
});
}
@RequestMapping(value = "rest/scoreLog", method = RequestMethod.GET)
public ModelAndView do_restScoreLog(
final HttpServletRequest req,
final HttpServletResponse resp
) throws IOException {
return wmScore(req, resp, false, false, new WebMethodScore() {
public ModelAndView handleScore(
HttpServletRequest req,
HttpServletResponse resp,
Ctx ctx,
FileSlot slot,
Solution file,
Long stamp,
Map<String, Object> model
) {
final Format f = (Format) model.get(FormatTool.MODEL_KEY);
final SortedMap<Long, Score> allScores =
core.getQueries().scores(ctx, slot, file);
final String mode = req.getParameter("f_mode");
final List<Object[]> logData =
core.logScore(allScores, ctx, slot, file, f, mode, stamp);
return new ModelAndView(ViewJackson.success(logData));
}
});
}
@RequestMapping(value = "approve", method = RequestMethod.POST)
public ModelAndView do_approvePost(
final HttpServletRequest req,
final HttpServletResponse resp
) throws IOException {
return wmScore(req, resp, false, true, new WebMethodScore() {
public ModelAndView handleScore(
HttpServletRequest req,
HttpServletResponse resp,
Ctx ctx, FileSlot slot,
Solution file, Long stamp,
Map<String, Object> model
) throws IOException {
final CtxSolution ctxSolution =
ctx.ctxSlot(slot).solution(file);
final long stampEff =
stamp == null ? Long.MAX_VALUE : stamp;
final Score score = core.getQueries().score(
ctxSolution, stampEff
);
final String action =
Strings.nullToEmpty(req.getParameter("action"));
final List<String> validActions =
Arrays.asList("next", "approve", "approve_clean", "decline");
if (!validActions.contains(action.toLowerCase())) {
resp.sendError(
HttpServletResponse.SC_BAD_REQUEST,
"Bad action: " + action
);
return null;
}
if ("approve".equalsIgnoreCase(action)
|| "approve_clean".equalsIgnoreCase(action)
|| "decline".equalsIgnoreCase(action)) {
score.setApproved(
"approve".equalsIgnoreCase(action)
|| "approve_clean".equalsIgnoreCase(action)
);
final Map<String, Integer> pows =
new TreeMap<String, Integer>(score.getPows());
final Map<String, Double> ratios =
new TreeMap<String, Double>(score.getRatios());
for (Criteria cri : slot.getCriterias().values()) {
final int powDef = score.getPow(slot, cri);
final double ratioDef = score.getRatio(slot, cri);
final String idFor = Score.idFor(slot, cri);
final String powReq = req.getParameter(idFor);
final String ratioReq = req.getParameter(idFor + "--ratio");
pows.put(idFor, Parse.oneInt(powReq, powDef));
ratios.put(idFor, Parse.oneDouble(ratioReq, ratioDef));
}
score.setPows(pows);
score.setRatios(ratios);
score.setComment(req.getParameter("comment"));
queries.createScore(ctxSolution, score);
}
if ("approve_clean".equalsIgnoreCase(action)) {
final List<Solution> dupes = queries.solutions(ctxSolution);
for (Solution dupe : dupes) {
if (dupe.getStamp().equals(ctxSolution.solution.getStamp())) {
continue;
}
if (dupe.getId().equals(ctxSolution.solution.getId())) {
// separate fix for name-id collision:
// uploads with the same name are treated as same entity
// when navigating from solution to score
continue;
}
final CtxSolution ctxDupe = ctxSolution.solution(dupe);
final Score scoreDupe = core.getQueries().score(
ctxDupe, Long.MAX_VALUE
);
scoreDupe.setApproved(Boolean.FALSE);
score.setComment("Duplicate");
queries.createScore(ctxDupe, scoreDupe);
}
}
resp.sendRedirect(
core.cmpForwardToEarliestPendingSince(
ctx, slot, file.getStamp()
)
);
return null;
}
});
}
@RequestMapping(value = "groups", method = RequestMethod.GET)
public ModelAndView do_groups(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final HashMap<String, Object> model = auth(req, resp, true, false);
if (model == null) {
return null;
}
final List<Group> groups = core.getQueries().groups();
model.put("groups", groups);
return new ModelAndView("a/groups", model);
}
@RequestMapping(value = "ul", method = {RequestMethod.GET})
public ModelAndView do_ul(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, FileUploadException {
return wmFile(req, resp, null, true, false, new WebMethodFile() {
@Override
protected ModelAndView handleFile(String scope, FileSlot slot) throws IOException {
model.put("slot", slot);
final Ctx ctx = (Ctx) model.get("elw_ctx");
model.put(
"elw_ctx_type",
Ctx.forEnr(ctx.getEnr())
.extCourse(ctx.getCourse())
.extIndexEntry(ctx.getIndexEntry())
);
return new ModelAndView("a/ul", model);
}
});
}
@RequestMapping(value = "ul", method = RequestMethod.POST)
public ModelAndView do_ulPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, FileUploadException {
return wmFile(req, resp, null, false, true, new WebMethodFile() {
@Override
protected ModelAndView handleFile(String scope, FileSlot slot) throws IOException {
final String failureUri = core.getUri().upload(ctx, scope, slot.getId());
final String refreshUri = core.getUri().logCourseE(ctx.getEnr().getId());
final String authorName = auth(model).getAdmin().getName();
return storeFile(
slot, refreshUri, failureUri, authorName,
core.getQueries(), new Attachment()
);
}
});
}
@RequestMapping(value = "dl/*.*", method = RequestMethod.GET)
public ModelAndView do_dl(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
return wmFile(req, resp, null, false, true, new WebMethodFile() {
@Override
protected ModelAndView handleFile(String scope, FileSlot slot) throws IOException {
final String fileId = req.getParameter("fId");
if (fileId == null || fileId.trim().length() == 0) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no fileId (fId) defined");
return null;
}
final FileBase entry = core.getQueries().file(scope, ctx, slot, fileId);
if (entry == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "no file found");
return null;
}
retrieveFile(entry, slot, core.getQueries());
return null;
}
});
}
@RequestMapping(value = "SelftestStatus", method = RequestMethod.GET)
public ModelAndView do_selftestStatus(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final List<Message> messages = new ArrayList<Message>();
queries.invalidateCaches();
final List<Admin> admins = queries.admins();
for (Admin a : admins) {
if (a.getOpenIds().isEmpty()) {
messages.add(Message.warn("no openIds set for " + a));
}
}
for (List<Admin> dupes : filterDuplicateIds(admins)) {
messages.add(Message.err("Admin records with duplicate ids: " + mapToCouchIds(dupes)));
}
// FIXME there's a GRAND MESS below this innocent method:
// aliased data objects updates under a global lock when the objects already sit in the cache
final List<Course> courses = queries.courses();
for (Course c : courses) {
// FIXME this place is really unsafe, but I don't have access to raw data to complain properly
// actually the GRAND MESS was caused by someone trying to prevent me from having the access
}
for (List<Course> dupes : filterDuplicateIds(courses)) {
messages.add(Message.err("Course records with duplicate ids: " + mapToCouchIds(dupes)));
}
final Map<String, Course> coursesById = groupById(courses);
final List<Group> groups = queries.groups();
for (Group g : groups) {
for (Student s : g.getStudents().values()) {
if (s.getEmail() == null || s.getEmail().isEmpty()) {
messages.add(Message.warn(
String.format(
"No email set for student %s(%s) of group %s",
s.getName(),
s.getId(),
g.getId()
)
));
}
}
// LATER check that all students have unique emails and names
}
for (List<Group> dupes : filterDuplicateIds(groups)) {
messages.add(Message.err("Group records with duplicate ids: " + mapToCouchIds(dupes)));
}
final Map<String, Group> groupsById = groupById(groups);
final List<Enrollment> enrollments = queries.enrollments();
for (Enrollment e : enrollments) {
if (!coursesById.containsKey(e.getCourseId())) {
messages.add(Message.err(
String.format("Enrollment %s refers to missing course %s", e.getCouchId(), e.getCourseId())
));
}
if (!groupsById.containsKey(e.getGroupId())) {
messages.add(Message.err(
String.format("Enrollment %s refers to missing group %s", e.getCouchId(), e.getGroupId())
));
}
}
for (List<Enrollment> dupes : filterDuplicateIds(enrollments)) {
messages.add(Message.err("Enrollment records with duplicate ids: " + mapToCouchIds(dupes)));
}
return new ModelAndView(ViewJackson.data(messages));
}
}