001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.odf.course; 017 018import java.time.LocalDate; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import javax.mail.MessagingException; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.commons.lang3.StringUtils; 033 034import org.ametys.cms.ObservationConstants; 035import org.ametys.cms.content.ContentHelper; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.ModifiableContent; 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.right.RightManager; 041import org.ametys.core.right.RightManager.RightResult; 042import org.ametys.core.user.CurrentUserProvider; 043import org.ametys.core.user.User; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.user.UserManager; 046import org.ametys.core.util.I18nUtils; 047import org.ametys.core.util.mail.SendMailHelper; 048import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareComposite; 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.model.ModelItem; 052import org.ametys.runtime.plugin.component.AbstractLogEnabled; 053 054/** 055 * Helper for shareable course status 056 */ 057public class ShareableCourseStatusHelper extends AbstractLogEnabled implements Component, Serviceable 058{ 059 /** The component role. */ 060 public static final String ROLE = ShareableCourseStatusHelper.class.getName(); 061 062 /** The metadata name for the shareable course composite */ 063 public static final String SHAREABLE_COURSE_COMPOSITE_METADATA = "shareable-workflow"; 064 065 /** The metadata name for the shareable course status */ 066 public static final String SHAREABLE_COURSE_STATUS_METADATA = "status"; 067 068 /** The metadata name for the date of the "proposed" state */ 069 private static final String __PROPOSED_DATE_METADATA = "proposed_date"; 070 071 /** The metadata name for the author of the "proposed" state */ 072 private static final String __PROPOSED_AUTHOR_METADATA = "proposed_author"; 073 074 /** The metadata name for the date of the "validated" state */ 075 private static final String __VALIDATED_DATE_METADATA = "validated_date"; 076 077 /** The metadata name for the author of the "validated" state */ 078 private static final String __VALIDATED_AUTHOR_METADATA = "validated_author"; 079 080 /** The notification right id */ 081 private static final String __NOTIFICATION_RIGHT_ID = "ODF_Rights_Shareable_Course_Receive_Notification"; 082 083 /** The propose right id */ 084 private static final String __PROPOSE_RIGHT_ID = "ODF_Rights_Shareable_Course_Propose"; 085 086 /** The validate right id */ 087 private static final String __VALIDATE_RIGHT_ID = "ODF_Rights_Shareable_Course_Validate"; 088 089 /** The refusal right id */ 090 private static final String __REFUSE_RIGHT_ID = "ODF_Rights_Shareable_Course_Refuse"; 091 092 /** The current user provider */ 093 protected CurrentUserProvider _currentUserProvider; 094 095 /** The observation manager */ 096 protected ObservationManager _observationManager; 097 098 /** The right manager */ 099 protected RightManager _rightManager; 100 101 /** The user manager */ 102 protected UserManager _userManager; 103 104 /** The i18n utils */ 105 protected I18nUtils _i18nUtils; 106 107 /** The content helper */ 108 protected ContentHelper _contentHelper; 109 110 /** 111 * Enumeration for the shareable course status 112 */ 113 public enum ShareableStatus 114 { 115 /** Aucun */ 116 NONE, 117 /** Proposé */ 118 PROPOSED, 119 /** Validé */ 120 VALIDATED, 121 } 122 123 @Override 124 public void service(ServiceManager manager) throws ServiceException 125 { 126 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 127 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 128 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 129 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 130 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 131 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 132 } 133 134 /** 135 * Get the shareable status of the course 136 * @param content the content 137 * @return the shareable course status 138 */ 139 public ShareableStatus getShareableStatus(Content content) 140 { 141 String metadataPath = SHAREABLE_COURSE_COMPOSITE_METADATA + ModelItem.ITEM_PATH_SEPARATOR + SHAREABLE_COURSE_STATUS_METADATA; 142 String status = content.hasValue(metadataPath) ? content.getValue(metadataPath, false, ShareableStatus.NONE.name()) : ShareableStatus.NONE.name(); 143 return ShareableStatus.valueOf(status.toUpperCase()); 144 } 145 146 /** 147 * Set the workflow state attribute (date, login) to the content 148 * @param content the content 149 * @param validationDate the validation date 150 * @param user the user 151 * @param status the shareable course status 152 * @param ignoreRights true to ignore user rights 153 */ 154 public void setWorkflowStateAttribute(ModifiableContent content, LocalDate validationDate, UserIdentity user, ShareableStatus status, boolean ignoreRights) 155 { 156 boolean hasChanges = false; 157 switch (status) 158 { 159 case NONE : 160 if (ignoreRights || _rightManager.hasRight(user, __REFUSE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW)) 161 { 162 UserIdentity proposedStateAuthor = getProposedStateAuthor(content); 163 hasChanges = removeShareableWorkflow(content); 164 165 // Send mail to users who propose the course as shareable 166 _sendNotificationMail(content, Collections.singleton(proposedStateAuthor), "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_REFUSE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_REFUSE"); 167 } 168 break; 169 case PROPOSED : 170 if (ignoreRights || _rightManager.hasRight(user, __PROPOSE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW)) 171 { 172 hasChanges = setProposedStateAttribute(content, validationDate, user); 173 174 // Send mail to users who have the right "ODF_Rights_Shareable_Course_Receive_Notification" 175 Set<UserIdentity> users = _rightManager.getAllowedUsers(__NOTIFICATION_RIGHT_ID, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 176 _sendNotificationMail(content, users, "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_PROPOSE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_PROPOSE"); 177 } 178 break; 179 case VALIDATED : 180 if (ignoreRights || _rightManager.hasRight(user, __VALIDATE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW)) 181 { 182 hasChanges = setValidatedStateAttribute(content, validationDate, user); 183 184 // Send mail to users who propose the course as shareable 185 UserIdentity proposedStateAuthor = getProposedStateAuthor(content); 186 if (proposedStateAuthor != null) 187 { 188 _sendNotificationMail(content, Collections.singleton(proposedStateAuthor), "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_VALIDATE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_VALIDATE"); 189 } 190 } 191 break; 192 default : 193 getLogger().error("{} is an unknown shareable course status", status); 194 } 195 196 if (hasChanges) 197 { 198 _notifyShareableCourseWorkflowModification(content); 199 } 200 } 201 202 /** 203 * Remove the shareable course workflow metadata. 204 * @param content The content to clean 205 * @return <code>true</code> if the content has changed 206 */ 207 public boolean removeShareableWorkflow(ModifiableContent content) 208 { 209 if (content.hasValue(SHAREABLE_COURSE_COMPOSITE_METADATA)) 210 { 211 content.removeValue(SHAREABLE_COURSE_COMPOSITE_METADATA); 212 213 if (content.needsSave()) 214 { 215 content.saveChanges(); 216 return true; 217 } 218 } 219 220 return false; 221 } 222 223 /** 224 * Set the attribute for 'proposed' state 225 * @param content the content 226 * @param proposeDate the proposed date 227 * @param user the user 228 * @return <code>true</code> if the content has changed. 229 */ 230 public boolean setProposedStateAttribute(ModifiableContent content, LocalDate proposeDate, UserIdentity user) 231 { 232 ModifiableModelAwareComposite composite = content.getComposite(SHAREABLE_COURSE_COMPOSITE_METADATA, true); 233 234 composite.setValue(__PROPOSED_DATE_METADATA, proposeDate); 235 composite.setValue(__PROPOSED_AUTHOR_METADATA, user); 236 237 composite.setValue(SHAREABLE_COURSE_STATUS_METADATA, ShareableStatus.PROPOSED.name()); 238 239 if (content.needsSave()) 240 { 241 content.saveChanges(); 242 return true; 243 } 244 245 return false; 246 } 247 248 /** 249 * Set the attribute for 'validated' state 250 * @param content the content 251 * @param validationDate the validation date 252 * @param user the login 253 * @return <code>true</code> if the content has changed. 254 */ 255 public boolean setValidatedStateAttribute(ModifiableContent content, LocalDate validationDate, UserIdentity user) 256 { 257 ModifiableModelAwareComposite composite = content.getComposite(SHAREABLE_COURSE_COMPOSITE_METADATA, true); 258 259 composite.setValue(__VALIDATED_DATE_METADATA, validationDate); 260 composite.setValue(__VALIDATED_AUTHOR_METADATA, user); 261 262 composite.setValue(SHAREABLE_COURSE_STATUS_METADATA, ShareableStatus.VALIDATED.name()); 263 264 if (content.needsSave()) 265 { 266 content.saveChanges(); 267 return true; 268 } 269 270 return false; 271 } 272 273 /** 274 * Get 'proposed' state author 275 * @param content the content 276 * @return the 'proposed' state author 277 */ 278 public UserIdentity getProposedStateAuthor(Content content) 279 { 280 return content.getValue(SHAREABLE_COURSE_COMPOSITE_METADATA + ModelItem.ITEM_PATH_SEPARATOR + __PROPOSED_AUTHOR_METADATA, false, null); 281 } 282 283 /** 284 * Send a notification with the content modified event. 285 * @param content The content to notify on 286 */ 287 protected void _notifyShareableCourseWorkflowModification(Content content) 288 { 289 Map<String, Object> eventParams = new HashMap<>(); 290 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 291 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 292 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 293 } 294 295 /** 296 * Send mail to users 297 * @param content the course modified 298 * @param usersNotified the list of user to notify 299 * @param mailSubjectKey the mail subject i18n key 300 * @param mailBodyKey the mail body i18n key 301 */ 302 protected void _sendNotificationMail(Content content, Set<UserIdentity> usersNotified, String mailSubjectKey, String mailBodyKey) 303 { 304 UserIdentity currentUser = _currentUserProvider.getUser(); 305 306 String mailSubject = _getMailSubject(mailSubjectKey, content); 307 String mailBody = _getMailBody(mailBodyKey, _userManager.getUser(currentUser), content); 308 String from = Config.getInstance().getValue("smtp.mail.from"); 309 310 for (UserIdentity userIdentity : usersNotified) 311 { 312 User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 313 if (user != null && StringUtils.isNotBlank(user.getEmail())) 314 { 315 String mail = user.getEmail(); 316 try 317 { 318 SendMailHelper.sendMail(mailSubject, null, mailBody, mail, from, true); 319 } 320 catch (MessagingException e) 321 { 322 getLogger().warn("Could not send a notification mail to " + mail, e); 323 } 324 } 325 } 326 } 327 328 /** 329 * Get the subject of mail 330 * @param subjectI18nKey the i18n key to use for subject 331 * @param content the content 332 * @return the subject 333 */ 334 protected String _getMailSubject (String subjectI18nKey, Content content) 335 { 336 I18nizableText subjectKey = new I18nizableText("plugin.odf", subjectI18nKey, _getSubjectI18nParams(content)); 337 return _i18nUtils.translate(subjectKey, content.getLanguage()); 338 } 339 340 /** 341 * Get the i18n parameters of mail subject 342 * @param content the content 343 * @return the i18n parameters 344 */ 345 protected List<String> _getSubjectI18nParams (Content content) 346 { 347 List<String> params = new ArrayList<>(); 348 params.add(_contentHelper.getTitle(content)); 349 return params; 350 } 351 352 /** 353 * Get the text body of mail 354 * @param bodyI18nKey the i18n key to use for body 355 * @param user the caller 356 * @param content the content 357 * @return the text body 358 */ 359 protected String _getMailBody (String bodyI18nKey, User user, Content content) 360 { 361 I18nizableText bodyKey = new I18nizableText("plugin.odf", bodyI18nKey, _getBodyI18nParams(user, content)); 362 return _i18nUtils.translate(bodyKey, content.getLanguage()); 363 } 364 365 /** 366 * Get the i18n parameters of mail body text 367 * @param user the caller 368 * @param content the content 369 * @return the i18n parameters 370 */ 371 protected List<String> _getBodyI18nParams (User user, Content content) 372 { 373 List<String> params = new ArrayList<>(); 374 375 params.add(user.getFullName()); // {0} 376 params.add(content.getTitle()); // {1} 377 params.add(_getContentUri(content)); // {2} 378 379 return params; 380 } 381 382 /** 383 * Get the content uri 384 * @param content the content 385 * @return the content uri 386 */ 387 protected String _getContentUri(Content content) 388 { 389 String requestUri = _getRequestUri(); 390 return requestUri + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27"; 391 } 392 393 /** 394 * Get the request URI. 395 * @return the full request URI. 396 */ 397 protected String _getRequestUri() 398 { 399 return StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 400 } 401 402}