001/* 002 * Copyright 2010 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.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.Set; 027import java.util.function.Predicate; 028import java.util.stream.Collectors; 029 030import javax.jcr.Node; 031 032import org.apache.commons.lang3.ArrayUtils; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.data.ContentDataHelper; 036import org.ametys.cms.data.ContentValue; 037import org.ametys.cms.data.RichText; 038import org.ametys.cms.data.holder.group.ModifiableIndexableRepeater; 039import org.ametys.cms.data.holder.group.ModifiableIndexableRepeaterEntry; 040import org.ametys.cms.repository.ModifiableDefaultContent; 041import org.ametys.odf.ProgramItem; 042import org.ametys.odf.content.code.DisplayCodeProperty; 043import org.ametys.odf.courselist.CourseList; 044import org.ametys.odf.courselist.CourseListContainer; 045import org.ametys.odf.coursepart.CoursePart; 046import org.ametys.odf.data.EducationalPath; 047import org.ametys.plugins.repository.AmetysRepositoryException; 048import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 049import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 050import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater; 051import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 052import org.ametys.runtime.model.exception.UndefinedItemPathException; 053 054/** 055 * Class representing a {@link Course} 056 */ 057public class Course extends ModifiableDefaultContent<CourseFactory> implements CourseListContainer, ProgramItem 058{ 059 /** Name of attribute for parent course lists */ 060 public static final String PARENT_COURSE_LISTS = "parentCourseLists"; 061 062 /** Name of attribute for parent course lists */ 063 public static final String CHILD_COURSE_LISTS = "courseLists"; 064 065 /** Mandatory Identifier to generate the CDM-fr id */ 066 public static final String CDM_CODE = "cdmCode"; 067 068 /** Constants for ects attribute */ 069 public static final String ECTS = "ects"; 070 071 /** Constants for path of ects attribute by education path */ 072 public static final String ECTS_BY_PATH = "ectsByEducationalPath/ects"; 073 074 /** Constants for level attribute */ 075 public static final String LEVEL = "level"; 076 077 /** Constants for description attribute */ 078 public static final String DESCRIPTION = "description"; 079 080 /** Constants for objectives attribute */ 081 public static final String OBJECTIVES = "objectives"; 082 083 /** Constants for nbHours attribute */ 084 public static final String NUMBER_OF_HOURS = "nbHours"; 085 086 /** Constants for neededPrerequisite attribute */ 087 public static final String NEEDED_PREREQUISITE = "neededPrerequisite"; 088 089 /** Constants for teds attribute */ 090 public static final String TEDS = "teds"; 091 092 /** Constants for formOfAssessment attribute */ 093 public static final String FORM_OF_ASSESSMENT = "formOfAssessment"; 094 095 /** Constants for repeater 'acquiredMicroSkills' activated with the configuration parameter 'odf.skills.enabled' */ 096 public static final String ACQUIRED_MICRO_SKILLS = "acquiredMicroSkills"; 097 /** The attribute name of the program in acquiredMicroSkills activated with the configuration parameter 'odf.skills.enabled' */ 098 public static final String ACQUIRED_MICRO_SKILLS_PROGRAM = "program"; 099 /** The attribute name of the microSkills in acquiredMicroSkills activated with the configuration parameter 'odf.skills.enabled' */ 100 public static final String ACQUIRED_MICRO_SKILLS_SKILLS = "microSkills"; 101 102 /** Constants for syllabus attribute */ 103 public static final String SYLLABUS = "syllabus"; 104 105 /** Constants for additionalInformations attribute */ 106 public static final String ADDITIONAL_INFORMATIONS = "additionalInformations"; 107 108 /** Constants for erasmusCode attribute */ 109 public static final String ERASMUS_CODE = "erasmusCode"; 110 111 /** Constants for teachingLocation attribute */ 112 public static final String TEACHING_LOCATION = "teachingLocation"; 113 114 /** Constants for maxNumberOfStudents attribute */ 115 public static final String MAX_NUMBER_OF_STUDENTS = "maxNumberOfStudents"; 116 117 /** Constants for teachingTerm attribute */ 118 public static final String TEACHING_TERM = "teachingTerm"; 119 120 /** Constants for timeSlot attribute */ 121 public static final String TIME_SLOT = "timeSlot"; 122 123 /** Constants for trainingCourseDuration attribute */ 124 public static final String TRAINING_COURSE_DURATION = "trainingCourseDuration"; 125 126 /** Constants for teachingMethod attribute */ 127 public static final String FORMODFTEACHING_METHOD = "formofteachingMethod"; 128 129 /** Constants for formofteachingOrg attribute */ 130 public static final String FORMOFTEACHING_ORG = "formofteachingOrg"; 131 132 /** Constants for formOfTeaching attribute */ 133 public static final String TEACHING_ACTIVITY = "teachingActivity"; 134 135 /** Constants for teachingLanguage attribute */ 136 public static final String TEACHING_LANGUAGE = "teachingLanguage"; 137 138 /** Constants for startDate attribute */ 139 public static final String START_DATE = "startDate"; 140 141 /** Constants for keywords attribute */ 142 public static final String KEYWORDS = "keywords"; 143 144 /** Constants for webLinkLabel attribute */ 145 public static final String WEB_LINK_LABEL = "webLinkLabel"; 146 147 /** Constants for webLinkUrl attribute */ 148 public static final String WEB_LINK_URL = "webLinkUrl"; 149 150 /** Constants for lomSheets attribute */ 151 public static final String LOM_SHEETS = "lomSheets"; 152 153 /** Constants for LOM Sheet URL attribute */ 154 public static final String LOM_SHEET_URL = "linkUrl"; 155 156 /** Constants for LOM Sheet label attribute */ 157 public static final String LOM_SHEET_LABEL = "linkLabel"; 158 159 /** Constants for attribute 'contacts' */ 160 public static final String CONTACTS = "contacts"; 161 162 /** Constants for attribute 'contacts/role' */ 163 public static final String CONTACTS_ROLE = "role"; 164 165 /** Constants for attribute 'contacts/persons' */ 166 public static final String CONTACTS_PERSONS = "persons"; 167 168 /** Constants for attribute 'courseType' */ 169 public static final String COURSE_TYPE = "courseType"; 170 171 /** Constants for attribute 'bibliography' */ 172 public static final String BIBLIOGRAPHY = "bibliography"; 173 174 /** Constants for attribute 'skills' */ 175 public static final String SKILLS = "skills"; 176 177 /** Constants for attribute 'openToExchangeStudents' */ 178 public static final String OPEN_TO_EXCHANGE_STUDENTS = "openToExchangeStudents"; 179 180 /** Constants for attribute 'courseParts' */ 181 public static final String CHILD_COURSE_PARTS = "courseParts"; 182 183 private String _contextPath; 184 185 private List<EducationalPath> _currentEducationalPath; 186 187 /** 188 * Constructor. 189 * @param node the JCR Node. 190 * @param parentPath the parent path 191 * @param factory the corresponding factory. 192 */ 193 public Course(Node node, String parentPath, CourseFactory factory) 194 { 195 super(node, parentPath, factory); 196 } 197 198 @Override 199 public List<CourseList> getCourseLists() 200 { 201 return Arrays.stream(getValue(CHILD_COURSE_LISTS, false, new ContentValue[0])) 202 .map(ContentValue::getContentIfExists) 203 .flatMap(Optional::stream) 204 .map(CourseList.class::cast) 205 .collect(Collectors.toList()); 206 } 207 208 /** 209 * Get the parent course lists 210 * @return The parent course lists 211 */ 212 public List<CourseList> getParentCourseLists() 213 { 214 return Arrays.stream(getValue(PARENT_COURSE_LISTS, false, new ContentValue[0])) 215 .map(ContentValue::getContentIfExists) 216 .flatMap(Optional::stream) 217 .map(CourseList.class::cast) 218 .collect(Collectors.toList()); 219 } 220 221 @Override 222 public boolean hasCourseLists() 223 { 224 return !ContentDataHelper.isMultipleContentDataEmpty(this, CHILD_COURSE_LISTS); 225 } 226 227 @Override 228 public boolean containsCourseList(String clId) 229 { 230 return ArrayUtils.contains(ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, CHILD_COURSE_LISTS), clId); 231 } 232 233 // --------------------------------------------------------------------------------------// 234 // CONTACTS 235 // --------------------------------------------------------------------------------------// 236 237 /** 238 * Return the list of Persons in charge binded to this program 239 * @return the id of contacts 240 */ 241 public Set<String> getContacts() 242 { 243 Set<String> contactIds = new HashSet<>(); 244 245 ModelAwareRepeater contacts = getRepeater(CONTACTS); 246 if (contacts != null) 247 { 248 for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries()) 249 { 250 // Remove empty values 251 contactIds.addAll(ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS) 252 .filter(contentId -> !contentId.isEmpty()) 253 .collect(Collectors.toSet())); 254 } 255 } 256 257 return Collections.EMPTY_SET; 258 } 259 260 /** 261 * Return the list of Persons in charge binded to this program 262 * @return a list of roles and UUID 263 */ 264 public Map<String, List<String>> getContactsByRole() 265 { 266 Map<String, List<String>> contactsByRole = new HashMap<>(); 267 268 ModelAwareRepeater contacts = getRepeater(CONTACTS); 269 if (contacts != null) 270 { 271 for (ModelAwareRepeaterEntry contactEntry : contacts.getEntries()) 272 { 273 // Remove empty values 274 List<String> persons = ContentDataHelper.getContentIdsStreamFromMultipleContentData(contactEntry, CONTACTS_PERSONS) 275 .filter(StringUtils::isNotEmpty) 276 .collect(Collectors.toList()); 277 if (!persons.isEmpty()) 278 { 279 String role = ContentDataHelper.getContentIdFromContentData(contactEntry, CONTACTS_ROLE); 280 contactsByRole.put(role, persons); 281 } 282 } 283 } 284 285 return contactsByRole; 286 } 287 288 289 // --------------------------------------------------------------------------------------// 290 // CONTEXT 291 // --------------------------------------------------------------------------------------// 292 293 // Méthodes utilisées lors du parcours d'une maquette uniquement, afin de contextualiser l'élément 294 // A ne pas utiliser n'importe ou ni n'importe comment 295 296 /** 297 * Set the parent path for links and breadcrumb 298 * @param path the parent path 299 */ 300 public void setContextPath (String path) 301 { 302 _contextPath = path; 303 } 304 305 /** 306 * Get the parent path. Can be null. 307 * @return the parent path 308 */ 309 public String getContextPath () 310 { 311 return _contextPath; 312 } 313 314 /** 315 * Set the current educational paths of this course 316 * @param paths the current educational paths 317 */ 318 public void setCurrentEducationalPaths(List<EducationalPath> paths) 319 { 320 _currentEducationalPath = paths; 321 } 322 323 /** 324 * Get the current educational paths of this course 325 * @return the current educational path 326 */ 327 public List<EducationalPath> getCurrentEducationalPaths() 328 { 329 return _currentEducationalPath; 330 } 331 332 333 // --------------------------------------------------------------------------------------// 334 // GETTERS AND SETTERS 335 // --------------------------------------------------------------------------------------// 336 @Override 337 public String getCatalog() 338 { 339 return getValue(CATALOG); 340 } 341 342 @Override 343 public void setCatalog(String catalog) throws AmetysRepositoryException 344 { 345 setValue(CATALOG, catalog); 346 } 347 348 @Override 349 public String getCode() 350 { 351 return getValue(CODE, false, StringUtils.EMPTY); 352 } 353 354 @Override 355 public void setCode(String code) throws AmetysRepositoryException 356 { 357 setValue(CODE, code); 358 } 359 360 public String getDisplayCode() 361 { 362 return getValue(DisplayCodeProperty.PROPERTY_NAME, false, StringUtils.EMPTY); 363 } 364 365 public boolean isPublishable() 366 { 367 return getInternalDataHolder().getValue(PUBLISHABLE, true); 368 } 369 370 public void setPublishable(boolean isPublishable) 371 { 372 getInternalDataHolder().setValue(PUBLISHABLE, isPublishable); 373 } 374 375 public List<String> getOrgUnits() 376 { 377 try 378 { 379 return ContentDataHelper.getContentIdsListFromMultipleContentData(this, ORG_UNITS_REFERENCES); 380 } 381 catch (UndefinedItemPathException e) 382 { 383 return Collections.EMPTY_LIST; // this attribute is not part of model 384 } 385 } 386 387 /** 388 * Get the description 389 * @return the description or null 390 */ 391 public RichText getDescription() 392 { 393 return getValue(DESCRIPTION); 394 } 395 396 /** 397 * Get the objectives 398 * @return objectives 399 */ 400 public RichText getObjectives() 401 { 402 return getValue(OBJECTIVES); 403 } 404 405 /** 406 * Get the course parts. 407 * @return The {@link List} of attached {@link CoursePart}s 408 */ 409 public List<CoursePart> getCourseParts() 410 { 411 return Arrays.stream(getValue(CHILD_COURSE_PARTS, false, new ContentValue[0])) 412 .map(ContentValue::getContentIfExists) 413 .flatMap(Optional::stream) 414 .map(CoursePart.class::cast) 415 .collect(Collectors.toList()); 416 } 417 418 /** 419 * Get the needed prerequisites 420 * @return the needed prerequisites or null if not set 421 */ 422 public RichText getNeededPrerequisite() 423 { 424 return getValue(NEEDED_PREREQUISITE); 425 } 426 427 /** 428 * Get the formOfAssessment attribute 429 * @return the formOfAssessment or null if not set 430 */ 431 public RichText getFormOfAssessment() 432 { 433 return getValue(FORM_OF_ASSESSMENT); 434 } 435 436 /** 437 * Get the syllabus 438 * @return the syllabus or null if not set 439 */ 440 public RichText getSyllabus() 441 { 442 return getValue(SYLLABUS); 443 } 444 445 /** 446 * Get the list of LOM sheets 447 * @return the list of LOMsheets or an empty list 448 */ 449 public Set<LOMSheet> getLOMSheets() 450 { 451 Set<LOMSheet> lomSheets = new HashSet<>(); 452 453 ModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS); 454 if (attributeLOMSheets != null) 455 { 456 for (ModelAwareRepeaterEntry attributeLOMSheet : attributeLOMSheets.getEntries()) 457 { 458 LOMSheet lomSheet = new LOMSheet(attributeLOMSheet.getValue(LOM_SHEET_URL, false, StringUtils.EMPTY), attributeLOMSheet.getValue(LOM_SHEET_LABEL, false, StringUtils.EMPTY)); 459 lomSheets.add(lomSheet); 460 } 461 } 462 463 return lomSheets; 464 } 465 466 /** 467 * Set the LOM sheets 468 * @param sheets The LOM sheets 469 */ 470 public void setLOMSheets (Set<LOMSheet> sheets) 471 { 472 // Remove old LOM sheets if exist 473 removeValue(LOM_SHEETS); 474 475 ModifiableModelAwareRepeater attributeLOMSheets = getRepeater(LOM_SHEETS, true); 476 for (LOMSheet lomSheet : sheets) 477 { 478 ModifiableModelAwareRepeaterEntry attributeLOMSheet = attributeLOMSheets.addEntry(); 479 attributeLOMSheet.setValue(LOM_SHEET_URL, lomSheet.getUrl()); 480 attributeLOMSheet.setValue(LOM_SHEET_LABEL, lomSheet.getLabel()); 481 } 482 } 483 484 /** 485 * Determines if the course has LOM sheet 486 * @param lomSheet The LOM sheet to test 487 * @return <code>true</code> if the course has LOM sheet 488 */ 489 public boolean hasLOMSheet (LOMSheet lomSheet) 490 { 491 for (LOMSheet lom : getLOMSheets()) 492 { 493 if (lom.getUrl().equals(lomSheet.getUrl()) && lom.getLabel().equals(lomSheet.getLabel())) 494 { 495 return true; 496 } 497 } 498 499 return false; 500 } 501 502 /** 503 * Get the additional information 504 * @return the additional information 505 */ 506 public RichText getAdditionalInformations() 507 { 508 return getValue(ADDITIONAL_INFORMATIONS); 509 } 510 511 /** 512 * Get the Erasmus code 513 * @return the Erasmus code 514 */ 515 public String getErasmusCode() 516 { 517 return ContentDataHelper.getContentIdFromContentData(this, ERASMUS_CODE); 518 } 519 520 /** 521 * Get the teaching location 522 * @return the teaching location 523 */ 524 public String[] getTeachingLocation() 525 { 526 return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LOCATION); 527 } 528 529 /** 530 * Set the teaching location 531 * @param teachingLocation the teaching location to set 532 * @throws AmetysRepositoryException if failed to set attribute 533 */ 534 public void setTeachingLocation(String[] teachingLocation) throws AmetysRepositoryException 535 { 536 setValue(Course.TEACHING_LOCATION, teachingLocation); 537 } 538 539 /** 540 * Get the ECTS 541 * @return the ECTS 542 */ 543 public double getEcts() 544 { 545 return getValue(ECTS, false, 0D); 546 } 547 548 549 /** 550 * Get the ECTS for a given educational path 551 * @param ctxEducationalPath The educational path 552 * @return the ECTS for this educational path 553 */ 554 public double getEcts(EducationalPath ctxEducationalPath) 555 { 556 return getEcts( 557 Optional.ofNullable(ctxEducationalPath) 558 .map(List::of) 559 .orElseGet(List::of) 560 ); 561 } 562 563 /** 564 * Get the ECTS for given educational paths 565 * @param ctxEducationalPaths The educational paths 566 * @return the ECTS for this educational path 567 */ 568 public double getEcts(List<EducationalPath> ctxEducationalPaths) 569 { 570 return Optional.ofNullable(ctxEducationalPaths) 571 .filter(Predicate.not(List::isEmpty)) 572 .flatMap(paths -> _getFactory()._getODFHelper().<Double>getValueForPath(this, ECTS_BY_PATH, paths)) 573 .orElseGet(this::getEcts); 574 } 575 576 /** 577 * Get the number of hours 578 * @return the number of hours 579 */ 580 public double getNumberOfHours() 581 { 582 return getValue(NUMBER_OF_HOURS, false, 0D); 583 } 584 585 /** 586 * Get the effectives 587 * @return the effectives 588 * @throws AmetysRepositoryException if failed to get attribute 589 */ 590 public String getMaxNumberOfStudents() 591 { 592 return getValue(MAX_NUMBER_OF_STUDENTS, false, StringUtils.EMPTY); 593 } 594 595 /** 596 * Get the teaching term 597 * @return the teaching term 598 */ 599 public String getTeachingTerm() 600 { 601 return ContentDataHelper.getContentIdFromContentData(this, TEACHING_TERM); 602 } 603 604 /** 605 * Get the level 606 * @return the level 607 */ 608 public String getLevel() 609 { 610 return ContentDataHelper.getContentIdFromContentData(this, LEVEL); 611 } 612 613 /** 614 * Get the teaching method 615 * @return the teaching method 616 */ 617 public String getFormOfTeachingMethod() 618 { 619 return ContentDataHelper.getContentIdFromContentData(this, FORMODFTEACHING_METHOD); 620 } 621 622 /** 623 * Get the teaching organizations 624 * @return the teaching organizations 625 */ 626 public String[] getFormOfTeachingOrgs() 627 { 628 return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, FORMOFTEACHING_ORG); 629 } 630 631 /** 632 * Get the teaching activity 633 * @return the teaching activity 634 */ 635 public String getTeachingActivity() 636 { 637 return ContentDataHelper.getContentIdFromContentData(this, TEACHING_ACTIVITY); 638 } 639 640 /** 641 * Get the teaching language 642 * @return the teaching language 643 */ 644 public String[] getTeachingLanguage() 645 { 646 return ContentDataHelper.getContentIdsArrayFromMultipleContentData(this, TEACHING_LANGUAGE); 647 } 648 649 /** 650 * Get the start date 651 * @return the start date 652 */ 653 public LocalDate getStartDate() 654 { 655 return getValue(START_DATE); 656 } 657 658 /** 659 * Get the time slot 660 * @return the time slot 661 */ 662 public String getTimeSlot() 663 { 664 return ContentDataHelper.getContentIdFromContentData(this, TIME_SLOT); 665 } 666 667 /** 668 * Get the keywords 669 * @return the keywords 670 */ 671 public String[] getKeywords() 672 { 673 return getValue(KEYWORDS, false, ArrayUtils.EMPTY_STRING_ARRAY); 674 } 675 676 /** 677 * Get the web link label 678 * @return the web link label 679 */ 680 public String getWebLinkLabel() 681 { 682 return getValue(WEB_LINK_LABEL, false, StringUtils.EMPTY); 683 } 684 685 /** 686 * Get the web link URL 687 * @return the web link URL 688 */ 689 public String getWebLinkUrl() 690 { 691 return getValue(WEB_LINK_URL, false, StringUtils.EMPTY); 692 } 693 694 /** 695 * Get the course type (nature) 696 * @return the course type 697 */ 698 public String getCourseType() 699 { 700 return ContentDataHelper.getContentIdFromContentData(this, COURSE_TYPE); 701 } 702 703 /** 704 * Get the bibliography 705 * @return the bibliography 706 */ 707 public RichText getBibliography() 708 { 709 return getValue(BIBLIOGRAPHY); 710 } 711 712 /** 713 * Get the skills 714 * @return the skills 715 */ 716 public RichText getSkills() 717 { 718 return getValue(SKILLS); 719 } 720 721 /** 722 * Is open to exchange students 723 * @return <code>true</code> if the course is open to exchange students 724 */ 725 public boolean isOpenToExchangeStudents() 726 { 727 return getValue(OPEN_TO_EXCHANGE_STUDENTS, false, false); 728 } 729 730 // --------------------------------------------------------------------------------------// 731 // CDM-fr 732 // --------------------------------------------------------------------------------------// 733 @Override 734 public String getCDMId() 735 { 736 return "FRUAI" + _getFactory()._getRootOrgUnitRNE() + "CO" + getCode(); 737 } 738 739 @Override 740 public String getCdmCode() 741 { 742 return getValue(CDM_CODE, false, StringUtils.EMPTY); 743 } 744 745 @Override 746 public void setCdmCode(String cdmCode) 747 { 748 setValue(CDM_CODE, cdmCode); 749 } 750 751 /** 752 * Get the acquired skills for a given program 753 * @param programId the Id of the program 754 * @return The list of acquired skills for the program, or an empty array if there are no skills 755 */ 756 public ContentValue[] getAcquiredSkills(String programId) 757 { 758 ModifiableIndexableRepeater repeater = getRepeater(ACQUIRED_MICRO_SKILLS); 759 760 if (repeater != null) 761 { 762 ModifiableIndexableRepeaterEntry entryForProgram = repeater.getEntries().stream() 763 .filter(entry -> entry.hasValue(ACQUIRED_MICRO_SKILLS_PROGRAM) && programId.equals(((ContentValue) entry.getValue(ACQUIRED_MICRO_SKILLS_PROGRAM, false, null)).getContentId())) 764 .findFirst() 765 .orElse(null); 766 767 return entryForProgram.getValue(ACQUIRED_MICRO_SKILLS_SKILLS, true, new ContentValue[0]); 768 } 769 770 return new ContentValue[0]; 771 } 772}