Skip to content

Commit 40d6bde

Browse files
committed
Fix token for invite link, updated accpet/reject invite functionality
1 parent e420942 commit 40d6bde

11 files changed

Lines changed: 281 additions & 33 deletions

File tree

hypha/apply/activity/adapters/emails.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,25 @@ def handle_transition(self, old_phase, source, **kwargs):
169169
)
170170

171171
def handle_co_applicant_invite(self, source, related, **kwargs):
172+
from hypha.apply.funds.utils import generate_signed_token
173+
172174
invited_user = User.objects.filter(email=related.invited_user_email).first()
173175
can_accept = True
174176
if invited_user and (
175177
invited_user.is_apply_staff or invited_user.is_apply_staff_admin
176178
):
177179
can_accept = False
178180

181+
token = generate_signed_token(
182+
data={
183+
"email": related.invited_user_email,
184+
"submission": related.submission.pk,
185+
},
186+
salt="co-applicant-invite-token",
187+
)
179188
accept_link = reverse(
180189
"apply:submissions:accept_coapplicant_invite",
181-
kwargs={"pk": source.id, "token": related.token},
190+
kwargs={"pk": source.id, "token": token},
182191
)
183192
return self.render_message(
184193
"messages/email/invite_co_applicant.html",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Generated by Django 4.2.20 on 2025-04-30 05:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("activity", "0086_remove_django_messages_adapter"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="event",
14+
name="type",
15+
field=models.CharField(
16+
choices=[
17+
("UPDATE_LEAD", "updated lead"),
18+
("BATCH_UPDATE_LEAD", "batch updated lead"),
19+
("EDIT_SUBMISSION", "edited submission"),
20+
("APPLICANT_EDIT", "edited applicant"),
21+
("NEW_SUBMISSION", "submitted new submission"),
22+
("DRAFT_SUBMISSION", "submitted new draft submission"),
23+
("SCREENING", "screened"),
24+
("TRANSITION", "transitioned"),
25+
("BATCH_TRANSITION", "batch transitioned"),
26+
("DETERMINATION_OUTCOME", "sent determination outcome"),
27+
("BATCH_DETERMINATION_OUTCOME", "sent batch determination outcome"),
28+
("INVITED_TO_PROPOSAL", "invited to proposal"),
29+
("REVIEWERS_UPDATED", "updated reviewers"),
30+
("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"),
31+
("PARTNERS_UPDATED", "updated partners"),
32+
("PARTNERS_UPDATED_PARTNER", "partners updated partner"),
33+
("READY_FOR_REVIEW", "marked ready for review"),
34+
("BATCH_READY_FOR_REVIEW", "marked batch ready for review"),
35+
("NEW_REVIEW", "added new review"),
36+
("COMMENT", "added comment"),
37+
("PROPOSAL_SUBMITTED", "submitted proposal"),
38+
("OPENED_SEALED", "opened sealed submission"),
39+
("REVIEW_OPINION", "reviewed opinion"),
40+
("DELETE_SUBMISSION", "deleted submission"),
41+
("DELETE_REVIEW", "deleted review"),
42+
("DELETE_REVIEW_OPINION", "deleted review opinion"),
43+
("CREATED_PROJECT", "created project"),
44+
("UPDATE_PROJECT_LEAD", "updated project lead"),
45+
("UPDATE_PROJECT_TITLE", "updated project title"),
46+
("EDIT_REVIEW", "edited review"),
47+
("SEND_FOR_APPROVAL", "sent for approval"),
48+
("APPROVE_PROJECT", "approved project"),
49+
("ASSIGN_PAF_APPROVER", "assign project form approver"),
50+
("APPROVE_PAF", "approved project form"),
51+
("PROJECT_TRANSITION", "transitioned project"),
52+
("REQUEST_PROJECT_CHANGE", "requested project change"),
53+
("SUBMIT_CONTRACT_DOCUMENTS", "submitted contract documents"),
54+
("UPLOAD_DOCUMENT", "uploaded document to project"),
55+
("UPLOAD_CONTRACT", "uploaded contract to project"),
56+
("APPROVE_CONTRACT", "approved contract"),
57+
("CREATE_INVOICE", "created invoice for project"),
58+
("UPDATE_INVOICE_STATUS", "updated invoice status"),
59+
("APPROVE_INVOICE", "approve invoice"),
60+
("DELETE_INVOICE", "deleted invoice"),
61+
("SENT_TO_COMPLIANCE", "sent project to compliance"),
62+
("UPDATE_INVOICE", "updated invoice"),
63+
("SUBMIT_REPORT", "submitted report"),
64+
("SKIPPED_REPORT", "skipped report"),
65+
("REPORT_FREQUENCY_CHANGED", "changed report frequency"),
66+
("DISABLED_REPORTING", "disabled reporting"),
67+
("REPORT_NOTIFY", "notified report"),
68+
("REVIEW_REMINDER", "reminder to review"),
69+
("BATCH_DELETE_SUBMISSION", "batch deleted submissions"),
70+
("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"),
71+
("BATCH_INVOICE_STATUS_UPDATE", "batch update invoice status"),
72+
("STAFF_ACCOUNT_CREATED", "created new account"),
73+
("STAFF_ACCOUNT_EDITED", "edited account"),
74+
("ARCHIVE_SUBMISSION", "archived submission"),
75+
("UNARCHIVE_SUBMISSION", "unarchived submission"),
76+
("REMOVE_TASK", "remove task"),
77+
("INVITE_COAPPLICANT", "invite co-applicant"),
78+
],
79+
max_length=50,
80+
verbose_name="verb",
81+
),
82+
),
83+
]

hypha/apply/activity/templates/messages/email/invite_co_applicant.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
{% trans "But You can't accept this invite because you already hold a responsible position in" %} {{ ORG_SHORT_NAME }}
88
{% endif %}
99
{% blocktrans %} Click on link if you want to accept it, otherwise leave it.{% endblocktrans %}
10-
{% trans "Link" %}: {{ accept_link }}
10+
{% trans "Link" %}: {{ request.scheme }}://{{ request.get_host }}{{ accept_link }}
1111
{% endblock %}{# fmt:on #}

hypha/apply/funds/forms.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import uuid
21
from collections import OrderedDict
32
from functools import partial
43
from itertools import groupby
@@ -478,9 +477,3 @@ def __init__(self, *args, submission, user=None, **kwargs):
478477
class Meta:
479478
model = CoApplicantInvite
480479
fields = ["invited_user_email", "submission"]
481-
482-
def save(self, *args, **kwargs):
483-
self.instance.submission = self.cleaned_data["submission"]
484-
self.instance.token = str(uuid.uuid4())
485-
self.instance.invited_user_email = self.cleaned_data["invited_user_email"]
486-
return super().save(*args, **kwargs)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 4.2.20 on 2025-04-30 05:36
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("funds", "0124_coapplicantinvite_coapplicant"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveField(
13+
model_name="coapplicant",
14+
name="accepted_on",
15+
),
16+
migrations.RemoveField(
17+
model_name="coapplicant",
18+
name="status",
19+
),
20+
migrations.RemoveField(
21+
model_name="coapplicantinvite",
22+
name="is_used",
23+
),
24+
migrations.RemoveField(
25+
model_name="coapplicantinvite",
26+
name="token",
27+
),
28+
migrations.AddField(
29+
model_name="coapplicant",
30+
name="created_at",
31+
field=models.DateTimeField(auto_now_add=True, null=True),
32+
),
33+
migrations.AddField(
34+
model_name="coapplicantinvite",
35+
name="responded_on",
36+
field=models.DateTimeField(blank=True, null=True),
37+
),
38+
migrations.AddField(
39+
model_name="coapplicantinvite",
40+
name="status",
41+
field=models.CharField(
42+
choices=[
43+
("pending", "Pending"),
44+
("accepted", "Accepted"),
45+
("rejected", "Rejected"),
46+
("expired", "Expired"),
47+
],
48+
default="pending",
49+
max_length=20,
50+
),
51+
),
52+
]

hypha/apply/funds/models/co_applicants.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313

1414
class CoApplicantInviteStatus(models.TextChoices):
15+
PENDING = "pending", "Pending"
1516
ACCEPTED = "accepted", "Accepted"
1617
REJECTED = "rejected", "Rejected"
1718
EXPIRED = "expired", "Expired"
@@ -24,15 +25,19 @@ class CoApplicantInvite(models.Model):
2425
related_name="co_applicant_invites",
2526
)
2627
invited_user_email = models.EmailField()
27-
token = models.CharField(max_length=256, unique=True)
28-
is_used = models.BooleanField(default=False)
2928
invited_by = models.ForeignKey(
3029
User,
3130
on_delete=models.SET_NULL,
3231
null=True,
3332
blank=True,
3433
related_name="co_applicant_invites",
3534
)
35+
status = models.CharField(
36+
max_length=20,
37+
choices=CoApplicantInviteStatus.choices,
38+
default=CoApplicantInviteStatus.PENDING,
39+
)
40+
responded_on = models.DateTimeField(blank=True, null=True)
3641
created_at = models.DateTimeField(auto_now_add=True)
3742

3843
class Meta:
@@ -55,12 +60,7 @@ class CoApplicant(models.Model):
5560
CoApplicantInvite, on_delete=models.CASCADE, related_name="co_applicant"
5661
)
5762
role = models.JSONField(default=list)
58-
status = models.CharField(
59-
max_length=20,
60-
choices=CoApplicantInviteStatus.choices,
61-
default=CoApplicantInviteStatus.ACCEPTED,
62-
)
63-
accepted_on = models.DateTimeField(null=True, blank=True)
63+
created_at = models.DateTimeField(auto_now_add=True, null=True)
6464

6565
class Meta:
6666
unique_together = ("submission", "user")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% extends 'base-apply.html' %}
2+
3+
{% block user_menu %}
4+
{% endblock user_menu %}
5+
6+
{% block content %}
7+
<div class="bg-gray-100 max-w-[70%] mx-auto mt-6 px-10 pt-6 pb-4 border border-gray-300 rounded-sm">
8+
You are invited as a co-applicant to submission "{{ submission.title }}", Please respond to the invitation by accept or reject the invite.
9+
<br>
10+
You will be auto signup/login to system and redirected to the submission on accept the invite. We strongly recommend you to update password after accept the invite. You may also update the email.
11+
<div class="flex justify-end mt-10">
12+
<a
13+
class="button button--submit button--white"
14+
hx-post="."
15+
hx-vals='{"action": "reject"}'
16+
hx-trigger="click"
17+
hx-swap="none"
18+
>Reject</a>
19+
<a
20+
class="button button--submit button--primary"
21+
type="button"
22+
href="{% url 'apply:submissions:invite_co_applicant' pk=submission.id %}"
23+
hx-post="."
24+
hx-vals='{"action": "accept"}'
25+
hx-trigger="click"
26+
hx-swap="none"
27+
>Accept </a>
28+
</div>
29+
</div>
30+
31+
{% endblock %}

hypha/apply/funds/templates/funds/includes/generic_primary_actions.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load i18n primaryactions_tags %}
1+
{% load i18n primaryactions_tags heroicons %}
22

33
{% if request.user|should_display_primary_actions_block:object %}
44
<div class="sidebar__inner sidebar__inner--light-blue sidebar__inner--actions" data-testid="sidebar-primary-actions">
@@ -27,13 +27,23 @@ <h5>{% trans "Actions to take" %}</h5>
2727
<div class="sidebar__inner sidebar__inner--light-blue sidebar__inner--actions" data-testid="sidebar-primary-actions">
2828
<h5>{% trans "Manage Co-applicants" %}</h5>
2929
{% for invite in object.co_applicant_invites.all %}
30-
<p> {{ invite.invited_user_email }}</p>
30+
<p>
31+
<span title="{{ invite.get_status_display }}">
32+
{% if invite.status == "pending" %}
33+
{% heroicon_solid "exclamation-triangle" size=20 class="inline align-text-bottom me-1 fill-yellow-400" aria_hidden=true %}
34+
{% elif invite.status == "accepted" %}
35+
{% heroicon_solid "check-circle" size=20 class="inline align-text-bottom me-1 fill-green-400" aria_hidden=true %}
36+
{% elif invite.status == "rejected" %}
37+
{% heroicon_solid "x-circle" size=20 class="inline align-text-bottom me-1 fill-red-400" aria_hidden=true %}
38+
{% endif %}
39+
</span>
40+
{{ invite.invited_user_email }}</p>
3141
{% endfor %}
3242
<button
3343
class="button button--primary button--full-width button--bottom-space"
3444
hx-get="{% url 'apply:submissions:invite_co_applicant' pk=object.id %}"
3545
hx-target="#htmx-modal"
36-
{% if object.co_applicant_invites.count >= 5 %}disabled{% endif %}
46+
{% if object.co_applicant_invites.count >= 10 %}disabled{% endif %}
3747
role="button"
3848
aria-label="{% trans "Invite CoApplicant" %}"
3949
>{% trans "Invite Co-applicant" %}</button>

hypha/apply/funds/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from operator import iconcat
88

99
import django_filters as filters
10+
from django.core import signing
1011
from django.utils.html import strip_tags
1112
from django.utils.translation import gettext as _
1213

@@ -223,3 +224,18 @@ def check_submissions_same_determination_form(submissions):
223224
if any(d_id != determination_form_ids[0] for d_id in determination_form_ids):
224225
same_form = False
225226
return same_form
227+
228+
229+
def generate_signed_token(data, salt):
230+
token = signing.dumps(data, salt=salt)
231+
return token
232+
233+
234+
def verify_signed_token(token, salt, max_age=86400): # default max_age 1 day in sec
235+
try:
236+
data = signing.loads(token, salt=salt, max_age=max_age)
237+
return data
238+
except signing.BadSignature:
239+
return None # invalid token
240+
except signing.SignatureExpired:
241+
return None # expired token

0 commit comments

Comments
 (0)