/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.commons.services.commentAndRating.ui;
import org.olat.core.commons.services.commentAndRating.CommentAndRatingSecurityCallback;
import org.olat.core.commons.services.commentAndRating.CommentAndRatingService;
import org.olat.core.commons.services.commentAndRating.model.UserRating;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.link.LinkFactory;
import org.olat.core.gui.components.rating.RatingComponent;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.BasicController;
import org.olat.core.id.OLATResourceable;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.event.GenericEventListener;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Description:<br>
* The user comments and rating controller displays a minimized view of the
* comments and rating with the option to expand to full view. Use this
* controller whenever you want a resource to be commented.
* <p>
* Events fired by this controller:
* <ul>
* <li>UserCommentsAndRatingsController.EVENT_COMMENT_LINK_CLICKED when user clicked the comments link</li>
* <li>UserCommentsAndRatingsController.EVENT_RATING_CHANGED when user changed the rating</li>
* </ul>
* <P>
* Initial Date: 30.11.2009 <br>
*
* @author gnaegi
*/
public class UserCommentsAndRatingsController extends BasicController implements GenericEventListener {
private static final int RATING_MAX = 5;
// Events
public static final Event EVENT_COMMENT_LINK_CLICKED = new Event("comment_link_clicked");
public static final Event EVENT_RATING_CHANGED = new Event("rating_changed");
private final VelocityContainer userCommentsAndRatingsVC;
private final OLATResourceable USER_COMMENTS_AND_RATING_CHANNEL;
private final boolean canExpandToFullView;
private Object userObject;
// Comments
private Link commentsCountLink;
private Long commentsCount;
private UserCommentsController commentsCtr;
// Ratings
private RatingComponent ratingUserC;
private RatingComponent ratingAverageC;
private UserRating userRating;
// Controller state
private boolean isExpanded = false; // default
// Configuration
private final String oresSubPath;
private final OLATResourceable ores;
private final CommentAndRatingSecurityCallback securityCallback;
@Autowired
private CommentAndRatingService commentAndRatingService;
/**
* Constructor for a user combined user comments and ratings controller. Use
* the CommentAndRatingService instead of calling this constructor directly!
*
* @param ureq
* @param wControl
* @param ores
* @param oresSubPath
* @param securityCallback
* @param enableComments
* @param enableRatings
* @param canExpandToFullView
*/
public UserCommentsAndRatingsController(UserRequest ureq, WindowControl wControl, OLATResourceable ores, String oresSubPath,
CommentAndRatingSecurityCallback securityCallback, boolean enableComments, boolean enableRatings, boolean canExpandToFullView) {
super(ureq, wControl);
this.ores = ores;
this.oresSubPath = oresSubPath;
this.securityCallback = securityCallback;
userCommentsAndRatingsVC = createVelocityContainer("userCommentsAndRatings");
this.canExpandToFullView = canExpandToFullView;
putInitialPanel(userCommentsAndRatingsVC);
// Add comments views
if (enableComments && securityCallback.canViewComments()) {
userCommentsAndRatingsVC.contextPut("enableComments", Boolean.valueOf(enableComments));
// Link with comments count to expand view
commentsCountLink = LinkFactory.createLink("comments.count", this.userCommentsAndRatingsVC, this);
commentsCountLink.setTitle("comments.count.tooltip");
// Init view with values from DB
updateCommentCountView();
}
// Add ratings view
userCommentsAndRatingsVC.contextPut("viewIdent", CodeHelper.getRAMUniqueID());
userCommentsAndRatingsVC.contextPut("enableRatings", Boolean.valueOf(enableRatings));
if (enableRatings) {
if (securityCallback.canRate()) {
ratingUserC = new RatingComponent("userRating", 0, RATING_MAX, true);
ratingUserC.addListener(this);
userCommentsAndRatingsVC.put("ratingUserC", ratingUserC);
ratingUserC.setShowRatingAsText(true);
ratingUserC.setTitle("rating.personal.title");
ratingUserC.setCssClass("o_rating_personal");
}
if (securityCallback.canViewRatingAverage()) {
ratingAverageC = new RatingComponent("ratingAverageC", 0, RATING_MAX, false);
ratingAverageC.addListener(this);
userCommentsAndRatingsVC.put("ratingAverageC", ratingAverageC);
ratingAverageC.setShowRatingAsText(true);
ratingAverageC.setTitle("rating.average.title");
ratingAverageC.setTranslateExplanation(false);
ratingAverageC.setCssClass("o_rating_average");
}
// Init view with values from DB
updateRatingView();
}
// Register to event channel for comments count change events
USER_COMMENTS_AND_RATING_CHANNEL = ores;
CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(this, getIdentity(), USER_COMMENTS_AND_RATING_CHANNEL);
}
/**
* Method to manually expand the comments view
*
* @param ureq
*/
public void expandComments(UserRequest ureq) {
if (canExpandToFullView) {
commentsCtr = new UserCommentsController(ureq, getWindowControl(), ores, oresSubPath, securityCallback);
listenTo(commentsCtr);
userCommentsAndRatingsVC.put("commentsCtr", commentsCtr.getInitialComponent());
isExpanded = true;
// Update our counter view in case changed since last loading
if (getCommentsCount() != commentsCtr.getCommentsCount()) {
updateCommentCountView();
}
}
}
public void expandCommentsAt(UserRequest ureq, Long commentId) {
expandComments(ureq);
commentsCtr.scrollTo(commentId);
}
/**
* Method to manually collapse the comments view
*
* @param ureq
*/
public void collapseComments() {
if (canExpandToFullView) {
userCommentsAndRatingsVC.remove(commentsCtr.getInitialComponent());
removeAsListenerAndDispose(commentsCtr);
commentsCtr = null;
isExpanded = false;
}
}
/**
* Package helper method to update the comment count view
*/
void updateCommentCountView() {
if (commentsCountLink != null) {
commentsCount = commentAndRatingService.countComments(ores, oresSubPath);
commentsCountLink.setCustomDisplayText(translate("comments.count", commentsCount.toString()));
String css = commentsCount > 0 ? "o_icon o_icon_comments o_icon-lg" : "o_icon o_icon_comments_none o_icon-lg";
commentsCountLink.setCustomEnabledLinkCSS("o_comments");
commentsCountLink.setIconLeftCSS(css);
}
}
/**
* Package helper to update the rating view
*/
void updateRatingView() {
if (ratingUserC != null) {
userRating = commentAndRatingService.getRating(getIdentity(), ores, oresSubPath);
if (userRating != null) {
ratingUserC.setCurrentRating(userRating.getRating());
}
}
if (ratingAverageC != null) {
ratingAverageC.setCurrentRating(commentAndRatingService.calculateRatingAverage(ores, oresSubPath));
long ratingsCounter = commentAndRatingService.countRatings(ores, oresSubPath);
ratingAverageC.setExplanation(translate("rating.average.explanation", ratingsCounter + ""));
}
}
/**
* Package method to get current number of
*
* @return
*/
long getCommentsCount() {
if (commentsCount != null) {
return commentsCount.longValue();
} else {
return 0l;
}
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose()
*/
@Override
protected void doDispose() {
// Remove event listener
CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(this, USER_COMMENTS_AND_RATING_CHANNEL);
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.components.Component,
* org.olat.core.gui.control.Event)
*/
@Override
protected void event(UserRequest ureq, Component source, Event event) {
// Forward comments counter links to parent listeners
if (source == commentsCountLink) {
if (canExpandToFullView) {
if (isExpanded) {
// Collapse
collapseComments();
} else {
// Expand now
expandComments(ureq);
}
}
fireEvent(ureq, EVENT_COMMENT_LINK_CLICKED);
} else if (source == ratingUserC) {
// Update user rating - convert component floats to integers (only discrete values possible)
Integer newRating = Float.valueOf(ratingUserC.getCurrentRating()).intValue();
if (userRating == null) {
// Create new rating
userRating = commentAndRatingService.createRating(getIdentity(), ores, oresSubPath, newRating);
} else {
// Update existing rating
userRating = commentAndRatingService.updateRating(userRating, newRating);
}
// Update GUI
updateRatingView();
// Notify other user who also have this component
UserRatingChangedEvent changedEvent = new UserRatingChangedEvent(this, this.oresSubPath);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(changedEvent, USER_COMMENTS_AND_RATING_CHANNEL);
}
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
*/
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
if (source == commentsCtr) {
if (event == UserCommentDisplayController.COMMENT_COUNT_CHANGED) {
updateCommentCountView();
// notify other user who also have this component
UserCommentsCountChangedEvent changedEvent = new UserCommentsCountChangedEvent(this, oresSubPath);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(changedEvent, USER_COMMENTS_AND_RATING_CHANNEL);
} else if (event == Event.CANCELLED_EVENT) {
fireEvent(ureq, event);
}
}
}
/**
* Store a user object in this controller that can be retrieved in a later
* stage when a workflow is done
*
* @param userObject
*/
public void setUserObject(Object userObject) {
this.userObject = userObject;
}
/**
* Get the user object associated with this controller
* @return
*/
public Object getUserObject() {
return userObject;
}
@Override
public void event(Event event) {
if (event instanceof UserCommentsCountChangedEvent) {
UserCommentsCountChangedEvent changedEvent = (UserCommentsCountChangedEvent) event;
if (!changedEvent.isSentByMyself(this) && !canExpandToFullView) {
// Update counter in GUI, but only when in minimized mode (otherwise might confuse user)
if ( (oresSubPath == null && changedEvent.getOresSubPath() == null)
|| (oresSubPath != null && oresSubPath.equals(changedEvent.getOresSubPath()))) {
updateCommentCountView();
}
}
} else if (event instanceof UserRatingChangedEvent) {
UserRatingChangedEvent changedEvent = (UserRatingChangedEvent) event;
// Update rating in GUI
if (!changedEvent.isSentByMyself(this)) {
if ( (oresSubPath == null && changedEvent.getOresSubPath() == null)
|| (oresSubPath != null && oresSubPath.equals(changedEvent.getOresSubPath()))) {
updateRatingView();
}
}
}
}
}