Skip to content

Commit 0339754

Browse files
committed
Merge branch 'develop'
2 parents a89d801 + ed84f57 commit 0339754

37 files changed

Lines changed: 841 additions & 298 deletions

File tree

frontend_tests/cypress/e2e/project/tasks/canPublishTaskWithRessource.cy.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
describe('I can attach miscellanious ressource to task @page-projet-recommandations-creation', () => {
2-
beforeEach(() => {
3-
cy.login('conseiller1');
4-
});
5-
62
it('publishes a task with resource comment / no comment', () => {
3+
cy.login('conseiller1');
74
cy.visit(`/projects/action/?project_id=25`);
85

96
cy.get('[data-cy="radio-push-reco-single-resource"]').should('be.checked');
@@ -25,7 +22,23 @@ describe('I can attach miscellanious ressource to task @page-projet-recommandati
2522
cy.url().should('include', '/actions');
2623
});
2724

25+
it('cannot select a draft resource and see warning', () => {
26+
cy.login('staff');
27+
cy.visit(`/projects/action/?project_id=25`);
28+
29+
cy.get('[data-cy="radio-push-reco-single-resource"]').should('be.checked');
30+
31+
cy.get('[data-test-id="search-resource-input"]').type('brouillon', {
32+
delay: 0,
33+
});
34+
cy.get('[data-cy="resource-warning-status-draft"]').should('be.visible');
35+
cy.get('[data-cy="radio-resource-list-task"]')
36+
.first()
37+
.should('be.disabled');
38+
});
39+
2840
it('publishes a task with external resource', () => {
41+
cy.login('conseiller1');
2942
cy.visit(`/projects/action/?project_id=25`);
3043

3144
cy.get('[data-cy="radio-push-reco-external-resource"]')
@@ -49,6 +62,7 @@ describe('I can attach miscellanious ressource to task @page-projet-recommandati
4962
});
5063

5164
it('publishes a task with no resource', () => {
65+
cy.login('conseiller1');
5266
cy.visit(`/projects/action/?project_id=25`);
5367

5468
cy.get('[data-cy="radio-push-reco-no-resource"]')
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 5.1.11 on 2025-08-25 13:25
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("invites", "0006_alter_invite_project"),
10+
("projects", "0111_delete_perm_use_project_tags"),
11+
("sites", "0002_alter_domain_unique"),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name="invite",
18+
name="refused_on",
19+
field=models.DateTimeField(
20+
blank=True,
21+
default=None,
22+
editable=False,
23+
null=True,
24+
verbose_name="date de refus",
25+
),
26+
),
27+
migrations.AddConstraint(
28+
model_name="invite",
29+
constraint=models.CheckConstraint(
30+
condition=models.Q(
31+
models.Q(("accepted_on", None), ("refused_on", None)),
32+
models.Q(
33+
("accepted_on", None),
34+
models.Q(("refused_on", None), _negated=True),
35+
),
36+
models.Q(
37+
models.Q(("accepted_on", None), _negated=True),
38+
("refused_on", None),
39+
),
40+
_connector="OR",
41+
),
42+
name="invites_invite_accepted_or_refused",
43+
),
44+
),
45+
]

recoco/apps/invites/models.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.sites.managers import CurrentSiteManager
55
from django.contrib.sites.models import Site
66
from django.db import models
7+
from django.db.models import Q
78
from django.urls import reverse
89
from django.utils import timezone
910

@@ -12,7 +13,7 @@
1213

1314
class InviteManager(models.Manager):
1415
def pending(self):
15-
return self.filter(accepted_on=None)
16+
return self.filter(accepted_on=None, refused_on=None)
1617

1718

1819
class InviteOnSiteManager(CurrentSiteManager, InviteManager):
@@ -22,6 +23,18 @@ class InviteOnSiteManager(CurrentSiteManager, InviteManager):
2223
class Invite(models.Model):
2324
"""Invitation for a project"""
2425

26+
class Meta:
27+
constraints = [
28+
models.CheckConstraint(
29+
name="%(app_label)s_%(class)s_accepted_or_refused",
30+
condition=(
31+
Q(accepted_on=None, refused_on=None)
32+
| Q(Q(accepted_on=None) & ~Q(refused_on=None))
33+
| Q(~Q(accepted_on=None) & Q(refused_on=None))
34+
),
35+
)
36+
]
37+
2538
INVITE_ROLES = (
2639
("COLLABORATOR", "Participant·e"),
2740
("SWITCHTENDER", "Conseiller·e"),
@@ -46,6 +59,14 @@ class Invite(models.Model):
4659
editable=False,
4760
verbose_name="date d'acceptation",
4861
)
62+
refused_on = models.DateTimeField(
63+
default=None,
64+
null=True,
65+
blank=True,
66+
editable=False,
67+
verbose_name="date de refus",
68+
)
69+
4970
email = models.EmailField(max_length=254)
5071

5172
inviter = models.ForeignKey(
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{% extends "account/layout/layout.html" %}
2+
{% block layout_content %}
3+
<div>
4+
<p class="fr-mb-4w">
5+
Vous avez été invité·e par {{ invite.inviter.get_full_name }} à rejoindre un dossier sur {{ request.site.name }} <strong>en tant que {{ invite.get_role_display }}</strong> :
6+
</p>
7+
<div class="project-card project-card__border--highlight fr-mb-4w">
8+
<div class="project-card__content">
9+
<div class="project__title">
10+
<span class="project-card__project-name">{{ invite.project.name }}</span>
11+
<span class="project-card__project-org-name">{{ invite.project.org_name }}</span>
12+
</div>
13+
<div class="project__info">
14+
{% if invite.project.project_sites.origin.site.configuration.logo_small %}
15+
<img src="{{ invite.project.project_sites.origin.site.configuration.logo_small.url }}"
16+
width="16px"
17+
height="auto"
18+
alt="Logo {{ invite.project.project_sites.origin.site.configuration.name }}" />
19+
{% else %}
20+
<span class="fr-icon--sm fr-icon-window-line" aria-hidden="true"></span>
21+
{% endif %}
22+
<span class="project-card__project-site fr-mr-1w">provient de {{ invite.project.project_sites.origin.site.name }}</span>
23+
<span class="project-card__project-insee fr-icon--sm fr-icon-map-pin-2-line fr-mr-1w">{{ invite.project.commune.name }} ({{ invite.project.commune.insee }})</span>
24+
{% if invite.project.owner %}
25+
<span class="project-card__project-org fr-icon--sm fr-icon-parent-line fr-mr-1w">{{ invite.project.owner.profile.organization.name }}</span>
26+
{% endif %}
27+
<span class="project-card__project-date fr-icon--sm fr-icon-calendar-event-line fr-mr-1w">déposé depuis {{ invite.project.created_on|timesince }}</span>
28+
</div>
29+
<div class="project__info">
30+
<div class="project-card__project-tags">
31+
<span class="icon fr-icon-tag-line fr-icon--sm" aria-hidden="true"></span>
32+
{% for tag in invite.project.tags.all %}<span class="project-card__project-tag">#{{ tag }}</span>{% endfor %}
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
<p class="fr-mb-3v">Message d'invitation :</p>
38+
{% if invite.message %}
39+
<div class="bg-light fr-p-2w fr-mb-4w">
40+
<p>{{ invite.message }}</p>
41+
{% include "user/user_card.html" with user=invite.inviter %}
42+
</div>
43+
{% endif %}
44+
<form class="form fr-mt-3w"
45+
action="{% url 'invites-invite-accept' invite.pk %}"
46+
method="post">
47+
{% csrf_token %}
48+
{% if not existing_account %}
49+
<hr class="fr-hr">
50+
<h3>Créez votre compte {{ request.site.name }}</h3>
51+
<div class="fr-input-group fr-my-1w">
52+
<label class="fr-label" for="email">Adresse courriel</label>
53+
<input type="text"
54+
class="fr-input"
55+
id="email"
56+
value="{{ invite.email }}"
57+
disabled>
58+
</div>
59+
<div class="fr-grid-row fr-grid-row--gutters fr-mt-1w">
60+
<div class="fr-col-6">
61+
<div class="fr-input-group">
62+
<label class="fr-label" for="input-first-name">Prénom</label>
63+
<input type="text"
64+
class="fr-input {% if form.first_name.errors %}is-invalid{% endif %}"
65+
id="input-first-name"
66+
name="{{ form.first_name.name }}"
67+
placeholder="Camille"
68+
value="{{ form.first_name.value|default:'' }}"
69+
required>
70+
{% for error in form.first_name.errors %}<div class="text-danger text-end">{{ error }}</div>{% endfor %}
71+
</div>
72+
</div>
73+
<div class="fr-col-6">
74+
<div class="fr-input-group">
75+
<label class="fr-label" for="input-last-name">Nom</label>
76+
<input type="text"
77+
class="fr-input {% if form.last_name.errors %}is-invalid{% endif %}"
78+
id="input-last-name"
79+
name="{{ form.last_name.name }}"
80+
placeholder="Dupont"
81+
value="{{ form.last_name.value|default:'' }}"
82+
required>
83+
{% for error in form.last_name.errors %}<div class="text-danger text-end">{{ error }}</div>{% endfor %}
84+
</div>
85+
</div>
86+
</div>
87+
<div class="fr-grid-row fr-grid-row--gutters fr-mt-1w">
88+
<div class="fr-col-6">
89+
<div class="fr-input-group">
90+
<label class="fr-label" for="input-organization">Organisation</label>
91+
<input type="text"
92+
class="fr-input {% if form.organization.errors %}is-invalid{% endif %}"
93+
id="input-organization"
94+
name="{{ form.organization.name }}"
95+
placeholder="DEFR59"
96+
value="{{ form.organization.value|default:'' }}"
97+
required>
98+
{% for error in form.organization.errors %}<div class="text-danger text-end">{{ error }}</div>{% endfor %}
99+
</div>
100+
</div>
101+
<div class="fr-col-6">
102+
<div class="fr-input-group">
103+
<label class="fr-label" for="input-position">Fonction</label>
104+
<input type="text"
105+
class="fr-input {% if form.position.errors %}is-invalid{% endif %}"
106+
id="input-position"
107+
name="{{ form.position.name }}"
108+
placeholder="Chargée de mission"
109+
value="{{ form.position.value|default:'' }}"
110+
required>
111+
{% for error in form.position.errors %}<div class="text-danger text-end">{{ error }}</div>{% endfor %}
112+
</div>
113+
</div>
114+
</div>
115+
<ul class="fr-btns-group fr-btns-group--inline-sm fr-mt-4w">
116+
<li class="w-50">
117+
{% comment %} TODO: add url/action from backend {% endcomment %}
118+
<button class="fr-btn fr-btn--tertiary-no-outline w-100 align-self-center"
119+
type="submit"
120+
form="refuse-form">Refuser</button>
121+
</li>
122+
<li class="w-50">
123+
<button class="fr-btn fr-btn--primary w-100 align-self-center" type="submit">Rejoindre le dossier</button>
124+
</li>
125+
</ul>
126+
{% endif %}
127+
</form>
128+
<form action="{% url 'invites-invite-refuse' invite.pk %}"
129+
method="post"
130+
id="refuse-form">
131+
{% csrf_token %}
132+
</form>
133+
</div>
134+
{% endblock layout_content %}
135+
{% block layout_secondary_content %}
136+
<h3 class="fw-bolder">Qu'est ce que {{ request.site.name }} ?</h3>
137+
{% include "invites/invite_site_message.html" %}
138+
<p>
139+
<a class="fr-link" href="{% url 'home' %}">En savoir plus</a>
140+
</p>
141+
{% endblock layout_secondary_content %}

0 commit comments

Comments
 (0)