/*
* 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.core;
import com.google.common.base.Strings;
import elw.dao.Ctx;
import elw.dao.QueriesImpl;
import elw.vo.*;
import elw.vo.Class;
import elw.web.ElwUri;
import elw.web.VelocityTemplates;
import elw.web.VtTuple;
import java.util.*;
public class Core {
private static final String CTX_TO_SCORE_TOTAL = "--total";
private final QueriesImpl queries;
private final VelocityTemplates vt = VelocityTemplates.INSTANCE;
private final ElwUri uri = new ElwUri();
public Core(QueriesImpl queries) {
this.queries = queries;
}
public VelocityTemplates getTemplates() {
return vt;
}
public ElwUri getUri() {
return uri;
}
public QueriesImpl getQueries() {
return queries;
}
public List<Object[]> log(
Ctx ctx, Format format, LogFilter logFilter, final boolean adm
) {
final List<Object[]> logData = new ArrayList<Object[]>();
if ("s".equalsIgnoreCase(logFilter.getScopePath()[0])) {
logStud(ctx, format, logFilter, logData, adm);
} else if ("c".equalsIgnoreCase(logFilter.getScopePath()[0])) {
logCourse(ctx, format, logFilter, logData, adm);
}
return logData;
}
private void logCourse(
Ctx ctx, Format format, LogFilter lf,
List<Object[]> logData, final boolean adm
) {
for (IndexEntry indexEntry : ctx.getEnr().getIndex().values()) {
final Ctx ctxAss = ctx.extendIndex(indexEntry.getId());
final TaskType aType = ctxAss.getAssType();
for (FileSlot slot : aType.getFileSlots().values()) {
if (W.excluded(lf.getSlotId(), aType.getId(), slot.getId())) {
continue;
}
if (!lf.cDue(ctxAss, slot)) {
continue;
}
if (!adm && !ctxAss.cFrom().isStarted()) {
continue;
}
final List<Version> versions;
if (adm) {
versions = new ArrayList<Version>(ctxAss.getAss().getVersions().values());
} else {
// FIXME also shared versions should be added here
versions = Collections.singletonList(ctxAss.getVer());
}
int total = 0;
for (Version ver : versions) {
final Ctx ctxVer = ctxAss.extendVer(ver);
final List<Attachment> uploadsVer = queries.attachments(ctxVer.ctxSlot(slot.getId()));
total += uploadsVer.size();
if (lf.cScopeOne('v') && lf.cVer(ctxVer)) {
logRows(format, lf, logData, indexEntry.getId(), ctxVer, slot, uploadsVer, Attachment.SCOPE, adm);
}
}
}
}
}
private void logStud(Ctx ctx, Format f, LogFilter lf, List<Object[]> logData, boolean adm) {
if (adm) {
if (lf.getVerId() != null && lf.getVerId().trim().length() > 0 &&
(lf.getStudId() == null || lf.getStudId().trim().length() == 0)) {
// do the same, but across all enrollments
for (Enrollment enr : queries.enrollments()) {
if (enr.getCourseId().equals(ctx.getCourse().getId())) {
final Ctx enrCtx = Ctx.forEnr(enr).resolve(queries);
for (Student stud : enrCtx.getGroup().getStudents().values()) {
if (W.excluded(lf.getStudId(), stud.getId())) {
continue;
}
final Ctx ctxStud = enrCtx.retainAll(Ctx.STATE_ECG).extendStudent(stud);
logStudForStud(ctxStud, f, lf, logData, ctxStud, adm);
}
}
}
} else {
for (Student stud : ctx.getGroup().getStudents().values()) {
if (W.excluded(lf.getStudId(), stud.getId())) {
continue;
}
final Ctx ctxStud = ctx.retainAll(Ctx.STATE_ECG).extendStudent(stud);
logStudForStud(ctx, f, lf, logData, ctxStud, adm);
}
}
} else {
if (ctx.getStudent() != null) {
logStudForStud(ctx, f, lf, logData, ctx, adm);
}
}
}
private void logStudForStud(
Ctx ctx, Format f,
LogFilter lf, List<Object[]> logData, Ctx ctxStud,
boolean adm) {
for (IndexEntry indexEntry : ctx.getEnr().getIndex().values()) {
final Ctx ctxVer = ctxStud.extendIndex(indexEntry.getId());
if (!adm && !ctxVer.cFrom().isStarted()) {
continue;
}
if (!lf.cVer(ctxVer)) {
continue;
}
final TaskType aType = ctxVer.getAssType();
for (FileSlot slot : aType.getFileSlots().values()) {
if (W.excluded(lf.getSlotId(), aType.getId(), slot.getId())) {
continue;
}
final List<Solution> uploads = queries.solutions(ctxVer.ctxSlot(slot));
if (!lf.cDue(ctxVer, slot)) {
continue;
}
logRows(f, lf, logData, indexEntry.getId(), ctxVer, slot, uploads, Solution.SCOPE, adm);
if (uploads.size() == 0
&& lf.cScopeStud(slot, null)
&& ctxVer.getIndexEntry().getScoreBudget() > 0) {
logData.add(logRow(f, lf.getMode(), logData, indexEntry.getId(), ctxVer, slot, null, Solution.SCOPE, adm));
}
}
}
}
private int logRows(
Format format, LogFilter logFilter, List<Object[]> logData, String index, Ctx ctxVer, FileSlot slot,
List<? extends FileBase> uploads, String scope, boolean adm
) {
int shown = 0;
for (int i = 0, uploadsLength = uploads.size(); i < uploadsLength; i++) {
final boolean last = i + 1 == uploadsLength;
if (!logFilter.cLatest(last)) {
continue;
}
final FileBase e = uploads.get(i);
if (Solution.SCOPE.equals(scope) && !logFilter.cScopeStud(slot, e)) {
continue;
}
shown += 1;
logData.add(logRow(format, logFilter.getMode(), logData, index, ctxVer, slot, e, scope, adm));
}
return shown;
}
private Object[] logRow(
Format f, final String mode, List<Object[]> data,
String index, Ctx ctx, FileSlot slot, FileBase fileBase, String scope,
boolean adm) {
final long time = fileBase == null ? System.currentTimeMillis() : fileBase.getStamp();
final String nameNorm;
if (fileBase == null) {
nameNorm = "";
} else {
if (adm && Solution.SCOPE.equalsIgnoreCase(scope)) {
nameNorm = ctx.cmpNameNorm(f, slot, fileBase);
} else {
nameNorm = fileBase.getName();
}
}
final SortedMap<String, List<Solution>> stud = queries.solutions(ctx);
final boolean studEditable =
Solution.SCOPE.equals(scope) && ctx.checkWrite(slot, stud);
final String ulRef;
if (adm) {
if (!Solution.SCOPE.equals(scope)) {
// FIXME context/scope here is not set properly
ulRef = uri.upload(ctx, scope, slot.getId());
} else {
ulRef = null;
}
} else {
if (studEditable) {
ulRef = uri.upload(ctx, scope, slot.getId());
} else {
ulRef = null;
}
}
String editRef = null;
if (slot.getFileTypes().size() == 1 && !Strings.isNullOrEmpty(IdNamed._.one(slot.getFileTypes()).getEditor())) {
if (adm || studEditable) {
editRef = uri.edit(ctx, scope, slot.getId(), fileBase == null ? null : fileBase.getId());
}
}
final String authorName;
if (fileBase == null) {
if (Solution.SCOPE.equalsIgnoreCase(scope)) {
authorName = ctx.getStudent().getName();
} else {
authorName = "Admin";
}
} else {
authorName = fileBase.getAuthor();
}
final VtTuple status = vt.status(f, mode, scope, ctx, slot, fileBase);
final String nameComment;
if (fileBase == null) {
nameComment = "";
} else {
if (fileBase.getComment() != null && fileBase.getComment().trim().length() > 0) {
nameComment = fileBase.getName() + " / " + fileBase.getComment();
} else {
nameComment = fileBase.getName();
}
}
final Object[] dataRow = {
/* 0 index */ data.size(),
/* 1 upload millis */ time,
/* 2 nice date - full */ fileBase == null ? "" : f.format(time, "EEE d HH:mm"),
/* 3 nice date - nice */ fileBase == null ? "" : f.format(time),
/* 4 author.id */ Solution.SCOPE.equalsIgnoreCase(scope) ? ctx.getStudent().getId() : "admin",
/* 5 author.name */ authorName,
/* 6 class.index */ index,
/* 7 class.name */ ctx.getAss().getName(),
/* 8 slot.id */ ctx.getAssType().getId() + "--" + slot.getId(),
/* 9 slot.name */ ctx.getVer() != null ? ctx.getVer().getName() + " / " + slot.getName() : slot.getName(),
/* 10 comment */ nameComment,
/* 11 status sort*/ status.getSort(),
/* 12 status text*/ status.getText(),
/* 13 status classes */ status.getClasses(),
/* 14 source ip */ fileBase == null ? "" : fileBase.getSourceAddress(),
/* 15 size bytes */ fileBase == null ? "" : fileBase.computeSize(),
/* 16 size */ fileBase == null ? "" : f.formatSize(fileBase.computeSize()),
/* 17 approve ref */ adm && Solution.SCOPE.equals(scope) ? uri.approve(ctx, scope, slot.getId(), fileBase) : null,
/* 18 dl ref */ uri.download(ctx, scope, slot.getId(), fileBase, nameNorm),
/* 19 ul ref */ ulRef,
/* 20 edit ref */ editRef,
/* 21 comment ref */ adm ? null : "#" // TODO comment edit url/page/method
};
return dataRow;
}
public List<IndexRow> index(List<Enrollment> enrolls) {
final List<IndexRow> indexData = new ArrayList<IndexRow>();
for (Enrollment enr : enrolls) {
indexData.add(indexRow(indexData, enr));
}
return indexData;
}
private IndexRow indexRow(List<IndexRow> indexData, Enrollment enr) {
final Group group = queries.group(enr.getGroupId());
final Course course = queries.course(enr.getCourseId());
final IndexRow indexRow = new IndexRow(
indexData.size(),
enr.getId(),
group.getId(),
group.getName(),
course.getId(),
course.getName(),
uri.summary(enr.getId()),
uri.tasks(enr.getId()),
uri.logAnyE(enr.getId()),
uri.logCourseE(enr.getId())
);
return indexRow;
}
public List<Object[]> logScore(
SortedMap<Long, Score> allScores, Ctx ctx, FileSlot slot, Solution file,
Format f, String mode, Long stamp
) {
final List<Object[]> logData = new ArrayList<Object[]>();
final Score scoreBest = chooseBestScore(allScores, ctx, slot);
for (Long s : allScores.keySet()) {
final Score scoreEntry = allScores.get(s);
final Long createStamp = scoreEntry.getStamp();
final boolean selected = stamp == null ? createStamp == null : stamp.equals(createStamp);
final long time;
String approveUri = "approve?elw_ctx=" + ctx.toString() + "&sId=" + slot.getId() + "&fId=" + file.getId();
if (createStamp == null) {
time = System.currentTimeMillis();
} else {
time = createStamp;
approveUri += "&stamp=" + createStamp.toString();
}
final VtTuple status = vt.status(f, mode, Solution.SCOPE, ctx, slot, file, scoreEntry);
final VtTuple statusScoring = vt.status(f, "s", Solution.SCOPE, ctx, slot, file, scoreEntry);
final Object[] logRow = new Object[]{
/* 0 index - */ logData.size(),
/* 1 selected 0 */ selected ? ">" : "",
/* 2 best 1 */ scoreBest == scoreEntry ? "*" : "",
/* 3 score date millis - */ time,
/* 4 score date full - */ f.format(time, "EEE d HH:mm"),
/* 5 score date nice 2 */ f.format(time),
/* 6 status classes - */ status.getClasses(),
/* 7 status text 3 */ status.getText(),
/* 8 scoring 4 */ statusScoring.getText(),
/* 9 comment 5 */ scoreEntry.getComment(),
/* 10 edit score 6 */ approveUri
};
logData.add(logRow);
}
return logData;
}
private Score chooseBestScore(SortedMap<Long, Score> allScores, Ctx ctx, FileSlot slot) {
Score scoreBest = null;
double pointsBest = 0;
for (Long s : allScores.keySet()) {
final Score scoreCur = allScores.get(s);
final double pointsCur = ctx.getIndexEntry().computePoints(scoreCur, slot);
if (scoreBest == null || pointsBest < pointsCur) {
scoreBest = scoreCur;
pointsBest = pointsCur;
}
}
return scoreBest;
}
public List<Object[]> tasks(Ctx ctx, final LogFilter filter, Format f, boolean adm) {
final List<Object[]> indexData = new ArrayList<Object[]>();
final SortedMap<String, Map<String, List<Solution>>> ctxVerToSlotToFiles =
new TreeMap<String, Map<String, List<Solution>>>();
final SortedMap<String, Double> ctxEsToScore = new TreeMap<String, Double>();
final SortedMap<String, Summary> ctxEsToSummary = new TreeMap<String, Summary>();
final int studCount = tasksData(ctx, filter, adm, ctxVerToSlotToFiles, ctxEsToScore, ctxEsToSummary);
int totalBudget = 0;
for (IndexEntry indexEntry : ctx.getEnr().getIndex().values()) {
final Ctx ctxAss = ctx.extendIndex(indexEntry.getId());
indexData.add(tasksRow(f, indexData, ctxAss, adm, ctxEsToScore, ctxEsToSummary, studCount));
totalBudget += ctxAss.getIndexEntry().getScoreBudget();
}
final Double totalScore = ctxEsToScore.get(CTX_TO_SCORE_TOTAL);
if (totalBudget > 0 && studCount > 0 && totalScore != null) {
final double avgScore = totalScore / studCount;
final String score = f.format2(avgScore) + " of " + totalBudget + ": " + vt.niceRatio(f, avgScore / totalBudget, "");
indexData.add(new Object[]{
/* 0 index - */ indexData.size(),
/* 1 date millis - */ 0,
/* 2 date full - */ "",
/* 3 date nice 0*/ "",
/* 4 tType.id - */ "",
/* 5 tType.name 1 */ "",
/* 6 task.id ref - */ "",
/* 7 task.name ref 2 */ "",
/* 8 summary status sort - */ 0,
/* 9 summary status text - */ "",
/* 10 summary status text 3 */ "",
/* 11 summary due millis - */ 0,
/* 12 summary due full - */ "",
/* 13 summary due nice 4 */ "<b>Total:</b>",
/* 14 score sort */ 0,
/* 15 score nice 5 */ score,
/* 16 uploads ref 6 */ null,
/* 17 uploads-open ref 7 */ null,
/* 18 uploads-course ref 8 */ null,
/* 19 task-total sort - */ 1
});
}
return indexData;
}
private Object[] tasksRow(
Format f, List<Object[]> indexData, Ctx ctxAss, boolean adm,
SortedMap<String, Double> ctxEsToScore, SortedMap<String, Summary> ctxEsToSummary, int studCount
) {
final Class classFrom = ctxAss.cFrom();
final VtTuple summary = vt.summary(ctxAss, ctxEsToSummary, studCount);
final Summary summ = ctxEsToSummary.get(ctxAss.ei());
final String dueNice;
final String dueFull;
final long dueSort;
if (classFrom.isStarted()) {
if (summ.getEarliestDue() == null) {
dueNice = "None";
dueFull = "";
dueSort = System.currentTimeMillis();
} else {
dueNice = "Due " + f.format(summ.getEarliestDue());
dueFull = f.format(summ.getEarliestDue(), "EEE d HH:mm");
dueSort = summ.getEarliestDue();
}
} else {
dueNice = "Opens " + f.format(classFrom.getFromDateTime().getMillis());
dueFull = f.format(classFrom.getFromDateTime().getMillis(), "EEE d HH:mm");
dueSort = classFrom.getFromDateTime().getMillis();
}
final String scoreNice;
final double scoreSort;
if (ctxEsToScore.get(ctxAss.ei()) != null) {
final int budget = ctxAss.getIndexEntry().getScoreBudget();
if (budget > 0) {
final double score = ctxEsToScore.get(ctxAss.ei()) / studCount;
scoreNice = f.format2(score) + " of " + budget + ": " + vt.niceRatio(f, score / budget, "");
scoreSort = score;
} else {
scoreSort = -1;
scoreNice = "";
}
} else {
scoreNice = "?";
scoreSort = -2;
}
final Object[] arr = {
/* 0 index - */ indexData.size(),
/* 1 date millis - */ classFrom.getFromDateTime().getMillis(),
/* 2 date full - */ f.format(classFrom.getFromDateTime().getMillis(), "EEE d HH:mm"),
/* 3 date nice 0*/ f.format(classFrom.getFromDateTime().getMillis()),
/* 4 tType.id - */ ctxAss.getAssType().getId(),
/* 5 tType.name 1 */ ctxAss.getAssType().getName(),
/* 6 task.id ref - */ ctxAss.getAss().getId(),
/* 7 task.name ref 2 */ ctxAss.getAss().getName(),
/* 8 summary status sort - */ summary.getSort(),
/* 9 summary status text - */ summary.getClasses(),
/* 10 summary status text 3 */ summary.getText(),
/* 11 summary due millis - */ dueSort,
/* 12 summary due full - */ dueFull,
/* 13 summary due nice 4 */ dueNice,
/* 14 score sort */ scoreSort,
/* 15 score nice 5 */ scoreNice,
/* 16 uploads ref 6 */ adm || classFrom.isStarted() ? uri.logOpenPendingEAV(ctxAss) : null,
/* 17 uploads-open ref 7 */ adm || classFrom.isStarted() ? uri.logApprovedDeclinedEAV(ctxAss) : null,
/* 18 uploads-course ref 8 */ adm || classFrom.isStarted() ? uri.logCourseEAV(ctxAss) : null,
/* 19 task-total sort - */ 0
};
return arr;
}
/**
* Generate per-task data score summaries
*
* @param ctxEnr context with a student set (non-adm) or only enrollment set (adm)
* @param filter to filter tasks and/or students
* @param adm whether this is an admin report or not
* @param fileMetas to store per slot file meta listings
* @param ctxToScore to store totals per task
* @param ctxToSummary to handle open/pending/approved stats
* @return number of students processed in this report
*/
public int tasksData(
Ctx ctxEnr, LogFilter filter, boolean adm,
Map<String, Map<String, List<Solution>>> fileMetas,
Map<String, Double> ctxToScore,
Map<String, Summary> ctxToSummary
) {
int students = 0;
if (adm) {
for (Student stud : ctxEnr.getGroup().getStudents().values()) {
if (W.excluded(filter.getStudId(), stud.getId())) {
continue;
}
students += 1;
storeTasksData(ctxEnr.extendStudent(stud), filter, fileMetas, ctxToScore, ctxToSummary);
}
} else {
// let's hope that student id is already present here...
students += 1;
storeTasksData(ctxEnr, filter, fileMetas, ctxToScore, ctxToSummary);
}
return students;
}
private void storeTasksData(
Ctx ctxStud, LogFilter filter,
Map<String, Map<String, List<Solution>>> fileMetas,
Map<String, Double> ctxToScore,
Map<String, Summary> ctxToSummary
) {
for (IndexEntry indexEntry : ctxStud.getEnr().getIndex().values()) {
storeTaskData(ctxStud.extendIndex(indexEntry.getId()), filter, fileMetas, ctxToScore, ctxToSummary);
}
}
private void storeTaskData(
Ctx ctxVer, LogFilter filter,
Map<String, Map<String, List<Solution>>> fileMetas,
Map<String, Double> ctxToScore,
Map<String, Summary> ctxToSummary
) {
final String assPath = ctxVer.toString();
final TaskType assType = ctxVer.getAssType();
final SortedMap<String, List<Solution>> slotIdToFiles = queries.solutions(ctxVer);
if (fileMetas != null) {
fileMetas.put(assPath, slotIdToFiles);
}
for (FileSlot slot : assType.getFileSlots().values()) {
if (W.excluded(filter.getSlotId(), assType.getId(), slot.getId())) {
continue;
}
final Class classDue = ctxVer.cDue(slot.getId());
final Long classDueStamp = classDue != null ? classDue.getToDateTime().getMillis() : null;
final List<Solution> filesForSlot = slotIdToFiles.get(slot.getId());
final Solution bestFile = selectBestFile(ctxVer, slot.getId(), filesForSlot, slot);
final double scoreForIdx;
final Summary sum;
if (bestFile != null) {
final Score score = bestFile.getScore();
if (score != null && Boolean.TRUE.equals(score.getApproved())) {
scoreForIdx = ctxVer.getIndexEntry().computePoints(score, slot);
} else {
scoreForIdx = 0;
}
sum = Summary.forScore(classDueStamp, score == null ? null : score.getApproved());
} else {
scoreForIdx = 0;
if (classDue == null) {
sum = new Summary(0, 0, 0, 0, null);
} else if (filesForSlot == null || filesForSlot.isEmpty()) {
sum = new Summary(0, 0, 1, 0, classDueStamp);
} else {
final Solution lastFile = filesForSlot.get(filesForSlot.size() - 1);
final Score score = lastFile.getScore();
sum = Summary.forScore(classDueStamp, score == null ? null : score.getApproved());
}
}
ctxToScore.put(ctxVer.toString(), scoreForIdx);
Summary.increment(ctxToScore, ctxVer.ei(), scoreForIdx);
Summary.increment(ctxToScore, ctxVer.es(), scoreForIdx);
Summary.increment(ctxToScore, CTX_TO_SCORE_TOTAL, scoreForIdx);
Summary.increment(ctxToSummary, ctxVer.ei(), sum);
}
}
private Solution selectBestFile(Ctx ctxVer, String slotId, List<Solution> filesForSlot, final FileSlot slot) {
if (filesForSlot == null || filesForSlot.isEmpty()) {
return null;
}
Solution usedEntry = null;
double maxScore = 0;
for (Solution e : filesForSlot) {
final Score score = e.getScore();
final double eScore = ctxVer.getIndexEntry().computePoints(score, slot);
if (Boolean.FALSE.equals(score.getApproved())) {
continue;
}
final boolean firstScore = usedEntry == null;
final boolean betterScore = maxScore < eScore;
final boolean sameButApproved = maxScore == eScore && Boolean.TRUE.equals(score.getApproved());
if ((firstScore || betterScore || sameButApproved)) {
maxScore = eScore;
usedEntry = e;
}
}
if (usedEntry != null) {
final Score s = usedEntry.getScore();
if (s != null) {
s.setBest(true);
}
}
return usedEntry;
}
public String cmpForwardToEarliestPendingSince(Ctx ctx, FileSlot slot, Long since) {
Solution epF = null; // earliest pending
Ctx epCtx = null;
final Ctx ctxEnr = Ctx.forEnr(ctx.getEnr()).resolve(queries);
// LATER oh this pretty obviously looks like we REALLY need some rdbms from now on... :D
for (Student stud : ctx.getGroup().getStudents().values()) {
final Ctx ctxStud = ctxEnr.extendStudent(stud);
for (IndexEntry indexEntry : ctx.getEnr().getIndex().values()) {
final Ctx ctxVer = ctxStud.extendIndex(indexEntry.getId());
if (!ctxVer.getAssType().getId().equals(ctx.getAssType().getId())) {
continue; // other ass types out of scope
}
for (FileSlot s : ctxVer.getAssType().getFileSlots().values()) {
if (!s.getId().equals(slot.getId())) {
continue; // other slots out of scope
}
final List<Solution> uploads = queries.solutions(ctxVer.ctxSlot(slot));
if (uploads != null && uploads.size() > 0) {
for (int i = uploads.size() - 1; i >= 0; i--) {
final Solution f = uploads.get(i);
if (since != null && f.getStamp().compareTo(since) <= 0) {
break; // oh this is overly stale
}
if (f.getScore() == null || f.getScore().getApproved() == null) {
if ((epF == null || epF.getStamp() > f.getStamp())) {
epF = f;
epCtx = ctxVer;
}
break; // don't look into earlier pending versions before this one is approved
} else if (f.getScore().state() == State.APPROVED) {
break; // don't look into earlier pending versions after this one is approved
}
}
}
}
}
}
final String forward;
if (epCtx != null) {
forward = "approve?"
+ "elw_ctx=" + epCtx.toString()
+ "&sId=" + slot.getId()
+ "&fId=" + ElwUri.urlEncode(epF.getId());
} else {
forward = "log?elw_ctx=" + ctxEnr.toString() + "&f_slot=" + slot.getId() + "&f_scope=s--p--";
}
return forward;
}
}