Exemple #1
0
def summary(request,
            employee_record_id,
            template_name="employee_record/summary.html"):
    """
    Display the summary of a given employee record (no update possible)
    """
    siae = get_current_siae_or_404(request)

    if not siae.can_use_employee_record:
        raise PermissionDenied

    query_base = EmployeeRecord.objects.full_fetch()
    employee_record = get_object_or_404(query_base, pk=employee_record_id)
    job_application = employee_record.job_application

    if not siae_is_allowed(job_application, siae):
        raise PermissionDenied

    status = request.GET.get("status")

    context = {
        "employee_record": employee_record,
        "status": status,
    }

    return render(request, template_name, context)
Exemple #2
0
def can_create_employee_record(request, job_application_id=None):
    """
    Check if conditions / permissions are set to use the employee record views
    If a valid job_application_id is given, return a full-fledged
    JobApplication object reusable in view (skip one extra DB query)
    """
    # SIAEs only
    siae = get_current_siae_or_404(request)

    # SIAE is eligible to employee record ?
    if not siae.can_use_employee_record:
        raise PermissionDenied

    if job_application_id:
        # We want to reuse a job application in view, but first check that all is ok
        job_application = get_object_or_404(
            JobApplication.objects.select_related(
                "approval",
                "to_siae",
                "job_seeker",
                "job_seeker__jobseeker_profile",
            ),
            pk=job_application_id,
        )

        if not (siae_is_allowed(job_application, siae)
                and tunnel_step_is_allowed(job_application)):
            raise PermissionDenied

        # Finally, the reusable Holy Grail
        return job_application

    # All checks performed, without asking for a job application object
    return None
Exemple #3
0
def list_for_siae(request, template_name="apply/list_for_siae.html"):
    """
    List of applications for an SIAE.
    """
    siae = get_current_siae_or_404(request)
    job_applications = siae.job_applications_received

    filters_form = SiaeFilterJobApplicationsForm(job_applications, request.GET
                                                 or None)
    filters = None

    job_applications = job_applications.not_archived().with_list_related_data()

    if filters_form.is_valid():
        job_applications = job_applications.filter(
            *filters_form.get_qs_filters())
        filters = filters_form.humanize_filters()

    job_applications_page = pager(job_applications,
                                  request.GET.get("page"),
                                  items_per_page=10)

    context = {
        "siae": siae,
        "job_applications_page": job_applications_page,
        "filters_form": filters_form,
        "filters": filters,
    }
    return render(request, template_name, context)
Exemple #4
0
def approval_as_pdf(request, job_application_id, template_name="approvals/approval_as_pdf.html"):
    """
    Displays the approval in pdf format
    """
    siae = get_current_siae_or_404(request)

    queryset = JobApplication.objects.select_related("job_seeker", "eligibility_diagnosis", "approval", "to_siae")
    job_application = get_object_or_404(queryset, pk=job_application_id, to_siae=siae)

    if not job_application.can_download_approval_as_pdf:
        # Message only visible in DEBUG
        raise Http404("Nous sommes au regret de vous informer que vous ne pouvez pas télécharger cet agrément.")

    diagnosis = job_application.get_eligibility_diagnosis()
    diagnosis_author = None
    diagnosis_author_org = None
    diagnosis_author_org_name = None

    if diagnosis:
        diagnosis_author = diagnosis.author.get_full_name()
        diagnosis_author_org = diagnosis.author_prescriber_organization or diagnosis.author_siae
        if diagnosis_author_org:
            diagnosis_author_org_name = diagnosis_author_org.display_name

    if not diagnosis and job_application.approval and job_application.approval.originates_from_itou:
        # On November 30th, 2021, AI were delivered a PASS IAE
        # without a diagnosis for all of their employees.
        if not job_application.approval.is_from_ai_stock:
            # Keep track of job applications without a proper eligibility diagnosis because
            # it shouldn't happen.
            # If this occurs too much we may have to change `can_download_approval_as_pdf()`
            # and investigate a lot more about what's going on.
            # See also migration `0035_link_diagnoses.py`.
            raise ObjectDoesNotExist("Job application %s has no eligibility diagnosis." % job_application.pk)

    # The PDFShift API can load styles only if it has the full URL.
    base_url = request.build_absolute_uri("/")[:-1]

    if settings.DEBUG:
        # Use staging or production styles when working locally
        # as PDF shift can't access local files.
        base_url = f"{settings.ITOU_PROTOCOL}://{settings.ITOU_STAGING_DN}"

    context = {
        "approval": job_application.approval,
        "base_url": base_url,
        "assistance_url": settings.ITOU_ASSISTANCE_URL,
        "diagnosis_author": diagnosis_author,
        "diagnosis_author_org_name": diagnosis_author_org_name,
        "siae": job_application.to_siae,
        "job_seeker": job_application.job_seeker,
    }

    html = SimpleTemplateResponse(template=template_name, context=context).rendered_content

    full_name_slug = slugify(job_application.job_seeker.get_full_name())
    filename = f"{full_name_slug}-pass-iae.pdf"

    with HtmlToPdf(html, autoclose=False) as transformer:
        return FileResponse(transformer.file, as_attachment=True, filename=filename)
Exemple #5
0
def suspension_update(request, suspension_id, template_name="approvals/suspension_update.html"):
    """
    Edit the given suspension.
    """

    siae = get_current_siae_or_404(request)
    suspension = get_object_or_404(Suspension, pk=suspension_id)

    if not suspension.can_be_handled_by_siae(siae):
        raise PermissionDenied()

    back_url = get_safe_url(request, "back_url", fallback_url=reverse("dashboard:index"))

    form = SuspensionForm(approval=suspension.approval, siae=siae, instance=suspension, data=request.POST or None)

    if request.method == "POST" and form.is_valid():
        suspension = form.save(commit=False)
        suspension.updated_by = request.user
        suspension.save()
        messages.success(request, "Modification de suspension effectuée.")
        return HttpResponseRedirect(back_url)

    context = {
        "suspension": suspension,
        "back_url": back_url,
        "form": form,
    }
    return render(request, template_name, context)
Exemple #6
0
def create_siae(request, template_name="siaes/create_siae.html"):
    """
    Create a new SIAE (Antenne in French).
    """
    current_siae = get_current_siae_or_404(request)
    if not request.user.can_create_siae_antenna(parent_siae=current_siae):
        raise PermissionDenied

    form = CreateSiaeForm(
        current_siae=current_siae,
        current_user=request.user,
        data=request.POST or None,
        initial={
            "siret": current_siae.siret,
            "kind": current_siae.kind,
            "department": current_siae.department
        },
    )

    if request.method == "POST" and form.is_valid():
        with transaction.atomic():
            # The form creates multiple objects
            siae = form.save(request)

        request.session[settings.ITOU_SESSION_CURRENT_SIAE_KEY] = siae.pk
        return HttpResponseRedirect(reverse("dashboard:index"))

    context = {"form": form}
    return render(request, template_name, context)
Exemple #7
0
def dashboard(request, template_name="dashboard/dashboard.html"):
    job_applications_counter = 0
    prescriber_authorization_status_not_set = None
    prescriber_is_orienter = False

    if request.user.is_siae_staff:
        siae = get_current_siae_or_404(request)
        job_applications_counter = siae.job_applications_received.filter(
            state=JobApplicationWorkflow.STATE_NEW).count()

    # See template for display message while authorized organization is being validated (prescriber path)

    if request.user.is_prescriber_with_org:
        prescriber_organization = get_current_org_or_404(request)
        prescriber_authorization_status_not_set = (
            prescriber_organization.authorization_status ==
            PrescriberOrganization.AuthorizationStatus.NOT_SET)
        # This is to hide the "secret code", except for orienter orgs
        prescriber_is_orienter = (
            prescriber_organization.authorization_status ==
            PrescriberOrganization.AuthorizationStatus.NOT_REQUIRED)

    context = {
        "job_applications_counter": job_applications_counter,
        "prescriber_authorization_status_not_set":
        prescriber_authorization_status_not_set,
        "prescriber_is_orienter": prescriber_is_orienter,
    }

    return render(request, template_name, context)
Exemple #8
0
def approval_as_pdf(request, job_application_id, template_name="approvals/approval_as_pdf.html"):

    siae = get_current_siae_or_404(request)

    queryset = JobApplication.objects.select_related("job_seeker", "approval", "to_siae")
    job_application = get_object_or_404(queryset, pk=job_application_id, to_siae=siae)

    if not job_application.can_download_approval_as_pdf:
        raise Http404(
            _(
                """
            Nous sommes au regret de vous informer que
            vous ne pouvez pas télécharger cet agrément."""
            )
        )

    job_seeker = job_application.job_seeker
    user_name = job_seeker.get_full_name()

    diagnosis = None
    diagnosis_author = None
    diagnosis_author_org = None
    diagnosis_author_org_name = None

    # If an approval has been delivered by Pole Emploi, a diagnosis might
    # exist in the real world but not in our database.
    # Raise an error only if the diagnosis does not exist for an Itou approval.
    if job_application.approval.originates_from_itou:
        diagnosis = job_seeker.get_eligibility_diagnosis()
        diagnosis_author = diagnosis.author.get_full_name()
        diagnosis_author_org = diagnosis.author_prescriber_organization or diagnosis.author_siae
        if diagnosis_author_org:
            diagnosis_author_org_name = diagnosis_author_org.display_name

    # The PDFShift API can load styles only if it has
    # the full URL.
    base_url = request.build_absolute_uri("/")[:-1]

    if settings.DEBUG:
        # Use staging or production styles when working locally
        # as PDF shift can't access local files.
        base_url = f"{settings.ITOU_PROTOCOL}://{settings.ITOU_STAGING_DN}"

    context = {
        "approval": job_application.approval,
        "base_url": base_url,
        "contact_email": settings.ITOU_EMAIL_CONTACT,
        "diagnosis_author": diagnosis_author,
        "diagnosis_author_org_name": diagnosis_author_org_name,
        "siae": job_application.to_siae,
        "user_name": user_name,
    }

    html = SimpleTemplateResponse(template=template_name, context=context).rendered_content

    filename = f"{slugify(user_name)}-pass-iae.pdf"

    with HtmlToPdf(html, autoclose=False) as transformer:
        return FileResponse(transformer.file, as_attachment=True, filename=filename)
Exemple #9
0
def configure_jobs(request, template_name="siaes/configure_jobs.html"):
    """
    Configure an SIAE's jobs.
    """
    siae = get_current_siae_or_404(request)

    if request.method == "POST":

        current_codes = set(
            siae.job_description_through.values_list("appellation__code",
                                                     flat=True))
        submitted_codes = set(request.POST.getlist("code"))

        codes_to_create = submitted_codes - current_codes
        # It is assumed that the codes to delete are not submitted (they must
        # be removed from the DOM via JavaScript). Instead, they are deducted.
        codes_to_delete = current_codes - submitted_codes
        codes_to_update = current_codes - codes_to_delete

        if codes_to_create or codes_to_delete or codes_to_update:

            # Create.
            for code in codes_to_create:
                appellation = Appellation.objects.get(code=code)
                through_defaults = {
                    "custom_name": request.POST.get(f"custom-name-{code}", ""),
                    "description": request.POST.get(f"description-{code}", ""),
                    "is_active": bool(request.POST.get(f"is_active-{code}")),
                }
                siae.jobs.add(appellation, through_defaults=through_defaults)

            # Delete.
            if codes_to_delete:
                appellations = Appellation.objects.filter(
                    code__in=codes_to_delete)
                siae.jobs.remove(*appellations)

            # Update.
            for job_through in siae.job_description_through.filter(
                    appellation__code__in=codes_to_update):
                code = job_through.appellation.code
                new_custom_name = request.POST.get(f"custom-name-{code}", "")
                new_description = request.POST.get(f"description-{code}", "")
                new_is_active = bool(request.POST.get(f"is_active-{code}"))
                if (job_through.custom_name != new_custom_name
                        or job_through.description != new_description
                        or job_through.is_active != new_is_active):
                    job_through.custom_name = new_custom_name
                    job_through.description = new_description
                    job_through.is_active = new_is_active
                    job_through.save()

            messages.success(request, _("Mise à jour effectuée !"))
            return HttpResponseRedirect(reverse_lazy("dashboard:index"))

    context = {"siae": siae}
    return render(request, template_name, context)
Exemple #10
0
def configure_jobs(request, template_name="siaes/configure_jobs.html"):
    """
    Configure an SIAE's jobs.

    Time was limited during the prototyping phase and this view is based on
    JavaScript to generate a dynamic form. No proper Django form is used.
    """
    siae = get_current_siae_or_404(request,
                                   with_job_app_score=True,
                                   with_job_descriptions=True)
    job_descriptions = (siae.job_description_through.select_related(
        "appellation__rome").all().order_by("-updated_at", "-created_at"))
    errors = {}

    if request.method == "POST":
        # Validate data for Siae block_job_applications
        form_siae_block_job_applications = BlockJobApplicationsForm(
            instance=siae, data=request.POST or None)

        if form_siae_block_job_applications.is_valid():
            form_siae_block_job_applications.save()

        refreshed_cards = refresh_card_list(request=request, siae=siae)

        if not refreshed_cards["errors"]:
            with transaction.atomic():
                if refreshed_cards["jobs"]["create"]:
                    SiaeJobDescription.objects.bulk_create(
                        refreshed_cards["jobs"]["create"])

                if refreshed_cards["jobs"]["update"]:
                    SiaeJobDescription.objects.bulk_update(
                        refreshed_cards["jobs"]["update"], [
                            "custom_name", "description", "is_active",
                            "updated_at"
                        ])

                if refreshed_cards["jobs"]["delete"]:
                    siae.jobs.remove(*refreshed_cards["jobs"]["delete"])

                messages.success(request, "Mise à jour effectuée !")
                return HttpResponseRedirect(reverse("dashboard:index"))
        else:
            errors = refreshed_cards["errors"]

    context = {
        "errors": errors,
        "job_descriptions": job_descriptions,
        "siae": siae
    }
    return render(request, template_name, context)
Exemple #11
0
def pe_approval_search(request, template_name="approvals/pe_approval_search.html"):
    """
    Entry point of the `PoleEmploiApproval`'s conversion process which consists of 3 steps
    and allows to convert a `PoleEmploiApproval` into an `Approval`.
    This process is required following the end of the software allowing Pôle emploi to manage
    their approvals.

    Search for a PoleEmploiApproval by number.
    Redirects to the existing Pass if it exists.
    If not, it will ask you to search for an user in order to import the "agrément" as a "PASS IAE".
    """
    approval = None
    form = PoleEmploiApprovalSearchForm(request.GET or None)
    number = None
    siae = get_current_siae_or_404(request)
    back_url = reverse("approvals:pe_approval_search")

    if form.is_valid():
        number = form.cleaned_data["number"]
        approvals_wrapper = ApprovalsWrapper(number=number)
        approval = approvals_wrapper.latest_approval

        if approval:
            if approval.is_pass_iae:
                job_application = approval.user.last_accepted_job_application
                if job_application and job_application.to_siae == siae:
                    # Suspensions and prolongations links are available in the job application details page.
                    application_details_url = reverse(
                        "apply:details_for_siae", kwargs={"job_application_id": job_application.pk}
                    )
                    return HttpResponseRedirect(application_details_url)
                # The employer cannot handle the PASS as it's already used by another one.
                # Suggest him to make a self-prescription. A link is offered in the template.

            context = {
                "approval": approval,
                "back_url": back_url,
                "form": form,
                "number": number,
                "siae": siae,
            }
            return render(request, "approvals/pe_approval_search_found.html", context)

    context = {
        "form": form,
        "number": number,
        "siae": siae,
    }
    return render(request, template_name, context)
Exemple #12
0
    def test_accept_existing_user_is_employer(self):
        self.user = SiaeWith2MembershipsFactory().members.first()
        self.invitation = SiaeSentInvitationFactory(
            sender=self.sender,
            siae=self.siae,
            first_name=self.user.first_name,
            last_name=self.user.last_name,
            email=self.user.email,
        )
        self.client.login(email=self.user.email, password=DEFAULT_PASSWORD)
        self.response = self.client.get(self.invitation.acceptance_link,
                                        follow=True)

        current_siae = get_current_siae_or_404(self.response.wsgi_request)
        self.assertEqual(self.invitation.siae.pk, current_siae.pk)
Exemple #13
0
def edit_siae(request, template_name="siaes/edit_siae.html"):
    """
    Edit an SIAE.
    """
    siae = get_current_siae_or_404(request)

    form = EditSiaeForm(instance=siae, data=request.POST or None)

    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, _("Mise à jour effectuée !"))
        return HttpResponseRedirect(reverse_lazy("dashboard:index"))

    context = {"form": form, "siae": siae}
    return render(request, template_name, context)
Exemple #14
0
def card_search_preview(request,
                        template_name="siaes/includes/_card_siae.html"):
    """
    SIAE's card (or "Fiche" in French) search preview.

    Return only html of card search preview without global template (header, footer, ...)
    Need to recount active jobs to avoid supress and updated count active jobs
    """
    siae = get_current_siae_or_404(request,
                                   with_job_app_score=True,
                                   with_job_descriptions=True)

    form_siae_block_job_applications = BlockJobApplicationsForm(
        instance=siae, data=request.POST or None)
    if form_siae_block_job_applications.is_valid():
        siae = form_siae_block_job_applications.instance

    refreshed_cards = refresh_card_list(request=request, siae=siae)

    if not refreshed_cards["errors"]:
        # sort all the jobs buy updated_at and created_at desc
        list_jobs_descriptions = sorted(
            refreshed_cards["jobs"]["create"] +
            refreshed_cards["jobs"]["update"] +
            refreshed_cards["jobs"]["unmodified"],
            key=lambda x: x.updated_at if x.updated_at else x.created_at,
            reverse=True,
        )

        # count the number of active jobs
        count_active_job_descriptions = 0
        for job in list_jobs_descriptions:
            # int(True) = 1, int(False) = 0
            count_active_job_descriptions += int(job.is_active)

        siae.count_active_job_descriptions = count_active_job_descriptions

        context = {
            "siae": siae,
            "jobs_descriptions": list_jobs_descriptions,
        }

        html = render_to_string(template_name, context)
    else:
        context = {"errors": refreshed_cards["errors"]}
        template_name_errors = "siaes/includes/_alert_configure_job.html"
        html = render_to_string(template_name_errors, context)
    return HttpResponse(html)
Exemple #15
0
def show_financial_annexes(request,
                           template_name="siaes/show_financial_annexes.html"):
    """
    Show a summary of the financial annexes of the convention to the siae admin user. Financial annexes are grouped
    by suffix and only the most relevant one (active if any, or most recent if not) is shown for each suffix.
    """
    current_siae = get_current_siae_or_404(request)
    if not current_siae.convention_can_be_accessed_by(request.user):
        raise PermissionDenied

    financial_annexes = []
    if current_siae.convention:
        financial_annexes = current_siae.convention.financial_annexes.all()

    # For each group of AFs sharing the same number prefix, show only the most relevant AF
    # (active if any, or most recent if not). We do this to avoid showing too many AFs and confusing the user.
    prefix_to_af = {}
    for af in financial_annexes:
        prefix = af.number_prefix
        if prefix not in prefix_to_af or af.is_active:
            # Always show an active AF when there is one.
            prefix_to_af[prefix] = af
            continue
        old_suffix = prefix_to_af[prefix].number_suffix
        new_suffix = af.number_suffix
        if not prefix_to_af[prefix].is_active and new_suffix > old_suffix:
            # Show the AF with the latest suffix when there is no active one.
            prefix_to_af[prefix] = af
            continue

    financial_annexes = list(prefix_to_af.values())
    financial_annexes.sort(key=lambda af: af.number, reverse=True)

    context = {
        "siae":
        current_siae,
        "convention":
        current_siae.convention,
        "financial_annexes":
        financial_annexes,
        "can_select_af":
        current_siae.convention_can_be_changed_by(request.user),
        "current_siae_is_asp":
        current_siae.source == Siae.SOURCE_ASP,
        "current_siae_is_user_created":
        current_siae.source == Siae.SOURCE_USER_CREATED,
    }
    return render(request, template_name, context)
Exemple #16
0
def deactivate_member(request,
                      user_id,
                      template_name="siaes/deactivate_member.html"):
    siae = get_current_siae_or_404(request)
    target_member = User.objects.get(pk=user_id)

    if deactivate_org_member(request=request,
                             target_member=target_member,
                             organization=siae):
        return HttpResponseRedirect(reverse("siaes_views:members"))

    context = {
        "structure": siae,
        "target_member": target_member,
    }

    return render(request, template_name, context)
Exemple #17
0
def members(request, template_name="siaes/members.html"):
    """
    List members of an SIAE.
    """
    siae = get_current_siae_or_404(request)

    members = siae.siaemembership_set.select_related("user").all().order_by(
        "joined_at")
    pending_invitations = siae.invitations.filter(
        accepted=False).all().order_by("sent_at")

    context = {
        "siae": siae,
        "members": members,
        "pending_invitations": pending_invitations
    }
    return render(request, template_name, context)
Exemple #18
0
def declare_prolongation(request, approval_id, template_name="approvals/declare_prolongation.html"):
    """
    Declare a prolongation for the given approval.
    """

    siae = get_current_siae_or_404(request)
    approval = get_object_or_404(Approval, pk=approval_id)

    if not approval.can_be_prolonged_by_siae(siae):
        raise PermissionDenied()

    back_url = get_safe_url(request, "back_url", fallback_url=reverse("dashboard:index"))
    preview = False

    form = DeclareProlongationForm(approval=approval, siae=siae, data=request.POST or None)

    if request.method == "POST" and form.is_valid():

        prolongation = form.save(commit=False)
        prolongation.created_by = request.user
        prolongation.declared_by = request.user
        prolongation.declared_by_siae = form.siae
        prolongation.validated_by = form.validated_by

        if request.POST.get("edit"):
            preview = False
        if request.POST.get("preview"):
            preview = True
        elif request.POST.get("save"):
            prolongation.save()

            if form.cleaned_data.get("email"):
                # Send an email w/o DB changes
                prolongation.notify_authorized_prescriber()

            messages.success(request, "Déclaration de prolongation enregistrée.")
            return HttpResponseRedirect(back_url)

    context = {
        "approval": approval,
        "back_url": back_url,
        "form": form,
        "preview": preview,
    }
    return render(request, template_name, context)
Exemple #19
0
def list_for_siae_exports(request,
                          template_name="apply/list_of_available_exports.html"
                          ):
    """
    List of applications for a SIAE, sorted by month, displaying the count of applications per month
    with the possibiliy to download those applications as a CSV file.
    """

    siae = get_current_siae_or_404(request)
    job_applications = siae.job_applications_received.not_archived()
    job_applications_by_month = job_applications.with_monthly_counts()

    context = {
        "job_applications_by_month": job_applications_by_month,
        "siae": siae,
        "export_for": "siae"
    }
    return render(request, template_name, context)
Exemple #20
0
def stats_siae(request, template_name=_STATS_HTML_TEMPLATE):
    """
    SIAE stats shown to their own members.
    They can only view data for their own SIAE.
    """
    current_org = get_current_siae_or_404(request)
    if not request.user.can_view_stats_siae(current_org=current_org):
        raise PermissionDenied
    params = {SIAE_FILTER_KEY: current_org.convention.asp_id}
    context = {
        "iframeurl":
        metabase_embedded_url(settings.SIAE_STATS_DASHBOARD_ID, params=params),
        "page_title":
        "Données de ma structure",
        "stats_base_url":
        settings.METABASE_SITE_URL,
    }
    return render(request, template_name, context)
Exemple #21
0
def block_job_applications(request,
                           template_name="siaes/block_job_applications.html"):
    """
    Settings: block job applications for given SIAE
    """
    siae = get_current_siae_or_404(request)

    form = BlockJobApplicationsForm(instance=siae, data=request.POST or None)

    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(
            request, _("Mise à jour du blocage des candidatures effectuée !"))
        return HttpResponseRedirect(reverse_lazy("dashboard:index"))

    context = {"siae": siae, "form": form}

    return render(request, template_name, context)
Exemple #22
0
def members(request, template_name="siaes/members.html"):
    """
    List members of an SIAE.
    """
    siae = get_current_siae_or_404(request)
    if not siae.is_active:
        raise PermissionDenied

    members = siae.siaemembership_set.active().select_related(
        "user").all().order_by("joined_at")
    pending_invitations = siae.invitations.pending()

    context = {
        "siae": siae,
        "members": members,
        "pending_invitations": pending_invitations,
    }
    return render(request, template_name, context)
Exemple #23
0
def invite_siae_staff(request, template_name="invitations_views/create.html"):
    siae = get_current_siae_or_404(request)
    form_kwargs = {"sender": request.user, "siae": siae}
    formset = NewSiaeStaffInvitationFormSet(data=request.POST or None,
                                            form_kwargs=form_kwargs)
    if request.POST:
        if formset.is_valid():
            invitations = formset.save()
            count = len(formset.forms)

            message_singular = (
                "Votre invitation a été envoyée par e-mail.<br>"
                "Pour rejoindre votre organisation, l'invité(e) peut désormais cliquer "
                "sur le lien de validation reçu dans le courriel.<br>")

            message_plural = (
                "Vos invitations ont été envoyées par e-mail.<br>"
                "Pour rejoindre votre organisation, vos invités peuvent désormais "
                "cliquer sur le lien de validation reçu dans l'e-mail.<br>")

            message = __(message_singular, message_plural, count) % {
                "count": count
            }
            expiration_date = formats.date_format(
                invitations[0].expiration_date)
            message += _(
                f"Le lien de validation est valable jusqu'au {expiration_date}."
            )
            message = safestring.mark_safe(message)

            messages.success(request, message)
            formset = NewSiaeStaffInvitationFormSet(form_kwargs=form_kwargs)
            return redirect(request.path)

    form_post_url = reverse("invitations_views:invite_siae_staff")
    back_url = reverse("siaes_views:members")
    context = {
        "back_url": back_url,
        "form_post_url": form_post_url,
        "formset": formset
    }

    return render(request, template_name, context)
Exemple #24
0
def list_for_siae_exports_download(request, month_identifier):
    """
    List of applications for a SIAE for a given month identifier (YYYY-mm),
    exported as a CSV file with immediate download
    """
    year, month = month_identifier.split("-")
    siae = get_current_siae_or_404(request)
    job_applications = siae.job_applications_received.not_archived()
    job_applications = job_applications.created_on_given_year_and_month(
        year, month).with_list_related_data()
    filename = f"candidatures-{slugify(siae.display_name)}-{month_identifier}.csv"

    response = HttpResponse(content_type="text/csv", charset="utf-8")
    response["Content-Disposition"] = 'attachment; filename="{}"'.format(
        filename)

    generate_csv_export(job_applications, response)

    return response
Exemple #25
0
def update_admin_role(request,
                      action,
                      user_id,
                      template_name="siaes/update_admins.html"):
    siae = get_current_siae_or_404(request)
    target_member = User.objects.get(pk=user_id)

    if update_org_admin_role(request=request,
                             organization=siae,
                             target_member=target_member,
                             action=action):
        return HttpResponseRedirect(reverse("siaes_views:members"))

    context = {
        "action": action,
        "structure": siae,
        "target_member": target_member,
    }

    return render(request, template_name, context)
Exemple #26
0
def create_siae(request, template_name="siaes/create_siae.html"):
    """
    Create a new SIAE (Agence / Etablissement in French).
    """
    current_siae = get_current_siae_or_404(request)
    form = CreateSiaeForm(
        current_siae=current_siae,
        current_user=request.user,
        data=request.POST or None,
        initial={"siret": current_siae.siret},
    )

    if request.method == "POST" and form.is_valid():
        siae = form.save(request)
        request.session[settings.ITOU_SESSION_CURRENT_SIAE_KEY] = siae.pk
        messages.success(request,
                         _(f"Vous travaillez sur {siae.display_name}"))
        return HttpResponseRedirect(reverse_lazy("dashboard:index"))

    context = {"form": form}
    return render(request, template_name, context)
Exemple #27
0
def select_financial_annex(request,
                           template_name="siaes/select_financial_annex.html"):
    """
    Let siae admin user select a new convention via a financial annex number.
    """
    current_siae = get_current_siae_or_404(request)
    if not current_siae.convention_can_be_changed_by(request.user):
        raise PermissionDenied

    # We only allow the user to select an AF under the same SIREN as the current siae.
    financial_annexes = (
        SiaeFinancialAnnex.objects.select_related("convention").filter(
            convention__kind=current_siae.kind,
            convention__siret_signature__startswith=current_siae.siren).
        order_by("-number"))

    # Show only one AF for each AF number prefix to significantly reduce the length of the dropdown when there are
    # many AFs in the same SIREN.
    prefix_to_af = {af.number_prefix: af for af in financial_annexes.all()}
    # The form expects a queryset and not a list.
    financial_annexes = financial_annexes.filter(
        pk__in=[af.pk for af in prefix_to_af.values()])

    select_form = FinancialAnnexSelectForm(data=request.POST or None,
                                           financial_annexes=financial_annexes)

    if request.method == "POST" and select_form.is_valid():
        financial_annex = select_form.cleaned_data["financial_annexes"]
        current_siae.convention = financial_annex.convention
        current_siae.save()
        message = (
            f"Nous avons bien attaché votre structure à l'annexe financière"
            f" {financial_annex.number_prefix_with_spaces}.")
        messages.success(request, message)
        return HttpResponseRedirect(
            reverse("siaes_views:show_financial_annexes"))

    context = {"select_form": select_form}
    return render(request, template_name, context)
Exemple #28
0
def suspension_delete(request, suspension_id, template_name="approvals/suspension_delete.html"):
    """
    Delete the given suspension.
    """

    siae = get_current_siae_or_404(request)
    suspension = get_object_or_404(Suspension, pk=suspension_id)

    if not suspension.can_be_handled_by_siae(siae):
        raise PermissionDenied()

    back_url = get_safe_url(request, "back_url", fallback_url=reverse("dashboard:index"))

    if request.method == "POST" and request.POST.get("confirm") == "true":
        suspension.delete()
        messages.success(request, "Annulation de suspension effectuée.")
        return HttpResponseRedirect(back_url)

    context = {
        "suspension": suspension,
        "back_url": back_url,
    }
    return render(request, template_name, context)
Exemple #29
0
def suspend(request, approval_id, template_name="approvals/suspend.html"):
    """
    Suspend the given approval.
    """

    siae = get_current_siae_or_404(request)
    approval = get_object_or_404(Approval, pk=approval_id)

    if not approval.can_be_suspended_by_siae(siae):
        raise PermissionDenied()

    back_url = get_safe_url(request, "back_url", fallback_url=reverse("dashboard:index"))
    preview = False

    form = SuspensionForm(approval=approval, siae=siae, data=request.POST or None)

    if request.method == "POST" and form.is_valid():

        suspension = form.save(commit=False)
        suspension.created_by = request.user

        if request.POST.get("edit"):
            preview = False
        if request.POST.get("preview"):
            preview = True
        elif request.POST.get("save"):
            suspension.save()
            messages.success(request, "Suspension effectuée.")
            return HttpResponseRedirect(back_url)

    context = {
        "approval": approval,
        "back_url": back_url,
        "form": form,
        "preview": preview,
    }
    return render(request, template_name, context)
Exemple #30
0
    def test_accept_existing_user_already_member_of_inactive_siae(self):
        """
        An inactive SIAE user (i.e. attached to a single inactive SIAE)
        can only be ressucitated by being invited to a new SIAE.
        We test here that this is indeed possible.
        """
        siae = SiaeWithMembershipFactory()
        sender = siae.members.first()
        user = SiaeWithMembershipFactory(
            convention__is_active=False).members.first()
        invitation = SentSiaeStaffInvitationFactory(
            sender=sender,
            siae=siae,
            first_name=user.first_name,
            last_name=user.last_name,
            email=user.email,
        )
        self.client.login(email=user.email, password=DEFAULT_PASSWORD)
        response = self.client.get(invitation.acceptance_link, follow=True)
        self.assertRedirects(response, reverse("dashboard:index"))

        current_siae = get_current_siae_or_404(response.wsgi_request)
        self.assertEqual(siae.pk, current_siae.pk)
        self.assert_accepted_invitation(invitation, user)