001/* 002 * Copyright 2020 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 */ 016 017package org.ametys.plugins.odfpilotage.helper; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.components.ContextHelper; 036import org.apache.cocoon.environment.Request; 037import org.apache.cocoon.environment.Session; 038 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.repository.ModifiableDefaultContent; 041import org.ametys.cms.workflow.ContentWorkflowHelper; 042import org.ametys.core.ui.Callable; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.odf.course.Course; 045import org.ametys.odf.coursepart.CoursePart; 046import org.ametys.odf.enumeration.OdfReferenceTableHelper; 047import org.ametys.odf.orgunit.OrgUnit; 048import org.ametys.odf.program.Container; 049import org.ametys.odf.program.Program; 050import org.ametys.odf.program.SubProgram; 051import org.ametys.odf.tree.ODFContentsTreeHelper; 052import org.ametys.plugins.contentstree.TreeConfiguration; 053import org.ametys.plugins.odfpilotage.cost.CostComputationComponent; 054import org.ametys.plugins.odfpilotage.cost.entity.CostComputationData; 055import org.ametys.plugins.odfpilotage.cost.entity.CoursePartCostData; 056import org.ametys.plugins.odfpilotage.cost.entity.Effectives; 057import org.ametys.plugins.odfpilotage.cost.entity.EqTD; 058import org.ametys.plugins.odfpilotage.cost.entity.NormDetails; 059import org.ametys.plugins.odfpilotage.cost.entity.OverriddenData; 060import org.ametys.plugins.odfpilotage.cost.entity.VolumesOfHours; 061import org.ametys.plugins.repository.AmetysRepositoryException; 062import org.ametys.plugins.repository.UnknownAmetysObjectException; 063import org.ametys.runtime.model.ModelItem; 064 065import com.opensymphony.workflow.WorkflowException; 066 067/** 068 * This component handle the content of the cost modeling tool 069 */ 070public class CostComputationTreeHelper extends ODFContentsTreeHelper implements Contextualizable 071{ 072 private static int _ACTION = 2; 073 074 private static final String __COST_DATA_KEY = "cost-data-key"; 075 076 /** The cost computation component */ 077 protected CostComputationComponent _costComputationComponent; 078 079 /** Workflow helper component */ 080 protected ContentWorkflowHelper _contentWorkflowHelper; 081 082 /** The ODF enumeration helper */ 083 protected OdfReferenceTableHelper _refTableHelper; 084 085 /** The current user provider */ 086 protected CurrentUserProvider _currentUserProvider; 087 088 /** The context */ 089 protected Context _context; 090 091 @Override 092 public void contextualize(Context context) throws ContextException 093 { 094 _context = context; 095 } 096 097 @Override 098 public void service(ServiceManager smanager) throws ServiceException 099 { 100 super.service(smanager); 101 _costComputationComponent = (CostComputationComponent) smanager.lookup(CostComputationComponent.ROLE); 102 _refTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE); 103 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 104 } 105 106 /** 107 * Get the cost data from user session 108 * @return the cost data 109 */ 110 protected CostComputationData _getCostData() 111 { 112 Request request = ContextHelper.getRequest(_context); 113 Session session = request.getSession(); 114 115 CostComputationData costData = (CostComputationData) session.getAttribute(__COST_DATA_KEY); 116 if (costData == null) 117 { 118 throw new UnsupportedOperationException("Cost data is not initalized in session attribute '" + __COST_DATA_KEY + "'"); 119 } 120 121 return costData; 122 } 123 124 /** 125 * Set the cost data to user session 126 * @param costData the cost data 127 */ 128 protected void setCostData(CostComputationData costData) 129 { 130 Request request = ContextHelper.getRequest(_context); 131 Session session = request.getSession(); 132 133 session.setAttribute(__COST_DATA_KEY, costData); 134 } 135 136 /** 137 * Launch the cost computation component algorithm 138 * @param content the content to compute 139 * @param catalog the catalog 140 * @param lang the lang 141 * @param overriddenData overridden data by the user 142 */ 143 public void launchProgram(Content content, String catalog, String lang, OverriddenData overriddenData) 144 { 145 if (content instanceof OrgUnit) 146 { 147 setCostData(_costComputationComponent.computeCostsOnOrgUnits((OrgUnit) content, catalog, lang, overriddenData)); 148 } 149 else if (content instanceof Program) 150 { 151 setCostData(_costComputationComponent.computeCostsOnProgram((Program) content, overriddenData)); 152 } 153 } 154 155 /** 156 * Get the children contents according the tree configuration 157 * @param parentContent the root content 158 * @param treeConfiguration the tree configuration 159 * @param catalog the catalog 160 * @param lang the lang 161 * @return the children content for each child attributes 162 */ 163 public Map<String, List<Content>> getChildrenContent(Content parentContent, TreeConfiguration treeConfiguration, String catalog, String lang) 164 { 165 Map<String, List<Content>> children = new HashMap<>(); 166 if (parentContent instanceof OrgUnit) 167 { 168 OrgUnit orgUnit = (OrgUnit) parentContent; 169 List<Content> contents = orgUnit.getSubOrgUnits() 170 .stream() 171 .map(this::_resolveSilently) 172 .filter(Objects::nonNull) 173 .collect(Collectors.toList()); 174 if (orgUnit.getParentOrgUnit() == null && !contents.isEmpty()) 175 { 176 children.put("childOrgUnits", contents); 177 } 178 else 179 { 180 List<Program> programs = _odfHelper.getProgramsFromOrgUnit((OrgUnit) parentContent, catalog, lang); 181 contents = programs.stream() 182 .filter(Content.class::isInstance) 183 .map(Content.class::cast) 184 .collect(Collectors.toList()); 185 186 // _programsLink n'est pas un vrai attribut, cet identifiant n'est jamais utilisé 187 children.put("_programsLink", contents); 188 } 189 } 190 else 191 { 192 children = super.getChildrenContent(parentContent, treeConfiguration); 193 } 194 return children; 195 } 196 197 /** 198 * Get the children contents according the tree configuration 199 * @param contentId the parent content 200 * @param treeId the tree configuration 201 * @param contentPath the content path 202 * @param catalog the catalog 203 * @param lang the lang 204 * @return the children content 205 */ 206 @Callable 207 public Map<String, Object> getChildrenContent(String contentId, String treeId, String contentPath, String catalog, String lang) 208 { 209 TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId); 210 Content parentContent = _getParentContent(contentId); 211 Map<String, List<Content>> children = getChildrenContent(parentContent, treeConfiguration, catalog, lang); 212 213 Map<String, Object> infos = new HashMap<>(); 214 215 List<Map<String, Object>> childrenInfos = new ArrayList<>(); 216 infos.put("children", childrenInfos); 217 218 for (String attributePath : children.keySet()) 219 { 220 for (Content childContent : children.get(attributePath)) 221 { 222 Map<String, Object> childInfo = content2Json(childContent, contentPath + ModelItem.ITEM_PATH_SEPARATOR + childContent.getName()); 223 childInfo.put("metadataPath", attributePath); 224 225 if (!hasChildrenContent(childContent, treeConfiguration)) 226 { 227 childInfo.put("children", Collections.EMPTY_LIST); 228 } 229 230 childrenInfos.add(childInfo); 231 } 232 } 233 return infos; 234 } 235 236 /** 237 * Get the root node informations 238 * @param contentId The content 239 * @param catalog the catalog 240 * @param lang the lang 241 * @param overriddenData overridden data by the user 242 * @return The informations 243 */ 244 @Callable 245 public Map<String, Object> getRootNodeInformations(String contentId, String catalog, String lang, Map<String, Map<String, String>> overriddenData) 246 { 247 return getNodeInformations(contentId, catalog, lang, overriddenData); 248 } 249 250 /** 251 * Get the node informations 252 * @param contentId The content 253 * @param catalog the catalog 254 * @param lang the lang 255 * @param overriddenData Overridden data by the user 256 * @return The informations 257 */ 258 @Callable 259 public Map<String, Object> getNodeInformations(String contentId, String catalog, String lang, Map<String, Map<String, String>> overriddenData) 260 { 261 Content content = _ametysResolver.resolveById(contentId); 262 OverriddenData data = new OverriddenData(overriddenData); 263 launchProgram(content, catalog, lang, data); 264 Map<String, Object> json = content2Json(content, content.getName()); 265 if (content instanceof Program) 266 { 267 json.put("lang", content.getLanguage()); 268 json.put("catalog", ((Program) content).getCatalog()); 269 } 270 return json; 271 } 272 273 /** 274 * Get the default JSON representation of a content of the tree 275 * @param content the content 276 * @param contentPath the content path 277 * @return the content as JSON 278 */ 279 protected Map<String, Object> content2Json(Content content, String contentPath) 280 { 281 Map<String, Object> infos = super.content2Json(content); 282 283 CostComputationData costData = _getCostData(); 284 285 // If the content is a program then we can launch the algorithm 286 if (content instanceof OrgUnit) 287 { 288 _handleOrgUnit(content, infos, contentPath, costData); 289 } 290 else if (content instanceof Program) 291 { 292 _handleProgram(content, infos, contentPath, costData); 293 } 294 else if (content instanceof SubProgram) 295 { 296 _handleSubProgram(content, infos, contentPath, costData); 297 } 298 else if (content instanceof Container) 299 { 300 _handleContainer(content, infos, contentPath, costData); 301 } 302 else if (content instanceof Course) 303 { 304 _handleCourse(content, infos, contentPath, costData); 305 } 306 else if (content instanceof CoursePart) 307 { 308 _handleCoursePart(content, infos, contentPath, costData); 309 } 310 311 if (costData.getEffectiveByStep(content.getId()) != null) 312 { 313 Map<String, Double> parents = costData.getEffectiveByStep(content.getId()).entrySet() 314 .stream() 315 .collect( 316 Collectors.toMap( 317 e -> _getTitle(e.getKey().getId()), 318 Map.Entry::getValue, 319 (title1, title2) -> title1 320 ) 321 ); 322 323 infos.put("parents", parents); 324 } 325 return infos; 326 } 327 328 private String _getTitle(String id) 329 { 330 return ((Container) _ametysResolver.resolveById(id)).getTitle(); 331 } 332 333 /** 334 * Retrieve hourly volume of a content and add it to the JSON structure 335 * @param json the structure 336 * @param content the content 337 * @param costData the cost data 338 */ 339 protected void _addVolumeHoraire(Map<String, Object> json, Content content, CostComputationData costData) 340 { 341 VolumesOfHours volumeHoraire = costData.getVolumesOfHours(content.getId()); 342 343 for (String natureId : volumeHoraire.getVolumes().keySet()) 344 { 345 Map<String, Object> data = new HashMap<>(); 346 if (content instanceof CoursePart) 347 { 348 data.put("volHorCoursePart", volumeHoraire.getVolumes().get(natureId)); 349 } 350 else 351 { 352 data.put("volHorProgramItem", volumeHoraire.getVolumes().get(natureId)); 353 } 354 json.put(natureId, data); 355 } 356 357 } 358 359 /** 360 * Retrieve the number of groups and add it to the JSON structure 361 * @param json the JSON structure 362 * @param groupeAOuvrir groups to open 363 * @param groupeCalcule calculated groups 364 */ 365 protected void _addGroupe(Map<String, Object> json, Long groupeAOuvrir, Long groupeCalcule) 366 { 367 Map<String, Object> data = new HashMap<>(); 368 if (groupeAOuvrir > 0) 369 { 370 data.put("groupeAOuvrir", groupeAOuvrir); 371 json.put("groupeCalcule", groupeCalcule); 372 } 373 else 374 { 375 data.put("groupeCalcule", groupeCalcule); 376 } 377 json.put("groupe", data); 378 } 379 380 /** 381 * Retrieve the effective and add it to the JSON structure 382 * @param json the JSON structure 383 * @param content the content 384 * @param contentPath the content path 385 * @param costData the cost data 386 */ 387 protected void _addEffectif(Map<String, Object> json, Content content, String contentPath, CostComputationData costData) 388 { 389 Map<String, Object> data = new HashMap<>(); 390 Effectives effectives = costData.getEffective(content.getId()); 391 392 if (effectives.getEnteredEffective().isPresent()) 393 { 394 data.put("effectifPrev", Math.round(effectives.getEnteredEffective().get())); 395 } 396 else 397 { 398 data.put("effectifCalc", Math.round(effectives.getComputedGlobalEffective())); 399 } 400 401 json.put("effectifGlobal", data); 402 json.put("effectifLocal", Math.round(effectives.getComputedLocalEffective(contentPath))); 403 404 } 405 406 /** 407 * Retrieve the TD equivalent value and add it to the JSON structure 408 * @param json the JSON structure 409 * @param content the content 410 * @param contentPath the content path 411 * @param costData the cost data 412 */ 413 protected void _addEqTD(Map<String, Object> json, Content content, String contentPath, CostComputationData costData) 414 { 415 EqTD eqTD = costData.getEqTD(content.getId()); 416 417 json.put("eqtdLocal", eqTD.getLocalEqTD(contentPath)); 418 json.put("eqtdProratise", eqTD.getProRatedEqTD(contentPath)); 419 if (eqTD.getGlobalEqTD() != 0d) 420 { 421 json.put("eqtdGlobal", eqTD.getGlobalEqTD()); 422 423 } 424 } 425 426 /** 427 * Compute the H/E report and add it to the JSON structure 428 * @param json the JSON structure 429 * @param content the content 430 * @param contentPath the content path 431 * @param costData the cost data 432 */ 433 protected void _addRapportHE(Map<String, Object> json, Content content, String contentPath, CostComputationData costData) 434 { 435 json.put("rapp", costData.getHeReport(contentPath)); 436 } 437 438 439 /** 440 * Handle coursePart informations 441 * @param content the content 442 * @param json the JSON structure 443 * @param contentPath the content path 444 * @param costData the cost data 445 */ 446 protected void _handleCoursePart(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 447 { 448 CoursePartCostData coursePartCostData = costData.getCoursePartCostData().get(content); 449 450 if (coursePartCostData != null) 451 { 452 _addEffectif(json, content, contentPath, costData); 453 _addGroupe(json, coursePartCostData.getGroupsToOpen(), coursePartCostData.getCalculatedGroups()); 454 _handleODFElement(content, json, contentPath, costData); 455 _addNorm(json, coursePartCostData.getNormDetails()); 456 } 457 } 458 459 /** 460 * Add norm informations 461 * @param json the JSON structure 462 * @param normDetails the norm details 463 */ 464 protected void _addNorm(Map<String, Object> json, NormDetails normDetails) 465 { 466 Map<String, Object> data = new HashMap<>(); 467 data.put("effMax", normDetails.getEffectiveMax()); 468 data.put("effMin", normDetails.getEffectiveMinSup()); 469 json.put("norme", data); 470 } 471 472 /** 473 * Handle sub program informations 474 * @param content the content 475 * @param json the JSON structure 476 * @param contentPath the current path 477 * @param costData the cost data 478 */ 479 protected void _handleSubProgram(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 480 { 481 _addEffectif(json, content, contentPath, costData); 482 _handleODFElement(content, json, contentPath, costData); 483 } 484 485 /** 486 * Handle program informations 487 * @param content the content 488 * @param json the JSON structure 489 * @param contentPath the current path 490 * @param costData the cost data 491 */ 492 protected void _handleProgram(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 493 { 494 _addEffectif(json, content, contentPath, costData); 495 _handleODFElement(content, json, contentPath, costData); 496 } 497 498 /** 499 * Handle orgUnit informations 500 * @param content the content 501 * @param json the JSON structure 502 * @param contentPath the current path 503 * @param costData the cost data 504 */ 505 protected void _handleOrgUnit(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 506 { 507 _addEffectif(json, content, contentPath, costData); 508 _handleODFElement(content, json, contentPath, costData); 509 } 510 511 private void _handleCourse(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 512 { 513 _addEffectif(json, content, contentPath, costData); 514 _handleODFElement(content, json, contentPath, costData); 515 } 516 517 /** 518 * Fill data in JSON describing the year container 519 * @param content the year container 520 * @param json the structure 521 * @param contentPath the content path 522 * @param costData the cost data 523 */ 524 protected void _handleContainer(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 525 { 526 Map<String, Object> data = new HashMap<>(); 527 Optional<Double> effectif = costData.getGlobalComputedEffective(content.getId()); 528 529 if (effectif.isPresent()) 530 { 531 if (_refTableHelper.getItemCode(((Container) content).getNature()).equals("annee")) 532 { 533 data.put("effectifYear", effectif.get()); 534 json.put("effectifGlobal", data); 535 json.put("effectifLocal", data); 536 } 537 else 538 { 539 _addEffectif(json, content, contentPath, costData); 540 } 541 _handleODFElement(content, json, contentPath, costData); 542 } 543 } 544 545 /** 546 * Fill data in JSON describing an ODF Element 547 * @param content the year container 548 * @param json the structure 549 * @param contentPath the content path 550 * @param costData the cost data 551 */ 552 protected void _handleODFElement(Content content, Map<String, Object> json, String contentPath, CostComputationData costData) 553 { 554 _addVolumeHoraire(json, content, costData); 555 _addEqTD(json, content, contentPath, costData); 556 _addRapportHE(json, content, contentPath, costData); 557 } 558 559 private Content _resolveSilently(String contentId) 560 { 561 try 562 { 563 return _ametysResolver.resolveById(contentId); 564 } 565 catch (UnknownAmetysObjectException e) 566 { 567 return null; 568 } 569 } 570 571 /** 572 * Launch the cost computation component algorithm with overridden data by the user 573 * @param contentsToRefresh all open contents in the tool to refresh 574 * @param contentId the root node 575 * @param catalog the catalog 576 * @param lang the lang 577 * @param overridenData overridden data by the user 578 * @return new values associated with their path 579 */ 580 @Callable 581 public Map<String, Map<String, Object>> refresh(Map<String, String> contentsToRefresh, String contentId, String catalog, String lang, Map<String, Map<String, String>> overridenData) 582 { 583 Map<String, Map<String, Object>> refreshedData = new HashMap<>(); 584 Content content = _ametysResolver.resolveById(contentId); 585 OverriddenData data = new OverriddenData(overridenData); 586 587 launchProgram(content, catalog, lang, data); 588 for (Entry<String, String> entry : contentsToRefresh.entrySet()) 589 { 590 Content item = _ametysResolver.resolveById(entry.getValue()); 591 refreshedData.put(entry.getKey(), content2Json(item, entry.getKey())); 592 } 593 return refreshedData; 594 } 595 596 /** 597 * Retrieve old data to compare with the new computed one 598 * @return all old data to compare 599 */ 600 @Callable 601 public Map<String, Map<String, Double>> getOldData() 602 { 603 CostComputationData costData = _getCostData(); 604 605 Map<String, Map<String, Double>> data = new HashMap<>(); 606 data.put("heReport", costData.getHeReport()); 607 data.put("eqtdPorte", costData.getEqTDPorte()); 608 data.put("eqTDproratise", costData.getEqTDProratise()); 609 data.put("eqTDglobal", costData.getEqTDGlobal()); 610 611 return data; 612 } 613 614 /** 615 * Save overridden data to the contents 616 * @param overriddenData overridden data by the user 617 * @return true if the saving is successful 618 * @throws WorkflowException if an error occurred 619 * @throws AmetysRepositoryException if an error occurred 620 */ 621 @Callable 622 public Boolean saveOverriddenData(Map<String, Map<String, String>> overriddenData) throws AmetysRepositoryException, WorkflowException 623 { 624 Boolean hasChanged = false; 625 for (Entry<String, Map<String, String>> data : overriddenData.entrySet()) 626 { 627 ModifiableDefaultContent content = _ametysResolver.resolveById(data.getKey()); 628 if (data.getValue().containsKey("effectifLocal") && content instanceof Container) 629 { 630 content.setValue("numberOfStudentsEstimated", Long.parseLong(data.getValue().get("effectifLocal"))); 631 hasChanged = true; 632 } 633 if (data.getValue().containsKey("groupe") && content instanceof CoursePart) 634 { 635 content.setValue("groupsToOpen", Long.parseLong(data.getValue().get("groupe"))); 636 hasChanged = true; 637 } 638 if (content instanceof CoursePart && _containsTeachingNature(data.getValue().keySet())) 639 { 640 for (String nbHours: data.getValue().values()) 641 { 642 content.setValue("nbHours", Double.parseDouble(nbHours)); 643 hasChanged = true; 644 } 645 } 646 if (hasChanged) 647 { 648 _contentWorkflowHelper.doAction(content, _ACTION); 649 } 650 } 651 return hasChanged; 652 } 653 654 private Boolean _containsTeachingNature(Set<String> overriddenType) 655 { 656 List<String> teachingNature = _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE).stream() 657 .map(t -> t.getCode()) 658 .collect(Collectors.toList()); 659 660 return overriddenType.stream() 661 .anyMatch(o -> teachingNature.contains(o)); 662 } 663}