Skip to content

Commit

Permalink
add project leaving (#918)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Feb 3, 2025
1 parent 507a654 commit 457702b
Show file tree
Hide file tree
Showing 13 changed files with 745 additions and 507 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Added
- ``SODARUserSerializer`` ``auth_type`` field (#1501)
- ``UserRetrieveAPIView`` REST API view (#1555)
- ``active`` arg in ``ProjectInviteMixin.make_invite()`` (#1403)
- Ability for users to leave project (#918)

Changed
-------
Expand Down
Binary file modified docs/source/_static/app_projectroles/sodar_role_list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 19 additions & 3 deletions docs/source/app_projectroles_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,9 @@ category (owner or delegate) or superuser status.
Project member list view

All members of categories automatically inherit identical access rights to
subcategories and projects under those categories, starting in SODAR Core
v0.13. Inherited member roles can be promoted to a higher local role, but
demoting to a lesser role for child categories or projects is not allowed.
subcategories and projects under those categories. Inherited member roles can be
promoted to a higher local role, but demoting to a lesser role for child
categories or projects is not allowed.

For inherited members, the member list displays a link to the category where
the inheritance is derived from. Inherited members can not be removed or edited
Expand Down Expand Up @@ -369,6 +369,22 @@ Invites expire after a certain time and can be reissued or revoked on the
Inviting a user is prohibited if they already have an active invite in a
parent category of the current category or project.

Leaving a Project
-----------------

A user may leave a category or project by clicking the
:guilabel:`Leave Category` or :guilabel:`Leave Project` button in the role list
view. Leaving a category will also remove the user's access to child categories
and projects, except for cases where another role has specifically been assigned
for them in children.

This operation can not be undone. To regain access, an owner or a delegate must
re-add the user to the category or project.

Owners are not able to directly leave a project. Instead, the owner role must be
transferred to another user. To do this, the user must select
:guilabel:`Transfer Ownership` in their role dropdown.

Batch Member Modifications
--------------------------

Expand Down
3 changes: 1 addition & 2 deletions docs/source/major_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Release Highlights
==================

- Add project deletion
- Add ability for user to leave project
- Add site read-only mode
- Add siteappsettings site app plugin
- Add removeroles management command
Expand Down Expand Up @@ -68,7 +69,6 @@ This release enables the deletion of categories and projects. See the
:ref:`project app development documentation <dev_project_app_delete>` for more
information on how to support this feature in your apps.


AppSettingAPI Definition Getter Return Data
-------------------------------------------

Expand Down Expand Up @@ -114,7 +114,6 @@ REST API View Changes
* ``TimelineEventRetrieveAPIView``: Return ``user`` as UUID instead of
``SODARUserSerializer`` dict


Deprecated Features
-------------------

Expand Down
44 changes: 43 additions & 1 deletion projectroles/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from django.utils.timezone import localtime

from projectroles.app_settings import AppSettingAPI
from projectroles.models import SODARUserAdditionalEmail
from projectroles.models import (
SODARUserAdditionalEmail,
SODAR_CONSTANTS,
ROLE_RANKING,
)
from projectroles.plugins import get_app_plugin
from projectroles.utils import get_display_name

Expand All @@ -26,6 +30,9 @@
DEBUG = settings.DEBUG
SITE_TITLE = settings.SITE_INSTANCE_TITLE

# SODAR constants
PROJECT_ROLE_DELEGATE = SODAR_CONSTANTS['PROJECT_ROLE_DELEGATE']

# Local constants
APP_NAME = 'projectroles'
EMAIL_RE = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)')
Expand Down Expand Up @@ -67,6 +74,7 @@
SUBJECT_ROLE_CREATE = 'Membership granted for {project_label} "{project}"'
SUBJECT_ROLE_UPDATE = 'Membership changed in {project_label} "{project}"'
SUBJECT_ROLE_DELETE = 'Membership removed from {project_label} "{project}"'
SUBJECT_ROLE_LEAVE = 'Member {user_name} left {project_label} "{project}"'

MESSAGE_ROLE_CREATE = r'''
{issuer} has granted you the membership
Expand All @@ -90,6 +98,12 @@
{issuer} has removed your membership from {project_label} "{project}".
'''.lstrip()

MESSAGE_ROLE_LEAVE = r'''
Member {user_name} has left the {project_label} "{project}".
For them to regain access, it has to be granted again by {project_label}
owner or delegate.
'''.lstrip()


# Invite Template --------------------------------------------------------------

Expand Down Expand Up @@ -505,6 +519,34 @@ def send_role_change_mail(change_type, project, user, role, request):
)


def send_project_leave_mail(project, user, request=None):
"""
Send email to project owners and delegates when a user leaves a project.
:param project: Project object
:param user: User object
:param request: HttpRequest object or None
:return: Amount of sent email (int)
"""
p_label = get_display_name(project.type)
subject = SUBJECT_ROLE_LEAVE.format(
user_name=user.username, project_label=p_label, project=project.title
)
message = MESSAGE_ROLE_LEAVE.format(
user_name=user.username, project_label=p_label, project=project.title
)
mail_count = 0
recipients = [
a.user
for a in project.get_roles(max_rank=ROLE_RANKING[PROJECT_ROLE_DELEGATE])
if a.user != user
and app_settings.get(APP_NAME, 'notify_email_role', user=a.user)
]
for r in recipients:
mail_count += send_mail(subject, message, get_user_addr(r), request)
return mail_count


def send_invite_mail(invite, request):
"""
Send an email invitation to user not yet registered in the system.
Expand Down
84 changes: 41 additions & 43 deletions projectroles/templates/projectroles/_project_role_ops.html
Original file line number Diff line number Diff line change
@@ -1,44 +1,42 @@
<div class="ml-auto">
<button class="btn btn-primary dropdown-toggle sodar-pr-role-ops-btn"
id="sodar-pr-role-ops-btn"
type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
{% if project.is_remote %}disabled="disabled"{% endif %}>
Member Operations
</button>
<div class="dropdown-menu dropdown-menu-right"
id="sodar-pr-role-ops-dropdown">
{% if role_perms.can_update_members %}
<a class="dropdown-item"
href="{% url 'projectroles:role_create' project=project.sodar_uuid %}"
title="Add Member"
id="sodar-pr-role-ops-create">
<i class="iconify" data-icon="mdi:account-plus"></i> Add Member
</a>
{% endif %}
{% if role_perms.can_invite %}
<a class="dropdown-item"
href="{% url 'projectroles:invite_create' project=project.sodar_uuid %}"
title="Send Invite"
id="sodar-pr-role-ops-invite">
<i class="iconify" data-icon="mdi:send"></i> Send Invite
</a>
{% endif %}
{% if role_perms.can_update_members and request.resolver_match.url_name != 'project_roles' %}
<a class="dropdown-item"
href="{% url 'projectroles:roles' project=project.sodar_uuid %}"
title="View Members"
id="sodar-pr-role-ops-roles">
<i class="iconify" data-icon="mdi:account-multiple"></i> View Members
</a>
{% endif %}
{% if role_perms.can_invite and request.resolver_match.url_name != 'role_invites' %}
<a class="dropdown-item"
href="{% url 'projectroles:invites' project=project.sodar_uuid %}"
title="View Invites"
id="sodar-pr-role-ops-invites">
<i class="iconify" data-icon="mdi:email"></i> View Invites
</a>
{% endif %}
</div>
<button class="btn btn-primary dropdown-toggle sodar-pr-role-ops-btn"
id="sodar-pr-role-ops-btn"
type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
{% if project.is_remote %}disabled="disabled"{% endif %}>
Member Operations
</button>
<div class="dropdown-menu dropdown-menu-right"
id="sodar-pr-role-ops-dropdown">
{% if role_perms.can_update_members %}
<a class="dropdown-item"
href="{% url 'projectroles:role_create' project=project.sodar_uuid %}"
title="Add Member"
id="sodar-pr-role-ops-create">
<i class="iconify" data-icon="mdi:account-plus"></i> Add Member
</a>
{% endif %}
{% if role_perms.can_invite %}
<a class="dropdown-item"
href="{% url 'projectroles:invite_create' project=project.sodar_uuid %}"
title="Send Invite"
id="sodar-pr-role-ops-invite">
<i class="iconify" data-icon="mdi:send"></i> Send Invite
</a>
{% endif %}
{% if role_perms.can_update_members and request.resolver_match.url_name != 'project_roles' %}
<a class="dropdown-item"
href="{% url 'projectroles:roles' project=project.sodar_uuid %}"
title="View Members"
id="sodar-pr-role-ops-roles">
<i class="iconify" data-icon="mdi:account-multiple"></i> View Members
</a>
{% endif %}
{% if role_perms.can_invite and request.resolver_match.url_name != 'role_invites' %}
<a class="dropdown-item"
href="{% url 'projectroles:invites' project=project.sodar_uuid %}"
title="View Invites"
id="sodar-pr-role-ops-invites">
<i class="iconify" data-icon="mdi:email"></i> View Invites
</a>
{% endif %}
</div>
20 changes: 19 additions & 1 deletion projectroles/templates/projectroles/project_roles.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,33 @@

{% get_role_perms project request.user as role_perms %}
{% get_info_link finder_info as finder_info_link %}
{% get_display_name project.type title=True as project_type_title %}

<div class="row sodar-subtitle-container bg-white sticky-top">
<h3>
<i class="iconify" data-icon="mdi:account-multiple"></i>
{% get_display_name project.type title=True %} Members
{{ project_type_title }} Members
</h3>
<div class="ml-auto">
<span class="d-inline-block" tabindex="0" data-toggle="tooltip"
id="sodar-pr-btn-leave-tooltip"
title="{{ project_leave_msg }}">
<a role="button" class="btn btn-danger ml-auto"
{% if own_local_as %}
href="{% url 'projectroles:role_delete_own' roleassignment=own_local_as.sodar_uuid %}"
{% else %}
href="#"
{% endif %}
{% if not project_leave_access %}disabled="disabled"{% endif %}
id="sodar-pr-btn-leave-project">
<i class="iconify" data-icon="mdi:account-remove"></i>
Leave {{ project_type_title }}
</a>
</span>
{% if role_perms.can_update_members or role_perms.can_invite %}
{% include 'projectroles/_project_role_ops.html' %}
{% endif %}
</div>
</div>

<div class="container-fluid sodar-page-container">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% extends 'projectroles/project_base.html' %}

{% load rules %}
{% load projectroles_tags %}
{% load projectroles_common_tags %}

{% block title %}
Confirm Leaving {{ object.project.title }}
{% endblock title %}

{% block projectroles_extend %}

{% get_display_name object.project.type as project_type %}

<div class="container-fluid sodar-page-container">
<h3>Confirm Leaving {{ object.project.title }}</h3>

<div class="alert alert-danger" role="alert">
Are you sure you really want to leave the {{ project_type }}
"{{ object.project.title }}"? This action can not be undone. To regain
access, an owner or a delegate will have to re-enable your membership.
</div>
{% if inh_child_projects %}
<div class="alert alert-danger" role="alert"
id="sodar-pr-role-leave-alert-child">
Leaving this {{ project_type }} will also remove your access from the
following inherited {% get_display_name 'CATEGORY' plural=True %} or
{% get_display_name 'PROJECT' plural=True %}:
<ul class="mt-2 mb-0">
{% for child in inh_child_projects %}
<li class="sodar-pr-role-leave-child-item">
<a href="{% url 'projectroles:roles' project=child.sodar_uuid %}">
{{ child.full_title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}

<form method="post">
{% csrf_token %}
<div class="btn-group pull-right">
<a role="button" class="btn btn-secondary"
href="{{ request.session.real_referer }}">
<i class="iconify" data-icon="mdi:arrow-left-circle"></i> Cancel
</a>
<button type="submit" class="btn btn-danger">
<i class="iconify" data-icon="mdi:account-remove"></i> Leave
</button>
</div>
</form>
</div>

{% endblock projectroles_extend %}
Loading

0 comments on commit 457702b

Please sign in to comment.