/**********************************************************************************
* $URL: $
* $Id: $
***********************************************************************************
*
* Author: Charles Hedrick, hedrick@rutgers.edu
*
* Copyright (c) 2010 Rutgers, the State University of New Jersey
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.lessonbuildertool.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Comparator;
import java.util.Date;
import java.util.Map;
import java.util.Iterator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.lessonbuildertool.service.LessonSubmission;
import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean;
import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean.UrlItem;
import org.sakaiproject.assignment.api.Assignment;
import org.sakaiproject.assignment.api.AssignmentEdit;
import org.sakaiproject.assignment.api.AssignmentSubmission;
import org.sakaiproject.assignment.api.AssignmentContent;
import org.sakaiproject.assignment.api.AssignmentContentEdit;
import org.sakaiproject.assignment.cover.AssignmentService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.InUseException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.site.api.Group;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.cover.SiteService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.site.api.ToolConfiguration;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.time.api.Time;
import org.sakaiproject.time.api.TimeBreakdown;
import org.sakaiproject.time.cover.TimeService;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.CacheRefresher;
import org.sakaiproject.memory.api.MemoryService;
import uk.org.ponder.messageutil.MessageLocator;
/**
* Interface to Assignment
*
* @author Charles Hedrick <hedrick@rutgers.edu>
*
*/
// NOTE: almost no other class should import this. We want to be able
// to support both forums and jforum. So typically there will be a
// forumEntity, but it's injected, and it can be either forum and jforum.
// Hence it has to be declared LessonEntity. That leads to a lot of
// declarations like LessonEntity forumEntity. In this case forumEntity
// means either a ForumEntity or a JForumEntity. We can't just call the
// variables lessonEntity because the same module will probably have an
// injected class to handle tests and quizes as well. That will eventually
// be converted to be a LessonEntity.
public class AssignmentEntity implements LessonEntity, AssignmentInterface {
private static Log log = LogFactory.getLog(AssignmentEntity.class);
private static Cache assignmentCache = null;
protected static final int DEFAULT_EXPIRATION = 10 * 60;
private SimplePageBean simplePageBean;
public void setSimplePageBean(SimplePageBean simplePageBean) {
this.simplePageBean = simplePageBean;
}
private LessonEntity nextEntity = null;
public void setNextEntity(LessonEntity e) {
nextEntity = e;
}
public LessonEntity getNextEntity() {
return nextEntity;
}
static MemoryService memoryService = null;
public void setMemoryService(MemoryService m) {
memoryService = m;
}
static MessageLocator messageLocator = null;
public void setMessageLocator(MessageLocator m) {
messageLocator = m;
}
public void init () {
assignmentCache = memoryService
.newCache("org.sakaiproject.lessonbuildertool.service.AssignmentEntity.cache");
log.info("init()");
}
public void destroy()
{
assignmentCache.destroy();
assignmentCache = null;
log.info("destroy()");
}
// to create bean. the bean is used only to call the pseudo-static
// methods such as getEntitiesInSite. So type, id, etc are left uninitialized
protected AssignmentEntity() {
}
protected AssignmentEntity(int type, String id, int level) {
this.type = type;
this.id = id;
this.level = level;
}
public String getToolId() {
return "sakai.assignment.grades";
}
// the underlying object, something Sakaiish
protected String id;
protected int type;
protected int level;
// not required fields. If we need to look up
// the actual objects, lets us cache them
protected Assignment assignment;
public Assignment getAssignment(String ref) {
return getAssignment(ref, false);
}
public Assignment getAssignment(String ref, boolean nocache) {
Assignment ret = (Assignment)assignmentCache.get(ref);
if (!nocache && ret != null)
return ret;
try {
ret = AssignmentService.getAssignment(ref);
} catch (Exception e) {
ret = null;
}
if (ret != null)
assignmentCache.put(ref, ret, DEFAULT_EXPIRATION);
return ret;
}
// type of the underlying object
public int getType() {
return type;
}
public int getLevel() {
return level;
}
public int getTypeOfGrade() {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return 1;
AssignmentContent content = assignment.getContent();
if (content == null) {
return 1;
} else
return assignment.getContent().getTypeOfGrade();
}
// hack for forums. not used for assessments, so always ok
public boolean isUsable() {
return true;
}
public String getReference() {
return "/" + ASSIGNMENT + "/" + id;
}
public List<LessonEntity> getEntitiesInSite() {
return getEntitiesInSite(null);
}
// find topics in site, but organized by forum
public List<LessonEntity> getEntitiesInSite(SimplePageBean bean) {
Iterator i = AssignmentService.getAssignmentsForContext(ToolManager.getCurrentPlacement().getContext());
List<LessonEntity> ret = new ArrayList<LessonEntity>();
// security. assume this is only used in places where it's OK, so skip security checks
while (i.hasNext()) {
Assignment a = (Assignment) i.next();
String deleted = a.getProperties().getProperty(ResourceProperties.PROP_ASSIGNMENT_DELETED);
// this somewhat odd test for deleted is the one used in the Assignment code
if ((deleted == null || "".equals(deleted)) && !a.getDraft()) {
AssignmentEntity entity = new AssignmentEntity(TYPE_ASSIGNMENT, a.getId(), 1);
entity.assignment = a;
entity.simplePageBean = bean;
ret.add(entity);
}
}
if (nextEntity != null)
ret.addAll(nextEntity.getEntitiesInSite(bean));
return ret;
}
public LessonEntity getEntity(String ref) {
return getEntity(ref, null);
}
public LessonEntity getEntity(String ref, SimplePageBean bean) {
int i = ref.indexOf("/",1);
if (i < 0) {
// old format, just the number
AssignmentEntity entity = new AssignmentEntity(TYPE_ASSIGNMENT, ref, 1);
entity.simplePageBean = bean;
return entity;
}
String typeString = ref.substring(1, i);
String idString = ref.substring(i+1);
String id = "";
try {
id = idString;
} catch (Exception ignore) {
return null;
}
if (typeString.equals(ASSIGNMENT)) {
AssignmentEntity entity = new AssignmentEntity(TYPE_ASSIGNMENT, id, 1);
entity.simplePageBean = bean;
return entity;
} else if (nextEntity != null) {
return nextEntity.getEntity(ref, simplePageBean);
} else
return null;
}
// properties of entities
public String getTitle() {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return null;
return assignment.getTitle();
}
public String getUrl() {
if (simplePageBean != null) {
return ServerConfigurationService.getToolUrl() + "/" + simplePageBean.getCurrentTool("sakai.assignment.grades") +
"?assignmentReference=/assignment/a/" + simplePageBean.getCurrentSiteId() + "/" + id + "&panel=Main&sakai_action=doView_submission";
}
Site site = null;
try {
site = SiteService.getSite(ToolManager.getCurrentPlacement().getContext());
} catch (Exception impossible) {
return null;
}
ToolConfiguration tool = site.getToolForCommonId("sakai.assignment.grades");
if(tool == null) {
return null;
}
String placement = tool.getId();
// https://sakai-test2.oirt.rutgers.edu/portal/tool/6b328952-cbcb-494b-0035-3c07120e4499?assignmentReference=/assignment/a/0aaae6ef-cb01-4578-0099-888d344b524b/0e52c5f6-ba73-40d2-961c-286533d59148&panel=Main&sakai_action=doView_submission
return ServerConfigurationService.getToolUrl() + "/" + placement + "?assignmentReference=/assignment/a/" + site.getId() + "/" + id + "&panel=Main&sakai_action=doView_submission";
// following was broken in 2.8.1
// return "/direct/assignment/" + id;
}
public Date getDueDate() {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return null;
return new Date(assignment.getDueTime().getTime());
}
// the following methods all take references. So they're in effect static.
// They ignore the entity from which they're called.
// The reason for not making them a normal method is that many of the
// implementations seem to let you set access control and find submissions
// from a reference, without needing the actual object. So doing it this
// way could save some database activity
// access control
public boolean addEntityControl(String siteId, String groupId) throws IOException {
Site site = null;
String ref = "/assignment/a/" + siteId + "/" + id;
try {
site = SiteService.getSite(siteId);
} catch (Exception e) {
log.warn("Unable to find site " + siteId, e);
return false;
}
AssignmentEdit edit = null;
try {
edit = AssignmentService.editAssignment(ref);
} catch (IdUnusedException e) {
log.warn("ID unused ", e);
return false;
} catch (PermissionException e) {
log.warn(e);
return false;
} catch (InUseException e) {
log.warn(e);
return false;
}
boolean doCancel = true;
try {
// need this to make sure we always unlock
if (edit.getAccess() == Assignment.AssignmentAccess.GROUPED) {
Collection<String> groups = edit.getGroups();
groupId = "/site/" + siteId + "/group/" + groupId;
if (groups.contains(groupId)) {
return true;
}
Group group = site.getGroup(groupId);
if (group == null) {
return false;
}
// odd; getgruops returns a list of string
// but setgroupacces wants a collection of actual groups
// so we have to copy the list
Collection<Group> newGroups = new ArrayList<Group>();
for (String gid : groups) {
newGroups.add(site.getGroup(gid));
}
// now add in this one
newGroups.add(group);
try {
edit.setGroupAccess(newGroups);
} catch (PermissionException e) {
log.warn(e);
return false;
}
AssignmentService.commitEdit(edit);
doCancel = false;
return true;
} else {
// currently not grouped
Collection groups = new ArrayList<String>();
Group group = site.getGroup(groupId);
if (group == null) {
log.warn("Could not find Group");
return false;
}
groups.add(group);
try {
// this change mode to grouped
edit.setGroupAccess(groups);
} catch (PermissionException e) {
log.warn(e);
return false;
}
AssignmentService.commitEdit(edit);
doCancel = false;
return true;
}
} catch (Exception e) {
log.warn(e);
return false;
} finally {
if (doCancel) {
AssignmentService.commitEdit(edit);
}
}
}
public boolean removeEntityControl(String siteId, String groupId) throws IOException {
Site site = null;
String ref = "/assignment/a/" + siteId + "/" + id;
try {
site = SiteService.getSite(siteId);
} catch (Exception e) {
log.warn("Unable to find site " + siteId, e);
return false;
}
AssignmentEdit edit = null;
try {
edit = AssignmentService.editAssignment(ref);
} catch (IdUnusedException e) {
log.warn(e);
return false;
} catch (PermissionException e) {
log.warn(e);
return false;
} catch (InUseException e) {
log.warn(e);
return false;
}
boolean doCancel = true;
try {
// need this to make sure we always unlock
if (edit.getAccess() == Assignment.AssignmentAccess.GROUPED) {
Collection<String> groups = edit.getGroups();
groupId = "/site/" + siteId + "/group/" + groupId;
if (!groups.contains(groupId)) {
// nothing to do
return true;
}
// odd; getgruops returns a list of string
// but setgroupacces wants a collection of actual groups
// so we have to copy the list
Collection<Group> newGroups = new ArrayList<Group>();
for (String gid : groups) {
// remove our group
if (!gid.equals(groupId)) {
newGroups.add(site.getGroup(gid));
}
}
if (newGroups.size() > 0) {
// there's groups left, just remove ours
try {
edit.setGroupAccess(newGroups);
} catch (PermissionException e) {
log.warn(e);
return false;
}
} else {
// no groups left, put site access back
edit.setAccess(Assignment.AssignmentAccess.SITE);
edit.clearGroupAccess();
}
AssignmentService.commitEdit(edit);
doCancel = false;
return true;
} else {
// currently not grouped
// nothing to do
return true;
}
} catch (Exception e) {
log.warn(e);
return false;
} finally {
if (doCancel) {
AssignmentService.commitEdit(edit);
}
}
}
// submission
// do we need the data from submission?
public boolean needSubmission(){
return true;
}
public LessonSubmission getSubmission(String userId) {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null) {
log.warn("can't find assignment " + id);
return null;
}
User user = null;
AssignmentSubmission submission = null;
try {
user = UserDirectoryService.getUser(userId);
submission = AssignmentService.getSubmission(assignment.getReference(), user);
} catch (Exception e) {
return null;
}
if (submission == null || !submission.getSubmitted())
return null;
LessonSubmission ret= new LessonSubmission(null);
if (submission.getGradeReleased()) {
String grade = submission.getGrade();
ret.setGradeString(grade);
}
return ret;
}
// we can do this for real, but the API will cause us to get all the submissions in full, not just a count.
// I think it's cheaper to get the best assessment, since we don't actually care whether it's 1 or >= 1.
public int getSubmissionCount(String user) {
if (getSubmission(user) == null)
return 0;
else
return 1;
}
// URL to create a new item. Normally called from the generic entity, not a specific one
// can't be null
public List<UrlItem> createNewUrls(SimplePageBean bean) {
ArrayList<UrlItem> list = new ArrayList<UrlItem>();
String tool = bean.getCurrentTool("sakai.assignment.grades");
if (tool != null) {
tool = ServerConfigurationService.getToolUrl() + "/" + tool + "?view=lisofass1&panel=Main&sakai_action=doView";
list.add(new UrlItem(tool, messageLocator.getMessage("simplepage.create_assignment")));
}
if (nextEntity != null)
list.addAll(nextEntity.createNewUrls(bean));
return list;
}
// URL to edit an existing entity.
// Can be null if we can't get one or it isn't needed
public String editItemUrl(SimplePageBean bean) {
String tool = bean.getCurrentTool("sakai.assignment.grades");
if (tool == null)
return null;
return ServerConfigurationService.getToolUrl() + "/" + tool + "?assignmentId=/assignment/a/" + bean.getCurrentSiteId() +
"/" + id + "&panel=Main&sakai_action=doEdit_assignment";
}
// for most entities editItem is enough, however tests allow separate editing of
// contents and settings. This will be null except in that situation
public String editItemSettingsUrl(SimplePageBean bean) {
return null;
}
public boolean objectExists() {
if (assignment == null)
assignment = getAssignment(id, true);
return assignment != null;
}
public boolean notPublished(String ref) {
return false;
}
// return the list of groups if the item is only accessible to specific groups
// null if it's accessible to the whole site.
public Collection<String> getGroups(boolean nocache) {
if (nocache)
assignment = getAssignment(id, true);
else if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return null;
if (assignment.getAccess() != Assignment.AssignmentAccess.GROUPED)
return null;
Collection<String> groupRefs = assignment.getGroups();
List<String> groupIds = new ArrayList<String>();
for (String ref: groupRefs) {
int i = ref.lastIndexOf("/");
if (i >= 0)
ref = ref.substring(i+1);
groupIds.add(ref);
}
return groupIds;
}
// set the item to be accessible only to the specific groups.
// null to make it accessible to the whole site
public void setGroups(Collection<String> groups) {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return;
String siteId = ToolManager.getCurrentPlacement().getContext();
Site site = null;
String ref = "/assignment/a/" + siteId + "/" + id;
try {
site = SiteService.getSite(siteId);
} catch (Exception e) {
log.warn("Unable to find site " + siteId, e);
return;
}
AssignmentEdit edit = null;
try {
edit = AssignmentService.editAssignment(ref);
} catch (IdUnusedException e) {
log.warn("ID unused ", e);
return;
} catch (PermissionException e) {
log.warn(e);
return;
} catch (InUseException e) {
log.warn(e);
return;
}
boolean doCancel = true;
try {
// need this to make sure we always unlock
if (groups != null && groups.size() > 0) {
List<Group> groupObjs = new ArrayList<Group>();
for (String groupId : groups) {
Group group = site.getGroup(groupId);
if (group != null)
groupObjs.add(group);
}
edit.setGroupAccess(groupObjs);
} else {
edit.setAccess(Assignment.AssignmentAccess.SITE);
edit.clearGroupAccess();
}
AssignmentService.commitEdit(edit);
doCancel = false;
return;
} catch (Exception e) {
log.warn(e);
return;
} finally {
if (doCancel) {
AssignmentService.commitEdit(edit);
}
}
}
public String getObjectId() {
String title = getTitle();
if (title == null)
return null;
return "assignment/" + id + "/" + title;
}
public String findObject(String objectid, Map<String,String>objectMap, String siteid) {
if (!objectid.startsWith("assignment/")) {
if (nextEntity != null) {
return nextEntity.findObject(objectid, objectMap, siteid);
}
return null;
}
String realobjectid = objectid;
// isolate forum_topic/NNN from title
int i = objectid.indexOf("/", "assignment/".length());
if (i > 0)
realobjectid = objectid.substring(0, i);
String newassignment = objectMap.get(realobjectid);
if (newassignment != null)
return "/" + newassignment; // sakaiid is /assignment/ID
// not in map. try title, but only if title given
if (i <= 0)
return null; // no title
// i is start of title
String title = objectid.substring(i+1);
Iterator aIter = AssignmentService.getAssignmentsForContext(ToolManager.getCurrentPlacement().getContext());
// security. assume this is only used in places where it's OK, so skip security checks
while (aIter.hasNext()) {
Assignment a = (Assignment) aIter.next();
if (title.equals(a.getTitle()))
return "/assignment/" + a.getId();
}
return null;
}
public String importObject(String title, String href, String mime){
String context = ToolManager.getCurrentPlacement().getContext();
try {
AssignmentContentEdit c = AssignmentService.addAssignmentContent(context);
c.setTitle(title);
// no instructions. It causes problems on export, because we can' recognize it as the special case.
c.setInstructions("");
// c.setInstructions(messageLocator.getMessage("simplepage.assign_seeattach"));
c.setHonorPledge(1); // no
c.setTypeOfSubmission(3); // inline and attachment
c.setAllowReviewService(false);
c.setTypeOfGrade(1); // ungraded
c.setAllowAttachments(true);
c.clearAttachments();
c.addAttachment(EntityManager.newReference("/content" + href));
c.setContext(context);
AssignmentService.commitEdit(c);
AssignmentEdit a = AssignmentService.addAssignment(context);
Time now = TimeService.newTime();
a.setOpenTime(now);
TimeBreakdown year = now.breakdownLocal();
year.setYear(year.getYear() + 1);
now = TimeService.newTimeLocal(year);
a.setDueTime(now);
a.setDraft(false);
a.setAccess(Assignment.AssignmentAccess.SITE);
a.clearGroupAccess();
a.setSection("");
a.setTitle(title);
a.setContent(c);
AssignmentService.commitEdit(a);
return "/assignment/" + a.getId();
} catch (Exception e) {
System.out.println("can't create assignment " + e);
};
return null;
}
public String getSiteId() {
if (assignment == null)
assignment = getAssignment(id);
if (assignment == null)
return null;
return assignment.getContext();
}
}