def _get_missing_options(self): """ In context of MA/MD/MS when we add an option [or group which contains options], this options must exist in parent context (2m) """ options_missing_by_finality = {} options_to_add = EducationGroupHierarchy( root=self.child).get_option_list() if self.child.education_group_type.name == MiniTrainingType.OPTION.name: options_to_add += [self.child] finalities_qs = self.parents | EducationGroupYear.objects.filter( pk=self.parent.pk) finalities_pks = finalities_qs.filter( education_group_type__name__in=TrainingType.finality_types( )).values_list('pk', flat=True) if self.child.education_group_type.name in TrainingType.finality_types( ): finalities_pks = list(finalities_pks) + [self.parent.pk] if finalities_pks: root_2m_qs = EducationGroupYear.hierarchy.filter( pk__in=finalities_pks).get_parents().filter( education_group_type__name__in=TrainingType. root_master_2m_types()) for root in root_2m_qs: options_in_2m = EducationGroupHierarchy( root=root).get_option_list() options_missing_by_finality[root] = set(options_to_add) - set( options_in_2m) return options_missing_by_finality
def is_valid(self): if self.parent.education_group_type.name in TrainingType.root_master_2m_types() or \ self.parents.filter(education_group_type__name__in=TrainingType.root_master_2m_types()).exists(): self._check_end_year_constraints_on_2m() self._check_attach_options_rules() if not self.instance: self._check_new_attach_is_not_duplication() return True
def _check_end_year_constraints_on_2m(self): qs = self.educationgroupyear_set.all().filter( education_group_type__name__in=TrainingType.finality_types() + TrainingType.root_master_2m_types() ).select_related('education_group_type') for education_group_year in qs: if education_group_year.type in TrainingType.finality_types(): self._check_end_year_finality_are_in_range_of_root_2m(education_group_year) elif education_group_year.type in TrainingType.root_master_2m_types() and self.end_year is not None: self._check_end_year_root_2m_cover_all_finalities(education_group_year)
def _check_end_year_constraints_on_2m(self): qs = self.educationgroupyear_set.all().filter( education_group_type__name__in=TrainingType.finality_types() + TrainingType.root_master_2m_types() ).select_related('education_group_type') for education_group_year in qs: if education_group_year.type in TrainingType.finality_types(): self._check_end_year_finality_are_in_range_of_root_2m(education_group_year) elif education_group_year.type in TrainingType.root_master_2m_types() and self.end_year is not None: self._check_end_year_root_2m_cover_all_finalities(education_group_year)
def create_partial_title(apps, schema_editor): education_group_year_mdl = apps.get_model("base", "EducationGroupYear") education_group_years = education_group_year_mdl.objects.filter( academic_year__year__gte=2019, education_group_type__name__in=TrainingType.finality_types()) for education_group_year in education_group_years: if education_group_year.education_group_type.name in [ TrainingType.MASTER_MA_120.name, TrainingType.MASTER_MA_180_240.name ]: education_group_year.partial_title = "Finalité approfondie" education_group_year.partial_title_english = "Research Focus" if education_group_year.education_group_type.name in [ TrainingType.MASTER_MD_120.name, TrainingType.MASTER_MD_180_240.name ]: education_group_year.partial_title = "Finalité didactique" education_group_year.partial_title_english = "Teaching Focus" if education_group_year.education_group_type.name in [ TrainingType.MASTER_MS_120.name, TrainingType.MASTER_MS_180_240.name ]: education_group_year.partial_title = "Finalité spécialisée" education_group_year.partial_title_english = "Professional Focus" education_group_year.save()
def _postpone_child_branch(self, old_gr: GroupElementYear, new_gr: GroupElementYear) -> GroupElementYear: """ Unlike child leaf, the child branch must be postponed (recursively) """ old_egy = old_gr.child_branch new_egy = old_egy.next_year() if new_egy: is_empty = self._is_empty(new_egy) if new_gr.link_type == LinkTypes.REFERENCE.name and is_empty: self.warnings.append(ReferenceLinkEmptyWarning(new_egy)) elif not is_empty: if not (new_egy.is_training() or new_egy.education_group_type.name in MiniTrainingType.to_postpone()): self.warnings.append( EducationGroupYearNotEmptyWarning( new_egy, self.next_academic_year)) else: self._postpone(old_egy, new_egy) else: # If the education group does not exists for the next year, we have to postpone. new_egy = self._duplication_education_group_year(old_gr, old_egy) self.number_elements_created += 1 new_gr.child_branch = new_egy if new_egy and new_egy.education_group_type.name == MiniTrainingType.OPTION.name: self.postponed_options[new_egy.id] = new_gr if new_egy and new_gr.parent.education_group_type.name in TrainingType.finality_types( ): self.postponed_finalities.append(new_gr) return new_gr
def _postpone_child_branch(self, old_gr: GroupElementYear, new_gr: GroupElementYear) -> GroupElementYear: """ Unlike child leaf, the child branch must be postponed (recursively) """ old_egy = old_gr.child_branch new_egy = old_egy.next_year() if new_egy: is_empty = self._is_empty(new_egy) if new_gr.link_type == LinkTypes.REFERENCE.name and is_empty: self.warnings.append(ReferenceLinkEmptyWarning(new_egy)) elif not is_empty: if not (new_egy.is_training() or new_egy.is_mini_training()): self.warnings.append(EducationGroupYearNotEmptyWarning(new_egy, self.next_academic_year)) else: self._postpone(old_egy, new_egy) else: # If the education group does not exists for the next year, we have to postpone. new_egy = self._duplication_education_group_year(old_gr, old_egy) new_gr.child_branch = new_egy if new_egy and new_egy.education_group_type.name == MiniTrainingType.OPTION.name: self.postponed_options[new_egy.id] = new_gr if new_egy and new_gr.parent.education_group_type.name in TrainingType.finality_types(): self.postponed_finalities.append(new_gr) return new_gr
def set_initial_diploma_values(self): if self.education_group_type and \ self.education_group_type.name in TrainingType.with_diploma_values_set_initially_as_true(): self.fields['joint_diploma'].initial = True self.fields['diploma_printing_title'].required = True else: self.fields['joint_diploma'].initial = False self.fields['diploma_printing_title'].required = False
def set_initial_diploma_values(self): if self.education_group_type and \ self.education_group_type.name in TrainingType.with_diploma_values_set_initially_as_true(): self.fields['joint_diploma'].initial = True self.fields['diploma_printing_title'].required = True else: self.fields['joint_diploma'].initial = False self.fields['diploma_printing_title'].required = False
def reverse_migration(apps, schema_editor): education_group_year_mdl = apps.get_model("base", "EducationGroupYear") education_group_years = education_group_year_mdl.objects.filter( academic_year__year__gte=2019, education_group_type__name__in=TrainingType.finality_types()) for education_group_year in education_group_years: education_group_year.partial_title = "" education_group_year.partial_title_english = "" education_group_year.save()
def test_get_publish_url_case_not_common_and_finality_case(self): training = TrainingFactory(education_group_type__name=random.choice( TrainingType.finality_types())) expected_url = "{api_url}/{endpoint}".format( api_url=settings.ESB_API_URL, endpoint=settings.ESB_REFRESH_PEDAGOGY_ENDPOINT.format( year=training.academic_year.year, code=training.partial_acronym), ) self.assertEqual(expected_url, _get_url_to_publish(training))
def _check_end_year_constraints_on_2m(self): """ In context of 2M, when we add a finality [or group which contains finality], we must ensure that the end date of all 2M is greater or equals of all finalities """ finalities_to_add_qs = EducationGroupYear.objects.filter( pk=self.child.pk) | EducationGroupYear.hierarchy.filter( pk=self.child.pk).get_children() finalities_to_add_qs = finalities_to_add_qs.filter( education_group_type__name__in=TrainingType.finality_types()) root_2m_qs = self.parents | EducationGroupYear.objects.filter( pk=self.parent.pk) root_2m_qs = root_2m_qs.filter( education_group_type__name__in=TrainingType.root_master_2m_types(), education_group__end_year__isnull=False, ).order_by('education_group__end_year') errors = [] if finalities_to_add_qs.exists() and root_2m_qs.exists(): root_2m_early_end_date = root_2m_qs.first() invalid_finalities_acronyms = finalities_to_add_qs.filter( Q(education_group__end_year__gt=root_2m_early_end_date. education_group.end_year) | Q(education_group__end_year__isnull=True)).values_list( 'acronym', flat=True) if invalid_finalities_acronyms: errors.append( ValidationError( ngettext( "Finality \"%(acronym)s\" has an end date greater than %(root_acronym)s program.", "Finalities \"%(acronym)s\" have an end date greater than %(root_acronym)s program.", len(invalid_finalities_acronyms)) % { "acronym": ', '.join(invalid_finalities_acronyms), "root_acronym": root_2m_early_end_date.acronym })) if errors: raise ValidationError(errors)
def _check_end_year_root_2m_cover_all_finalities(self, root_2m_egy): qs = EducationGroupYear.hierarchy.filter(pk=root_2m_egy.pk) \ .get_children() \ .filter( Q(education_group__end_year__gt=self.end_year) | Q(education_group__end_year__isnull=True), education_group_type__name__in=TrainingType.finality_types(), ) for invalid_finality in qs: raise ValidationError({ 'end_year': _('The end date must be greater or equals to the finality %(acronym)s') % {'acronym': invalid_finality.acronym} })
def _check_end_year_root_2m_cover_all_finalities(self, root_2m_egy): qs = EducationGroupYear.hierarchy.filter(pk=root_2m_egy.pk) \ .get_children() \ .filter( Q(education_group__end_year__gt=self.end_year) | Q(education_group__end_year__isnull=True), education_group_type__name__in=TrainingType.finality_types(), ) for invalid_finality in qs: raise ValidationError({ 'end_year': _('The end date must be greater or equals to the finality %(acronym)s') % {'acronym': invalid_finality.acronym} })
def _check_end_year_finality_are_in_range_of_root_2m(self, finality_egy): qs = EducationGroupYear.hierarchy.filter(pk=finality_egy.pk) \ .get_parents().filter(education_group_type__name__in=TrainingType.root_master_2m_types()) if self.end_year is None: qs = qs.filter(education_group__end_year__isnull=False) else: qs = qs.filter(education_group__end_year__lt=self.end_year) for invalid_root_2m in qs: raise ValidationError({ 'end_year': _('The end date must be less or equals to the root %(acronym)s') % {'acronym': invalid_root_2m.acronym} })
def _check_end_year_finality_are_in_range_of_root_2m(self, finality_egy): qs = EducationGroupYear.hierarchy.filter(pk=finality_egy.pk)\ .get_parents().filter(education_group_type__name__in=TrainingType.root_master_2m_types()) if self.end_year is None: qs = qs.filter(education_group__end_year__isnull=False) else: qs = qs.filter(education_group__end_year__lt=self.end_year) for invalid_root_2m in qs: raise ValidationError({ 'end_year': _('The end date must be less or equals to the root %(acronym)s') % {'acronym': invalid_root_2m.acronym} })
def _get_missing_options(self): """ In context of MA/MD/MS when we add an option [or group which contains options], this options must exist in parent context (2m) """ options_missing_by_finality = {} options_to_add = EducationGroupHierarchy(root=self.child).get_option_list() if self.child.education_group_type.name == MiniTrainingType.OPTION.name: options_to_add += [self.child] finalities_qs = self.parents | EducationGroupYear.objects.filter(pk=self.parent.pk) finalities_pks = finalities_qs.filter( education_group_type__name__in=TrainingType.finality_types() ).values_list('pk', flat=True) if finalities_pks: root_2m_qs = EducationGroupYear.hierarchy.filter(pk__in=finalities_pks).get_parents().filter( education_group_type__name__in=TrainingType.root_master_2m_types() ) for root in root_2m_qs: options_in_2m = EducationGroupHierarchy(root=root).get_option_list() options_missing_by_finality[root] = set(options_to_add) - set(options_in_2m) return options_missing_by_finality
def _check_detatch_options_rules(self): """ In context of 2M when we detach an option [or group which contains option], we must ensure that these options are not present in MA/MD/MS """ options_to_detach = self._get_options_to_detach() errors = [] for master_2m in self.get_parents_program_master(): master_2m_tree = EducationGroupHierarchy(root=master_2m) counter_options = Counter(master_2m_tree.get_option_list()) counter_options.subtract(options_to_detach) options_to_check = [ opt for opt, count in counter_options.items() if count == 0 ] if not options_to_check: continue finality_list = [ elem.child for elem in master_2m_tree.to_list(flat=True) if isinstance(elem.child, EducationGroupYear) and elem.child. education_group_type.name in TrainingType.finality_types() ] for finality in finality_list: mandatory_options = EducationGroupHierarchy( root=finality).get_option_list() missing_options = set(options_to_check) & set( mandatory_options) if missing_options: errors.append( ValidationError( ngettext( "Option \"%(acronym)s\" cannot be detach because it is contained in" " %(finality_acronym)s program.", "Options \"%(acronym)s\" cannot be detach because they are contained in" " %(finality_acronym)s program.", len(missing_options)) % { "acronym": ', '.join(option.acronym for option in missing_options), "finality_acronym": finality.acronym })) if errors: raise ValidationError(errors)
def test_get_publish_url_case_not_common_and_finality_or_option_case(self): training = EducationGroupYearFactory( education_group_type__name=random.choice( TrainingType.finality_types() + [MiniTrainingType.OPTION.name])) parent = TrainingFactory( education_group_type__name=TrainingType.PGRM_MASTER_120.name, academic_year=training.academic_year) GroupElementYearFactory(parent=parent, child_branch=training) expected_url = "{api_url}/{endpoint}".format( api_url=settings.ESB_API_URL, endpoint=settings.ESB_REFRESH_PEDAGOGY_ENDPOINT.format( year=training.academic_year.year, code="{parent}-{partial_acronym}".format( parent=parent.acronym, partial_acronym=training.partial_acronym)), ) self.assertEqual(expected_url, _get_url_to_publish(training))
def _check_detatch_options_rules(self): """ In context of 2M when we detach an option [or group which contains option], we must ensure that these options are not present in MA/MD/MS """ options_to_detach = self._get_options_to_detach() errors = [] for master_2m in self.get_parents_program_master(): master_2m_tree = EducationGroupHierarchy(root=master_2m) counter_options = Counter(master_2m_tree.get_option_list()) counter_options.subtract(options_to_detach) options_to_check = [opt for opt, count in counter_options.items() if count == 0] if not options_to_check: continue finality_list = [elem.child for elem in master_2m_tree.to_list(flat=True) if isinstance(elem.child, EducationGroupYear) and elem.child.education_group_type.name in TrainingType.finality_types()] for finality in finality_list: mandatory_options = EducationGroupHierarchy(root=finality).get_option_list() missing_options = set(options_to_check) & set(mandatory_options) if missing_options: errors.append( ValidationError( ngettext( "Option \"%(acronym)s\" cannot be detach because it is contained in" " %(finality_acronym)s program.", "Options \"%(acronym)s\" cannot be detach because they are contained in" " %(finality_acronym)s program.", len(missing_options) ) % { "acronym": ', '.join(option.acronym for option in missing_options), "finality_acronym": finality.acronym }) ) if errors: raise ValidationError(errors)
def _check_end_year_constraints_on_2m(self): """ In context of 2M, when we add a finality [or group which contains finality], we must ensure that the end date of all 2M is greater or equals of all finalities """ finalities_to_add_qs = EducationGroupYear.objects.filter(pk=self.child.pk) | \ EducationGroupYear.hierarchy.filter(pk=self.child.pk).get_children() finalities_to_add_qs = finalities_to_add_qs.filter(education_group_type__name__in=TrainingType.finality_types()) root_2m_qs = self.parents | EducationGroupYear.objects.filter(pk=self.parent.pk) root_2m_qs = root_2m_qs.filter( education_group_type__name__in=TrainingType.root_master_2m_types(), education_group__end_year__isnull=False, ).order_by('education_group__end_year') errors = [] if finalities_to_add_qs.exists() and root_2m_qs.exists(): root_2m_early_end_date = root_2m_qs.first() invalid_finalities_acronyms = finalities_to_add_qs.filter( Q(education_group__end_year__gt=root_2m_early_end_date.education_group.end_year) | Q(education_group__end_year__isnull=True) ).values_list('acronym', flat=True) if invalid_finalities_acronyms: errors.append( ValidationError( ngettext( "Finality \"%(acronym)s\" has an end date greater than %(root_acronym)s program.", "Finalities \"%(acronym)s\" have an end date greater than %(root_acronym)s program.", len(invalid_finalities_acronyms) ) % { "acronym": ', '.join(invalid_finalities_acronyms), "root_acronym": root_2m_early_end_date.acronym } ) ) if errors: raise ValidationError(errors)
from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.views.generic import UpdateView, DetailView from base.models.education_group_year import EducationGroupYear from base.models.enums.education_group_types import TrainingType, MiniTrainingType, GroupType from base.models.group_element_year import GroupElementYear from base.models.learning_unit_year import LearningUnitYear from base.models.person import Person from base.views.education_groups import perms from base.views.education_groups.detail import CatalogGenericDetailView from base.views.mixins import RulesRequiredMixin, FlagMixin, AjaxTemplateMixin from program_management.business.group_element_years.group_element_year_tree import EducationGroupHierarchy NO_PREREQUISITES = TrainingType.finality_types() + [ MiniTrainingType.OPTION.name, MiniTrainingType.MOBILITY_PARTNERSHIP.name, ] + GroupType.get_names() @method_decorator(login_required, name='dispatch') class GenericGroupElementYearMixin(FlagMixin, RulesRequiredMixin, SuccessMessageMixin, AjaxTemplateMixin): model = GroupElementYear context_object_name = "group_element_year" pk_url_kwarg = "group_element_year_id" # FlagMixin flag = "education_group_update" # RulesRequiredMixin
def is_attestation(self): return self.type in TrainingType.attestation_types()
def get_finality_list(self): return [ element.child_branch for element in self.to_list(flat=True) if element.child_branch and element.child_branch. education_group_type.name in TrainingType.finality_types() ]
def show_skills_and_achievements(self): return not self.object.is_common and \ self.object.education_group_type.name in itertools.chain(TrainingType.with_skills_achievements(), MiniTrainingType.with_admission_condition()) \ and self.is_general_info_and_condition_admission_in_display_range()
def show_admission_conditions(self): # @TODO: Need to refactor after business clarification return not self.object.is_main_common and \ self.object.education_group_type.name in itertools.chain(TrainingType.with_admission_condition(), MiniTrainingType.with_admission_condition()) \ and self.is_general_info_and_condition_admission_in_display_range()
def _show_free_text(self): return not self.object.is_common and self.object.education_group_type.name in itertools.chain( TrainingType.with_admission_condition(), MiniTrainingType.with_admission_condition() )
def is_valid(self): if self.parent.education_group_type.name in TrainingType.root_master_2m_types() or \ self.parents.filter(education_group_type__name__in=TrainingType.root_master_2m_types()).exists(): self._check_end_year_constraints_on_2m() self._check_attach_options_rules() return True
def is_finality(self): return self.type in TrainingType.finality_types()