Skip to content

Commit a0729a9

Browse files
Merge pull request #248 from RSE-Sheffield/feat/21-manage-org-members
Organisation member management
2 parents 8977b80 + dcfb4cd commit a0729a9

23 files changed

+560
-128
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# SORT
33
### Self-Assessment of Organisational Readiness Tool
44

5-
65
The SORT provides a comprehensive self-assessment framework, enabling organisations to evaluate and strengthen their research capabilities within nursing and
76
broader health and care practices. By guiding you through forty-four targeted statements, SORT helps assess your current level of research maturity
87
and the support available for nurses involved in research. Upon completion, your organisation will be equipped to create a tailored improvement plan to better
@@ -70,6 +69,7 @@ DJANGO_SECRET_KEY=your_secret_key
7069
DJANGO_DEBUG=True
7170
DJANGO_ALLOWED_HOSTS=127.0.0.1 localhost
7271
DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
72+
DJANGO_LOG_LEVEL=DEBUG
7373
```
7474

7575
---

SORT/settings.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ def cast_to_boolean(obj: Any) -> bool:
6060
"django.contrib.sessions",
6161
"django.contrib.messages",
6262
"django.contrib.staticfiles",
63-
# Plugins
63+
# Plugin apps
6464
"django_bootstrap5",
6565
"django_extensions",
6666
"qr_code",
6767
"crispy_forms",
6868
"crispy_bootstrap5",
69+
"invitations",
6970
# SORT apps
7071
"home",
7172
"survey",
@@ -260,3 +261,9 @@ def cast_to_boolean(obj: Any) -> bool:
260261
# https://django-crispy-forms.readthedocs.io/en/latest/install.html
261262
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
262263
CRISPY_TEMPLATE_PACK = "bootstrap5"
264+
265+
# Invitations options
266+
# https://django-invitations.readthedocs.io/en/latest/configuration.html
267+
INVITATIONS_SIGNUP_REDIRECT = "signup"
268+
INVITATIONS_CONFIRMATION_URL_NAME = "member_invite_accept"
269+
INVITATIONS_EMAIL_SUBJECT_PREFIX = "SORT"

docs/invitations.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Invitations
2+
3+
https://django-invitations.readthedocs.io/
4+
5+
# Usage
6+
7+
## Clear invitations
8+
9+
[Invitations management command](https://django-invitations.readthedocs.io/en/latest/usage.html#management-commands)
10+
11+
```bash
12+
python manage.py clear_expired_invitations
13+
```
14+

docs/templates.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Icons
2+
3+
The [Boxicons](https://boxicons.com/) website has a gallery of available icons that can be implemented using the following HTML code:
4+
5+
```html
6+
<i class='bx bxs-envelope'></i>
7+
```
8+

home/forms.py

Lines changed: 0 additions & 74 deletions
This file was deleted.

home/forms/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .manager_signup import ManagerSignupForm
2+
from .manager_login import ManagerLoginForm
3+
from .search_bar import SearchBarForm
4+
from .user_profile import UserProfileForm
5+
6+
__all__ = ["ManagerSignupForm", "ManagerLoginForm", "SearchBarForm", "UserProfileForm"]

home/forms/manager_login.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import django.forms as forms
2+
from django.contrib.auth.forms import AuthenticationForm
3+
4+
5+
class ManagerLoginForm(AuthenticationForm):
6+
username = forms.EmailField(
7+
label="Email", max_length=60, widget=forms.EmailInput(attrs={"autofocus": True})
8+
)

home/forms/manager_signup.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
New manager registration form
3+
"""
4+
5+
import django.contrib.auth.models
6+
import django.forms as forms
7+
from django.contrib.auth.forms import UserCreationForm
8+
from invitations.models import Invitation
9+
10+
from home.services import organisation_service
11+
from home.models import Organisation
12+
from home.constants import ROLE_PROJECT_MANAGER
13+
14+
User = django.contrib.auth.get_user_model()
15+
16+
17+
class ManagerSignupForm(UserCreationForm):
18+
"""
19+
A form to register a new user (a new manager) who was invited by
20+
an existing manager of an organisation.
21+
"""
22+
23+
class Meta:
24+
model = User
25+
fields = ("password1", "password2")
26+
27+
# Secret key for the invitation (hidden form field)
28+
key = forms.CharField(required=False, disabled=True, widget=forms.HiddenInput, label="")
29+
30+
@property
31+
def invitation(self) -> Invitation:
32+
"""
33+
The invitation that the existing manager send to the new user.
34+
"""
35+
return Invitation.objects.get(key=self.data["key"])
36+
37+
@property
38+
def inviter(self) -> User:
39+
"""
40+
The user (manager) who invited this new manager.
41+
"""
42+
return User.objects.get(pk=self.invitation.inviter_id)
43+
44+
@property
45+
def organisation(self) -> Organisation:
46+
"""
47+
The organisation that the new user was invited to join.
48+
"""
49+
organisation = organisation_service.get_user_organisation(user=self.inviter)
50+
if organisation is None:
51+
raise forms.ValidationError("This user is not a manager of an organisation")
52+
return organisation
53+
54+
@property
55+
def email(self) -> str:
56+
"""
57+
The email address of the new user that received the invitation email.
58+
"""
59+
return self.invitation.email
60+
61+
def save(self, commit=True):
62+
user = super().save(commit=False)
63+
user.email = self.email
64+
user.username = user.email
65+
if commit:
66+
user.save()
67+
68+
# Add user to organisation
69+
organisation_service.add_user_to_organisation(
70+
user_to_add=user,
71+
organisation=self.organisation,
72+
user=self.inviter,
73+
role=ROLE_PROJECT_MANAGER,
74+
)
75+
76+
return user

home/forms/search_bar.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import django.forms as forms
2+
3+
4+
class SearchBarForm(forms.Form):
5+
q = forms.CharField(
6+
required=False,
7+
widget=forms.TextInput(
8+
attrs={
9+
"class": "form-control",
10+
"placeholder": "Search...",
11+
"aria-label": "Search",
12+
"name": "q",
13+
}
14+
),
15+
)

home/forms/user_profile.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import django.contrib.auth
2+
import django.forms as forms
3+
4+
User = django.contrib.auth.get_user_model()
5+
6+
7+
class UserProfileForm(forms.ModelForm):
8+
class Meta:
9+
model = User
10+
fields = ["email", "first_name", "last_name"]
11+
12+
def clean_email(self):
13+
email = self.cleaned_data.get("email")
14+
15+
if email == self.instance.email:
16+
return email
17+
18+
if User.objects.exclude(pk=self.instance.pk).filter(email=email).exists():
19+
raise forms.ValidationError("This email is already in use.")
20+
return email
21+
22+
def save(self, commit=True):
23+
user = super().save(commit=False)
24+
if self.cleaned_data.get("password"):
25+
user.set_password(self.cleaned_data["password"])
26+
if commit:
27+
user.save()
28+
return user

0 commit comments

Comments
 (0)