Skip to content

Commit

Permalink
code ok?
Browse files Browse the repository at this point in the history
TODO: Verify all method calls have been refactored, update the tests
  • Loading branch information
francoisfreitag committed Jun 12, 2024
1 parent 19b9a71 commit 208768c
Show file tree
Hide file tree
Showing 17 changed files with 131 additions and 97 deletions.
29 changes: 17 additions & 12 deletions itou/eligibility/models/iae.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@


class EligibilityDiagnosisQuerySet(CommonEligibilityDiagnosisQuerySet):
def for_job_seeker_and_siae(self, job_seeker, *, siae=None):
def for_job_seeker_and_siae(self, viewing_user, *, job_seeker, siae=None):
author_filter = models.Q(author_kind=AuthorKind.PRESCRIBER)
if siae is not None:
# In most cases, the viewing user is the acting user and only employers
# should see their eligibility diagnosis.
# In Django admin, the viewing user does not matter and None is provided.
if (viewing_user is None or viewing_user.is_employer) and siae is not None:
author_filter |= models.Q(author_siae=siae)
return self.filter(author_filter, job_seeker=job_seeker)

Expand All @@ -38,7 +41,7 @@ def has_approval(self):


class EligibilityDiagnosisManager(models.Manager):
def has_considered_valid(self, job_seeker, for_siae=None):
def has_considered_valid(self, viewing_user, job_seeker, for_siae=None):
"""
Returns True if the given job seeker has a considered valid diagnosis,
False otherwise.
Expand All @@ -56,22 +59,24 @@ def has_considered_valid(self, job_seeker, for_siae=None):
Hence the Trello #2604 decision: if a PASS IAE is valid, we do not
check the presence of an eligibility diagnosis.
"""
return job_seeker.has_valid_common_approval or bool(self.last_considered_valid(job_seeker, for_siae=for_siae))
return job_seeker.has_valid_common_approval or bool(
self.last_considered_valid(viewing_user, job_seeker, for_siae=for_siae)
)

def last_considered_valid(self, job_seeker, for_siae=None):
def last_considered_valid(self, viewing_user, job_seeker, for_siae=None):
"""
Retrieves the given job seeker's last considered valid diagnosis or None.
If the `for_siae` argument is passed, it means that we are looking for
a diagnosis from an employer perspective. The scope is restricted to
avoid showing diagnoses made by other employers.
If the `for_siae` argument is passed and we are looking for a diagnosis
from an employer perspective. The scope is restricted to avoid showing
diagnoses made by employers to other employers and prescribers.
A diagnosis made by a prescriber takes precedence even when an employer
diagnosis already exists.
"""

query = (
self.for_job_seeker_and_siae(job_seeker, siae=for_siae)
self.for_job_seeker_and_siae(viewing_user, job_seeker, siae=for_siae)
.select_related("author", "author_siae", "author_prescriber_organization")
.annotate(from_prescriber=Case(When(author_kind=AuthorKind.PRESCRIBER, then=1), default=0))
.order_by("-from_prescriber", "-created_at")
Expand All @@ -83,7 +88,7 @@ def last_considered_valid(self, job_seeker, for_siae=None):
# not.
return query.first()

def last_expired(self, job_seeker, for_siae=None):
def last_expired(self, viewing_user, job_seeker, for_siae=None):
"""
Retrieves the given job seeker's last expired diagnosis or None.
Expand All @@ -97,8 +102,8 @@ def last_expired(self, job_seeker, for_siae=None):
.order_by("created_at")
)

if not self.has_considered_valid(job_seeker=job_seeker, for_siae=for_siae):
last = query.for_job_seeker_and_siae(job_seeker, siae=for_siae).last()
if not self.has_considered_valid(viewing_user, job_seeker, for_siae=for_siae):
last = query.for_job_seeker_and_siae(viewing_user, job_seeker, siae=for_siae).last()

return last

Expand Down
3 changes: 2 additions & 1 deletion itou/job_applications/admin_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def clean(self):
and self.cleaned_data.get("hiring_without_approval") is not True
and (job_seeker := self.cleaned_data.get("job_seeker"))
and not job_seeker.has_valid_common_approval
and not EligibilityDiagnosis.objects.last_considered_valid(job_seeker, for_siae=to_company)
# Viewing user isn’t relevant, pass None.
and not EligibilityDiagnosis.objects.last_considered_valid(None, job_seeker, for_siae=to_company)
):
self.add_error(
None,
Expand Down
20 changes: 12 additions & 8 deletions itou/job_applications/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ def _get_selected_jobs(job_application):
return selected_jobs


def _get_eligibility_status(job_application):
def _get_eligibility_status(job_application, viewing_user):
eligibility = "non"
# Eligibility diagnoses made by SIAE are ignored.
if job_application.job_seeker.has_valid_diagnosis():
if job_application.job_seeker.has_valid_diagnosis(viewing_user):
eligibility = "oui"

return eligibility
Expand Down Expand Up @@ -92,7 +92,7 @@ def _resolve_title(title, nir):
return ""


def _serialize_job_application(job_application):
def _serialize_job_application(job_application, viewing_user):
job_seeker = job_application.job_seeker
company = job_application.to_company

Expand Down Expand Up @@ -126,19 +126,23 @@ def _serialize_job_application(job_application):
_format_date(job_application.hiring_start_at),
_format_date(job_application.hiring_end_at),
job_application.get_refusal_reason_display(),
_get_eligibility_status(job_application),
_get_eligibility_status(job_application, viewing_user),
numero_pass_iae,
_format_date(approval_start_date),
_format_date(approval_end_date),
approval_state,
]


def _job_applications_serializer(queryset):
return [_serialize_job_application(job_application) for job_application in queryset]
class JobApplicationSerializer:
def __init__(self, request):
self.viewing_user = request.user

def __call__(self, queryset):
return [_serialize_job_application(job_application, self.viewing_user) for job_application in queryset]

def stream_xlsx_export(job_applications, filename):

def stream_xlsx_export(request, job_applications, filename):
"""
Takes a list of job application, converts them to XLSX and writes them in the provided stream
The stream can be for instance an http response, a string (io.StringIO()) or a file
Expand All @@ -147,5 +151,5 @@ def stream_xlsx_export(job_applications, filename):
job_applications,
filename,
JOB_APPLICATION_CSV_HEADERS,
_job_applications_serializer,
JobApplicationSerializer(request),
)
24 changes: 16 additions & 8 deletions itou/job_applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def with_jobseeker_eligibility_diagnosis(self):
)
return self.annotate(jobseeker_eligibility_diagnosis=Coalesce(F("eligibility_diagnosis"), sub_query, None))

def eligibility_validated(self):
def eligibility_validated(self, viewing_user):
return self.filter(
Exists(
Approval.objects.filter(
Expand All @@ -240,7 +240,9 @@ def eligibility_validated(self):
)
| Exists(
EligibilityDiagnosis.objects.for_job_seeker_and_siae(
OuterRef("job_seeker"), siae=OuterRef("to_company")
viewing_user,
OuterRef("job_seeker"),
siae=OuterRef("to_company"),
).valid()
)
)
Expand Down Expand Up @@ -785,13 +787,13 @@ def is_sent_by_authorized_prescriber(self):
def is_spontaneous(self):
return not self.selected_jobs.exists()

def eligibility_diagnosis_by_siae_required(self):
def eligibility_diagnosis_by_siae_required(self, viewing_user):
"""
Returns True if an eligibility diagnosis must be made by an SIAE
when processing an application, False otherwise.
"""
return self.to_company.is_subject_to_eligibility_rules and not self.job_seeker.has_valid_diagnosis(
for_siae=self.to_company
viewing_user, for_siae=self.to_company
)

@property
Expand Down Expand Up @@ -951,7 +953,7 @@ def transfer_to(self, transferred_by, target_company):
if self.sender_kind == SenderKind.PRESCRIBER and self.sender_id: # Sender user may have been deleted.
self.notifications_transfer_for_proxy(notification_context).send()

def get_eligibility_diagnosis(self):
def get_eligibility_diagnosis(self, viewing_user):
"""
Returns the eligibility diagnosis linked to this job application or None.
"""
Expand All @@ -961,7 +963,11 @@ def get_eligibility_diagnosis(self):
return self.eligibility_diagnosis
# As long as the job application has not been accepted, diagnosis-related
# business rules may still prioritize one diagnosis over another.
return EligibilityDiagnosis.objects.last_considered_valid(self.job_seeker, for_siae=self.to_company)
return EligibilityDiagnosis.objects.last_considered_valid(
viewing_user,
self.job_seeker,
for_siae=self.to_company,
)

# Workflow transitions.

Expand All @@ -985,14 +991,16 @@ def accept(self, *args, **kwargs):
# Link to the job seeker's eligibility diagnosis.
if self.to_company.is_subject_to_eligibility_rules:
self.eligibility_diagnosis = EligibilityDiagnosis.objects.last_considered_valid(
self.job_seeker, for_siae=self.to_company
accepted_by, self.job_seeker, for_siae=self.to_company
)

# Approval issuance logic.
if not self.hiring_without_approval and self.to_company.is_subject_to_eligibility_rules:
if self.job_seeker.has_common_approval_in_waiting_period:
if self.job_seeker.new_approval_blocked_by_waiting_period(
siae=self.to_company, sender_prescriber_organization=self.sender_prescriber_organization
accepted_by,
siae=self.to_company,
sender_prescriber_organization=self.sender_prescriber_organization,
):
# Security check: it's supposed to be blocked upstream.
raise xwf_models.AbortTransition("Job seeker has an approval in waiting period.")
Expand Down
3 changes: 2 additions & 1 deletion itou/templates/apply/includes/list_card_body_company.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% load call_method %}
{% load str_filters %}

<div class="c-box--results__header">
Expand All @@ -22,7 +23,7 @@ <h3>{{ job_application.job_seeker.get_full_name|mask_unless:job_application.user
PASS IAE {{ approval.get_state_display|lower }}
</span>
{% else %}
{% if job_application.iae_eligibility_diagnosis_required %}
{% if call_method job_application "eligibility_diagnosis_by_siae_required" request.user %}
<span class="badge badge-xs rounded-pill bg-accent-02-lighter text-primary">
<i class="ri-error-warning-line" aria-hidden="true"></i>
Éligibilité IAE à valider
Expand Down
7 changes: 4 additions & 3 deletions itou/templates/apply/includes/siae_hiring_actions.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% load call_method %}
{% load django_bootstrap5 %}
{% load format_filters %}
{% load matomo %}
Expand All @@ -20,7 +21,7 @@ <h2 class="visually-hidden">Actions rapides</h2>

{# Possible next steps when the state is processing / prior_to_hire ------------------------------------- #}
{% if job_application.state.is_processing or job_application.state.is_prior_to_hire %}
{% if eligibility_diagnosis_by_siae_required %}
{% if call_method job_application "eligibility_diagnosis_by_siae_required" request.user %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/new_diagnosis.html" %}</div>
{% else %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/accept.html" %}</div>
Expand All @@ -31,7 +32,7 @@ <h2 class="visually-hidden">Actions rapides</h2>

{# Possible next steps when the state is postponed ------------------------------------------------------ #}
{% if job_application.state.is_postponed %}
{% if eligibility_diagnosis_by_siae_required %}
{% if call_method job_application "eligibility_diagnosis_by_siae_required" request.user %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/new_diagnosis.html" %}</div>
{% else %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/accept.html" %}</div>
Expand All @@ -41,7 +42,7 @@ <h2 class="visually-hidden">Actions rapides</h2>

{# Possible next steps when the state is obsolete, refused or cancelled --------------------------------- #}
{% if job_application.state.is_obsolete or job_application.state.is_refused or job_application.state.is_cancelled %}
{% if eligibility_diagnosis_by_siae_required %}
{% if call_method job_application "eligibility_diagnosis_by_siae_required" request.user %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/new_diagnosis.html" %}</div>
{% else %}
<div class="form-group col col-lg-auto">{% include "apply/includes/buttons/accept.html" %}</div>
Expand Down
4 changes: 2 additions & 2 deletions itou/templates/approvals/includes/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
{% else %}
Numéro d'agrément :
{% endif %}
{% if user.is_employer and common_approval.is_in_waiting_period and common_approval.user.has_valid_diagnosis %}
{% if user.is_employer and common_approval.is_in_waiting_period and call_method common_approval.user "has_valid_diagnosis" request.user %}
{% comment %}
If the PASS IAE number is displayed at this time, some employers think that there is
no need to validate the application because a number is already assigned.
Expand Down Expand Up @@ -74,7 +74,7 @@
{% endif %}
</ul>
{% elif common_approval.is_in_waiting_period %}
{% if user.is_employer and common_approval.user.has_valid_diagnosis %}
{% if user.is_employer and call_method common_approval.user "has_valid_diagnosis" request.user %}
{% comment %}
When an authorized prescriber bypasses the waiting period and sends a candidate
with an "expired" approval, the employer receives the application with the mention
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{% load call_method %}

<div class="col-md-8 mb-3 mb-md-5">
<div class="c-box p-0 h-100">
<div class="d-flex p-3 p-lg-4">
Expand All @@ -14,7 +16,7 @@
<li class="mb-2">{% include "approvals/includes/status.html" with common_approval=user.latest_common_approval %}</li>
{% if user.has_common_approval_in_waiting_period %}
<li class="mb-2">
{% if user.has_valid_diagnosis %}
{% if call_method user "has_valid_diagnosis" request.user %}
<p class="mb-0">
Un prescripteur habilité a réalisé un diagnostic d'éligibilité. <b>Vous pouvez commencer un nouveau parcours.</b>
</p>
Expand Down
10 changes: 5 additions & 5 deletions itou/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def latest_common_approval(self):
"""
Rationale:
- if there is a latest PASS IAE that is valid, it is returned.
- if there is no PASS IAE, we return the longest PE Approval whatever its state.
- if there is no PASS IAE, we return the latest, longest PE Approval whatever its state.
- if there is no PASS nor PE Approval, or the waiting period for those is over, return nothing.
- if the latest PASS IAE is invalid:
* but still in waiting period:
Expand Down Expand Up @@ -535,7 +535,7 @@ def has_common_approval_in_waiting_period(self):
def has_no_common_approval(self):
return not self.latest_approval and not self.latest_pe_approval

def new_approval_blocked_by_waiting_period(self, siae, sender_prescriber_organization):
def new_approval_blocked_by_waiting_period(self, viewing_user, siae, sender_prescriber_organization):
"""
Don’t create approvals for users whose approval recently ended,
unless an authorized prescriber asks for it, or the structure isn’t an SIAE.
Expand All @@ -545,7 +545,7 @@ def new_approval_blocked_by_waiting_period(self, siae, sender_prescriber_organiz
)

# Only diagnoses made by authorized prescribers are taken into account.
has_valid_diagnosis = self.has_valid_diagnosis()
has_valid_diagnosis = self.has_valid_diagnosis(viewing_user)
return (
self.has_common_approval_in_waiting_period
and siae.is_subject_to_eligibility_rules
Expand Down Expand Up @@ -599,8 +599,8 @@ def has_external_data(self):
def has_jobseeker_profile(self):
return self.is_job_seeker and hasattr(self, "jobseeker_profile")

def has_valid_diagnosis(self, for_siae=None):
return self.eligibility_diagnoses.has_considered_valid(job_seeker=self, for_siae=for_siae)
def has_valid_diagnosis(self, viewing_user, for_siae=None):
return self.eligibility_diagnoses.has_considered_valid(viewing_user, self, for_siae=for_siae)

def joined_recently(self):
time_since_date_joined = timezone.now() - self.date_joined
Expand Down
13 changes: 7 additions & 6 deletions itou/www/apply/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,8 @@ class CompanyPrescriberFilterJobApplicationsForm(FilterJobApplicationsForm):
selected_jobs = forms.MultipleChoiceField(required=False, label="Fiches de poste", widget=Select2MultipleWidget)

@sentry_sdk.trace
def __init__(self, job_applications_qs, *args, **kwargs):
def __init__(self, user, job_applications_qs, *args, **kwargs):
self.user = user
self.job_applications_qs = job_applications_qs
super().__init__(*args, **kwargs)
senders = self.job_applications_qs.get_unique_fk_objects("sender")
Expand Down Expand Up @@ -987,7 +988,7 @@ def _get_choices_for_jobs(self):
def filter(self, queryset):
queryset = super().filter(queryset)
if self.cleaned_data.get("eligibility_validated"):
queryset = queryset.eligibility_validated()
queryset = queryset.eligibility_validated(self.user)

if senders := self.cleaned_data.get("senders"):
queryset = queryset.filter(sender__id__in=senders)
Expand All @@ -1008,8 +1009,8 @@ class CompanyFilterJobApplicationsForm(CompanyPrescriberFilterJobApplicationsFor
widget=Select2MultipleWidget,
)

def __init__(self, job_applications_qs, company, *args, **kwargs):
super().__init__(job_applications_qs, *args, **kwargs)
def __init__(self, user, job_applications_qs, company, *args, **kwargs):
super().__init__(user, job_applications_qs, *args, **kwargs)
self.fields["sender_organizations"].choices += self.get_sender_organization_choices()

if company.kind not in SIAE_WITH_CONVENTION_KINDS:
Expand Down Expand Up @@ -1045,8 +1046,8 @@ class PrescriberFilterJobApplicationsForm(CompanyPrescriberFilterJobApplications

to_companies = forms.MultipleChoiceField(required=False, label="Structure", widget=Select2MultipleWidget)

def __init__(self, job_applications_qs, *args, **kwargs):
super().__init__(job_applications_qs, *args, **kwargs)
def __init__(self, user, job_applications_qs, *args, **kwargs):
super().__init__(user, job_applications_qs, *args, **kwargs)
self.fields["to_companies"].choices += self.get_to_companies_choices()

def filter(self, queryset):
Expand Down
Loading

0 comments on commit 208768c

Please sign in to comment.