From 1c0c39f43f49ee7a24ce78486937fbfde14a1b16 Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Wed, 23 Apr 2025 15:25:19 +0100 Subject: [PATCH 01/12] Add a basic org members view --- home/templates/organisation/members.html | 56 ++++++++++++++++++++++++ home/urls.py | 2 +- home/views.py | 25 +++++++++-- static/templates/base_manager.html | 37 +++++++++------- 4 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 home/templates/organisation/members.html diff --git a/home/templates/organisation/members.html b/home/templates/organisation/members.html new file mode 100644 index 00000000..478eb5f2 --- /dev/null +++ b/home/templates/organisation/members.html @@ -0,0 +1,56 @@ +{% extends "base_manager.html" %} +{% load project_filters %} +{% block content %} +
+
+ +
+
+
+

{{ organisation.name }}

+

{{ organisation.description }}

+
+
+

Members

+
+ + + Invite new member + + +
+ + + + + + + + + + + {% for membership in memberships %} + + + + + + + + {% endfor %} +
UserRoleJoined atAdded byRemove
{{ membership.user }}{{ membership.role | title }}{{ membership.joined_at }}{{ membership.added_by }} + + Remove + +
+
+{% endblock %} diff --git a/home/urls.py b/home/urls.py index 3feaa87d..ac4ee852 100644 --- a/home/urls.py +++ b/home/urls.py @@ -30,8 +30,8 @@ views.CustomPasswordResetCompleteView.as_view(), name="password_reset_complete", ), - # path('password_reset/expired/', views.PasswordResetExpiredView.as_view(), name='password_reset_expired'), path("myorganisation/", views.MyOrganisationView.as_view(), name="myorganisation"), + path("myorganisation/members/", views.OrganisationMembershipListView.as_view(), name="members"), path( "organisation/create/", views.OrganisationCreateView.as_view(), diff --git a/home/views.py b/home/views.py index c685f3ac..5e9978e1 100644 --- a/home/views.py +++ b/home/views.py @@ -22,7 +22,7 @@ from .constants import ROLE_ADMIN, ROLE_PROJECT_MANAGER from .forms import ManagerLoginForm, ManagerSignupForm, UserProfileForm from .mixins import OrganisationRequiredMixin -from .models import Organisation, Project +from .models import Organisation, Project, OrganisationMembership from .services import organisation_service, project_service from survey.services import survey_service @@ -61,11 +61,11 @@ def dispatch(self, request, *args, **kwargs): class HomeView(LoginRequiredMixin, View): template_name = "home/welcome.html" context_object_name = "projects" - + def get(self, request): user = self.request.user # all projects for current user - projects = project_service.get_user_projects(user) + projects = project_service.get_user_projects(user) return render(request, self.template_name, dict(projects=projects)) @@ -360,3 +360,22 @@ def form_valid(self, form): def get_success_url(self): return reverse_lazy("myorganisation") + + +class OrganisationMembershipListView(LoginRequiredMixin, OrganisationRequiredMixin, ListView): + model = OrganisationMembership + context_object_name = "memberships" + template_name = "organisation/members.html" + + @property + def organisation(self) -> Organisation: + return organisation_service.get_user_organisation(self.request.user) + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(organisation=self.organisation) + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["organisation"] = self.organisation + return context diff --git a/static/templates/base_manager.html b/static/templates/base_manager.html index c0ff3005..a91cddd9 100644 --- a/static/templates/base_manager.html +++ b/static/templates/base_manager.html @@ -6,23 +6,25 @@ -
+

{{ organisation.name }}

{{ organisation.description }}

-
-
- {% csrf_token %} - {{ form | crispy }} -
+
+

Invite new member

+
+
+
+ {% csrf_token %} + {{ form | crispy }} + +
+
+
{% endblock %} diff --git a/home/templatetags/search_tags.py b/home/templatetags/search_tags.py index 01014313..5cae64a7 100644 --- a/home/templatetags/search_tags.py +++ b/home/templatetags/search_tags.py @@ -1,8 +1,8 @@ -from django import template +import django.template -from ..forms import SearchBarForm +from home.forms.search_bar import SearchBarForm -register = template.Library() +register = django.template.Library() @register.inclusion_tag("components/search_bar.html", takes_context=True) diff --git a/home/tests/test_services.py b/home/tests/test_services.py index ad9ae363..759cb816 100644 --- a/home/tests/test_services.py +++ b/home/tests/test_services.py @@ -4,8 +4,7 @@ from home.constants import ROLE_ADMIN, ROLE_PROJECT_MANAGER from home.services import organisation_service, project_service from survey.services import survey_service - -from ..models import Organisation, OrganisationMembership +from home.models import Organisation, OrganisationMembership from .model_factory import ( OrganisationFactory, ProjectFactory, diff --git a/home/views.py b/home/views.py index 8d31b350..391201e8 100644 --- a/home/views.py +++ b/home/views.py @@ -14,14 +14,17 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.views import View -from django.views.generic import ListView +from django.views.generic import ListView, FormView from django.views.generic.edit import CreateView, DeleteView, UpdateView from survey.models import Survey from survey.services import survey_service from .constants import ROLE_ADMIN, ROLE_PROJECT_MANAGER -from .forms import ManagerLoginForm, ManagerSignupForm, UserProfileForm +from .forms.user_profile import UserProfileForm +from .forms.manager_login import ManagerLoginForm +from .forms.manager_signup import ManagerSignupForm +from .forms.organisation_membership_create import OrganisationMembershipCreateForm from .mixins import OrganisationRequiredMixin from .models import Organisation, Project, OrganisationMembership from .services import organisation_service, project_service @@ -375,16 +378,26 @@ def get_queryset(self): queryset = super().get_queryset() return queryset.filter(organisation=self.organisation) - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context["organisation"] = self.organisation return context -class OrganisationMembershipCreateView(LoginRequiredMixin, OrganisationRequiredMixin, CreateView): +class OrganisationMembershipCreateView(LoginRequiredMixin, OrganisationRequiredMixin, FormView): """ Add a new member to your organisation. """ model = OrganisationMembership fields = ["user"] template_name = "organisation/members/create.html" + form_class = OrganisationMembershipCreateForm + + @property + def organisation(self) -> Organisation: + return organisation_service.get_user_organisation(self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["organisation"] = self.organisation + return context From 2584d03cf8a5068014788b4db5fadf4fe8004a17 Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Mon, 28 Apr 2025 15:42:37 +0100 Subject: [PATCH 04/12] Add django-invitations plugin --- SORT/settings.py | 10 ++- docs/invitations.md | 14 ++++ docs/templates.md | 8 ++ home/forms/manager_signup.py | 8 +- home/forms/organisation_membership_create.py | 12 --- home/templates/home/login.html | 1 - home/templates/home/register.html | 13 +++- .../organisation/members/create.html | 24 ++++-- home/templates/organisation/members/list.html | 6 +- home/urls.py | 26 +++++-- home/views.py | 74 +++++++++++++++---- requirements.txt | 1 + 12 files changed, 148 insertions(+), 49 deletions(-) create mode 100644 docs/invitations.md create mode 100644 docs/templates.md delete mode 100644 home/forms/organisation_membership_create.py diff --git a/SORT/settings.py b/SORT/settings.py index 5c795907..6d32be24 100644 --- a/SORT/settings.py +++ b/SORT/settings.py @@ -61,13 +61,15 @@ def cast_to_boolean(obj: Any) -> bool: "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + # Plugin apps "django_bootstrap5", "django_extensions", "debug_toolbar", "qr_code", "crispy_forms", "crispy_bootstrap5", - # apps created by FA: + "invitations", + # SORT apps "home", "survey", ] @@ -261,3 +263,9 @@ def cast_to_boolean(obj: Any) -> bool: # https://django-crispy-forms.readthedocs.io/en/latest/install.html CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5" + +# Invitations options +# https://django-invitations.readthedocs.io/en/latest/configuration.html +INVITATIONS_SIGNUP_REDIRECT = "signup" +INVITATIONS_CONFIRMATION_URL_NAME = "member_invite_accept" +INVITATIONS_EMAIL_SUBJECT_PREFIX = "SORT" diff --git a/docs/invitations.md b/docs/invitations.md new file mode 100644 index 00000000..56c74c95 --- /dev/null +++ b/docs/invitations.md @@ -0,0 +1,14 @@ +# Invitations + +https://django-invitations.readthedocs.io/ + +# Usage + +## Clear invitations + +[Invitations management command](https://django-invitations.readthedocs.io/en/latest/usage.html#management-commands) + +```bash +python manage.py clear_expired_invitations +``` + diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 00000000..7b9c5cf4 --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,8 @@ +# Icons + +The [Boxicons](https://boxicons.com/) website has a gallery of available icons that can be implemented using the following HTML code: + +```html + +``` + diff --git a/home/forms/manager_signup.py b/home/forms/manager_signup.py index 42614145..7c9da6fb 100644 --- a/home/forms/manager_signup.py +++ b/home/forms/manager_signup.py @@ -7,14 +7,14 @@ class ManagerSignupForm(UserCreationForm): - email = forms.EmailField( - required=True, label="Email", error_messages={"required": "Email is required."} - ) - class Meta: model = User fields = ("email", "password1", "password2") + email = forms.EmailField( + required=True, label="Email", error_messages={"required": "Email is required."} + ) + def clean_email(self) -> str: email = self.cleaned_data.get("email") if User.objects.filter(email=email).exists(): diff --git a/home/forms/organisation_membership_create.py b/home/forms/organisation_membership_create.py deleted file mode 100644 index 55830174..00000000 --- a/home/forms/organisation_membership_create.py +++ /dev/null @@ -1,12 +0,0 @@ -import django.forms -import django.contrib.auth - -from home.models import OrganisationMembership - -User = django.contrib.auth.get_user_model() - - -class OrganisationMembershipCreateForm(django.forms.ModelForm): - class Meta: - model = User - fields = ["email"] diff --git a/home/templates/home/login.html b/home/templates/home/login.html index a4ca01da..49ef83b9 100644 --- a/home/templates/home/login.html +++ b/home/templates/home/login.html @@ -20,7 +20,6 @@ - Don't have an account? Sign up
{% endblock %} diff --git a/home/templates/home/register.html b/home/templates/home/register.html index 6f27f55d..d0f31a7e 100644 --- a/home/templates/home/register.html +++ b/home/templates/home/register.html @@ -5,22 +5,27 @@ {% block title %}Register{% endblock %} {% block content %} +
+

{{ organisation }}

+

{{ organisation.description }}

+
{% csrf_token %} {% for field in form %}
- + - {% if field.errors %} + {% if field.field.required %} required {% endif %} /> + {% if field.errors %} - {% endif %} + {% endif %}
{% endfor %} diff --git a/home/templates/organisation/members/create.html b/home/templates/organisation/members/create.html index 67a9eb91..21a82089 100644 --- a/home/templates/organisation/members/create.html +++ b/home/templates/organisation/members/create.html @@ -23,11 +23,25 @@

{{ organisation.name }}

Invite new member

- - {% csrf_token %} - {{ form | crispy }} - - + {% if success_message %} + + + + {% else %} + +
+ {% csrf_token %} + {{ form | crispy }} + + +
+ {% endif %}
diff --git a/home/templates/organisation/members/list.html b/home/templates/organisation/members/list.html index 34312e14..aa7b7a10 100644 --- a/home/templates/organisation/members/list.html +++ b/home/templates/organisation/members/list.html @@ -22,10 +22,8 @@

{{ organisation.name }}

Members

- - - Invite new member - + + Invite new member
diff --git a/home/urls.py b/home/urls.py index e7557970..559e0de1 100644 --- a/home/urls.py +++ b/home/urls.py @@ -1,6 +1,6 @@ __author__ = "Farhad Allian" -from django.urls import path +from django.urls import include, path, re_path from . import views @@ -8,7 +8,11 @@ path("", views.HomeView.as_view(), name="home"), path("login/", views.LoginInterfaceView.as_view(), name="login"), path("logout/", views.LogoutInterfaceView.as_view(), name="logout"), - path("signup/", views.SignupView.as_view(), name="signup"), + re_path( + r"^signup/(?P\w+)/?$", + views.SignupView.as_view(), + name="signup", + ), path("profile/", views.ProfileView.as_view(), name="profile"), path( "password_reset/", @@ -31,14 +35,26 @@ name="password_reset_complete", ), path("myorganisation/", views.MyOrganisationView.as_view(), name="myorganisation"), - path("myorganisation/members/", views.OrganisationMembershipListView.as_view(), name="members"), - path("myorganisation/members/invite", views.OrganisationMembershipCreateView.as_view(), name="member_create"), + path( + "myorganisation/members/", + views.OrganisationMembershipListView.as_view(), + name="members", + ), + path( + "myorganisation/members/invite", + views.MyOrganisationInviteView.as_view(), + name="member_invite", + ), + re_path( + r"^myorganisation/members/accept/(?P\w+)/?$", + views.MyOrganisationAcceptInviteView.as_view(), + name="member_invite_accept", + ), path( "organisation/create/", views.OrganisationCreateView.as_view(), name="organisation_create", ), - # path("projects/create/", views.ProjectCreateView.as_view(), name="project_create"), path("projects//", views.ProjectView.as_view(), name="project"), path( "projects/create//", diff --git a/home/views.py b/home/views.py index 391201e8..095e8c4d 100644 --- a/home/views.py +++ b/home/views.py @@ -16,6 +16,8 @@ from django.views import View from django.views.generic import ListView, FormView from django.views.generic.edit import CreateView, DeleteView, UpdateView +import invitations.views +import invitations.models from survey.models import Survey from survey.services import survey_service @@ -24,7 +26,6 @@ from .forms.user_profile import UserProfileForm from .forms.manager_login import ManagerLoginForm from .forms.manager_signup import ManagerSignupForm -from .forms.organisation_membership_create import OrganisationMembershipCreateForm from .mixins import OrganisationRequiredMixin from .models import Organisation, Project, OrganisationMembership from .services import organisation_service, project_service @@ -36,6 +37,30 @@ class SignupView(CreateView): form_class = ManagerSignupForm template_name = "home/register.html" + @property + def invitation(self) -> invitations.models.Invitation: + try: + return self._invitation + except AttributeError: + try: + self._invitation = invitations.models.Invitation.objects.get(key=self.kwargs["key"]) + return self._invitation + # This signup must have an invitation + except invitations.models.Invitation.DoesNotExist: + raise PermissionDenied("You must be invited to sign up.") + + def get_context_data(self, **context): + context = super().get_context_data(**context) + context["email"] = self.invitation.email + context["organisation"] = organisation_service.get_user_organisation(self.invitation.inviter) + + return context + + def get_initial(self): + initial = super().get_initial() + initial["email"] = self.invitation.email + return initial + def form_valid(self, form): user = form.save() login(self.request, user) @@ -384,20 +409,43 @@ def get_context_data(self, **kwargs): return context -class OrganisationMembershipCreateView(LoginRequiredMixin, OrganisationRequiredMixin, FormView): +# class OrganisationMembershipCreateView(LoginRequiredMixin, OrganisationRequiredMixin, FormView): +# """ +# Add a new member to your organisation. +# """ +# model = OrganisationMembership +# fields = ["user"] +# template_name = "organisation/members/create.html" +# form_class = OrganisationMembershipCreateForm +# +# @property +# def organisation(self) -> Organisation: +# return organisation_service.get_user_organisation(self.request.user) +# +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# context["organisation"] = self.organisation +# return context + +class MyOrganisationInviteView(LoginRequiredMixin, OrganisationRequiredMixin, invitations.views.SendInvite): """ - Add a new member to your organisation. + Invite a new member to join an organisation via email. + + https://django-invitations.readthedocs.io/en/latest/usage.html """ - model = OrganisationMembership - fields = ["user"] + # Based on the template in the django-invitations plugin + # https://github.com/jazzband/django-invitations/blob/master/invitations/templates/invitations/forms/_invite.html template_name = "organisation/members/create.html" - form_class = OrganisationMembershipCreateForm - @property - def organisation(self) -> Organisation: - return organisation_service.get_user_organisation(self.request.user) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["organisation"] = self.organisation - return context +class MyOrganisationAcceptInviteView(invitations.views.AcceptInvite): + def post(self, *args, **kwargs): + import django.urls + try: + super().post(*args, **kwargs) + # There is no public signup URL + except django.urls.NoReverseMatch: + pass + + # Signup requires a key from an invitation + return redirect("signup", key=self.object.key) diff --git a/requirements.txt b/requirements.txt index eb8d830f..1e828da3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,3 +33,4 @@ django-qr-code==4.1.0 factory-boy==3.3.3 django-crispy-forms==2.4 crispy-bootstrap5==2025.4 +django-invitations==2.1.0 From d9cc87e1567fc44fbd2fe18e6fb4d0b9887d028e Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Mon, 28 Apr 2025 18:15:16 +0100 Subject: [PATCH 05/12] Register new user based on invitation key --- home/forms/manager_signup.py | 20 ++++++++++---------- home/templates/home/register.html | 14 +++++++++++++- home/views.py | 23 +++++++++++++++++------ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/home/forms/manager_signup.py b/home/forms/manager_signup.py index 7c9da6fb..b4c7b697 100644 --- a/home/forms/manager_signup.py +++ b/home/forms/manager_signup.py @@ -3,27 +3,27 @@ from django.contrib.auth.forms import UserCreationForm from django.core.exceptions import ValidationError +from invitations.models import Invitation + User = django.contrib.auth.get_user_model() class ManagerSignupForm(UserCreationForm): class Meta: model = User - fields = ("email", "password1", "password2") + fields = ("password1", "password2") - email = forms.EmailField( - required=True, label="Email", error_messages={"required": "Email is required."} - ) + # Secret key for the invitation (hidden form field) + key = forms.CharField(required=False, disabled=True, widget=forms.HiddenInput, label="") - def clean_email(self) -> str: - email = self.cleaned_data.get("email") - if User.objects.filter(email=email).exists(): - raise ValidationError("This email is already registered.") - return email + @property + def email(self) -> str: + invitation = Invitation.objects.get(key=self.data["key"]) + return invitation.email def save(self, commit=True): user = super().save(commit=False) - user.email = self.cleaned_data["email"] + user.email = self.email user.username = user.email if commit: user.save() diff --git a/home/templates/home/register.html b/home/templates/home/register.html index d0f31a7e..4e5578d2 100644 --- a/home/templates/home/register.html +++ b/home/templates/home/register.html @@ -7,10 +7,22 @@ {% block content %}

{{ organisation }}

-

{{ organisation.description }}

+

{{ organisation.description | truncatechars:128 }}

{% csrf_token %} +
+ + +
{% for field in form %}
- - - - - - - - - - {% for membership in memberships %} +
+
UserRoleJoined atAdded byRemove
+ - - - - - + + + + + - {% endfor %} -
{{ membership.user }}{{ membership.role | title }}{{ membership.joined_at }}{{ membership.added_by }} - - Remove - - UserRoleJoined atAdded byRemove
+ + {% for membership in memberships %} + + {{ membership.user }} + {{ membership.role }} + {{ membership.joined_at }} + {{ membership.added_by }} + + + Remove + + + + {% endfor %} + +
{% endblock %} diff --git a/home/templates/organisation/organisation.html b/home/templates/organisation/organisation.html index 7bb3d7ac..26478195 100644 --- a/home/templates/organisation/organisation.html +++ b/home/templates/organisation/organisation.html @@ -15,7 +15,14 @@

{{ organisation.name }}

{{ organisation.description }}

+ +
+ + Manage members + +
+

Projects

From a231b28fbc17ea015e430ba9f14f17bafcd62479 Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Tue, 29 Apr 2025 15:11:40 +0100 Subject: [PATCH 09/12] Remove unused import --- survey/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/survey/forms.py b/survey/forms.py index 4c38a94e..e264f3bf 100644 --- a/survey/forms.py +++ b/survey/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.core.validators import EmailValidator from django.forms import BaseFormSet, formset_factory from strenum import StrEnum From 04115c79f0d168fb0598de20aeb904b8714186d9 Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Tue, 29 Apr 2025 15:44:33 +0100 Subject: [PATCH 10/12] Added remove user feature --- home/models.py | 3 ++ .../organisation/members/delete.html | 39 +++++++++++++++++++ home/templates/organisation/members/list.html | 18 +++++++-- home/urls.py | 5 +++ home/views.py | 36 ++++++++++------- 5 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 home/templates/organisation/members/delete.html diff --git a/home/models.py b/home/models.py index 39fa92f7..77899ac9 100644 --- a/home/models.py +++ b/home/models.py @@ -50,6 +50,9 @@ class User(AbstractBaseUser, PermissionsMixin): objects = UserManager() def __str__(self): + # If they didn't enter their name, default to email address + if not self.first_name and not self.last_name: + return self.email return f"{self.first_name} {self.last_name}" diff --git a/home/templates/organisation/members/delete.html b/home/templates/organisation/members/delete.html new file mode 100644 index 00000000..eb8b8aba --- /dev/null +++ b/home/templates/organisation/members/delete.html @@ -0,0 +1,39 @@ +{% extends "base_manager.html" %} +{% block content %} +
+ +

Remove user

+

+ Please confirm that you would like to remove permission for + {{ organisation_membership.user }} to + access {{ organisation_membership.organisation }}. +

+ + + {% csrf_token %} + {{ form }} +
+ + +
+ + +
+{% endblock %} diff --git a/home/templates/organisation/members/list.html b/home/templates/organisation/members/list.html index 9c502ecd..b1dc7b5e 100644 --- a/home/templates/organisation/members/list.html +++ b/home/templates/organisation/members/list.html @@ -31,9 +31,10 @@

Members

- + - + + @@ -43,11 +44,20 @@

Members

{% for membership in memberships %} + - +
UserUser nameEmail address Role Joined at Added by
{{ membership.user }} + + {{ membership.user.email }} + + {{ membership.role }}{{ membership.joined_at }} + + {{ membership.added_by }} - Remove diff --git a/home/urls.py b/home/urls.py index 178650b9..e36cecaa 100644 --- a/home/urls.py +++ b/home/urls.py @@ -40,6 +40,11 @@ views.OrganisationMembershipListView.as_view(), name="members", ), + path( + "myorganisation/members/delete//", + views.OrganisationMembershipDeleteView.as_view(), + name="member_delete", + ), path( "myorganisation/members/invite", views.MyOrganisationInviteView.as_view(), diff --git a/home/views.py b/home/views.py index af9ed76c..ca18e0ca 100644 --- a/home/views.py +++ b/home/views.py @@ -1,35 +1,32 @@ from typing import Optional +import invitations.models +import invitations.views from django.contrib import messages from django.contrib.auth import get_user_model, login from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.views import ( - LoginView, - LogoutView, - PasswordResetCompleteView, - PasswordResetConfirmView, - PasswordResetDoneView, - PasswordResetView, -) +from django.contrib.auth.views import (LoginView, LogoutView, + PasswordResetCompleteView, + PasswordResetConfirmView, + PasswordResetDoneView, + PasswordResetView) +from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import PermissionDenied from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.views import View -from django.views.generic import ListView -from django.views.generic.edit import CreateView, DeleteView, UpdateView -import invitations.views -import invitations.models +from django.views.generic import CreateView, DeleteView, ListView, UpdateView from survey.models import Survey from survey.services import survey_service from .constants import ROLE_ADMIN, ROLE_PROJECT_MANAGER -from .forms.user_profile import UserProfileForm from .forms.manager_login import ManagerLoginForm from .forms.manager_signup import ManagerSignupForm +from .forms.user_profile import UserProfileForm from .mixins import OrganisationRequiredMixin -from .models import Organisation, Project, OrganisationMembership +from .models import Organisation, OrganisationMembership, Project from .services import organisation_service, project_service User = get_user_model() @@ -449,3 +446,14 @@ def post(self, *args, **kwargs): # Signup requires a key from an invitation return redirect("signup", key=self.object.key) + + +class OrganisationMembershipDeleteView(LoginRequiredMixin, OrganisationRequiredMixin, SuccessMessageMixin, DeleteView): + """ + Remove a user from an organisation. + """ + model = OrganisationMembership + template_name = "organisation/members/delete.html" + context_object_name = "organisation_membership" + success_url = reverse_lazy("members") + success_message = "The user was removed from the organisation." From b232c9dd6b18c202f71e096d8554fb0c32dac7f6 Mon Sep 17 00:00:00 2001 From: Joe Heffer Date: Tue, 29 Apr 2025 15:51:28 +0100 Subject: [PATCH 11/12] Remove members top-level nav link --- static/templates/base_manager.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/templates/base_manager.html b/static/templates/base_manager.html index a91cddd9..1343a1c8 100644 --- a/static/templates/base_manager.html +++ b/static/templates/base_manager.html @@ -37,9 +37,6 @@ {% if '/invite' in request.path %}active{% endif %}" href="{% url 'myorganisation' %}"> My Organisation -