#169 Team delete-restore

* adds restorable delete functionality to Team model
* refactors minor code model parts by introducing DeletableObjectMixin
* only non-deleted Teams can be chosen for sharing
* deleted Teams can be restored using the proper function on the backend admin
* deleted Teams do not provide
* adds migration
pull/170/head
mpeltriaux 2 years ago
parent 170e5798ec
commit 5de3f4c24e

@ -109,7 +109,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.intervention.teams.all %} {% for team in obj.intervention.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>

@ -87,7 +87,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>

@ -73,11 +73,11 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
{% for user in obj.users.all %} {% for user in obj.user.all %}
{% include 'user/includes/contact_modal_button.html' %} {% include 'user/includes/contact_modal_button.html' %}
{% endfor %} {% endfor %}
</td> </td>

@ -125,7 +125,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>

@ -98,6 +98,18 @@ class DeadlineAdmin(admin.ModelAdmin):
] ]
class DeletableObjectMixinAdmin(admin.ModelAdmin):
class Meta:
abstract = True
def restore_deleted_data(self, request, queryset):
queryset = queryset.filter(
deleted__isnull=False
)
for entry in queryset:
entry.deleted.delete()
class BaseResourceAdmin(admin.ModelAdmin): class BaseResourceAdmin(admin.ModelAdmin):
fields = [ fields = [
"created", "created",
@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
] ]
class BaseObjectAdmin(BaseResourceAdmin): class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [ search_fields = [
"identifier", "identifier",
"title", "title",
@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted", "deleted",
] ]
def restore_deleted_data(self, request, queryset):
queryset = queryset.filter(
deleted__isnull=False
)
for entry in queryset:
entry.deleted.delete()
# Outcommented for a cleaner admin backend on production # Outcommented for a cleaner admin backend on production

@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return Team.objects.none() return Team.objects.none()
qs = Team.objects.all() qs = Team.objects.filter(
deleted__isnull=True
)
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(

@ -87,25 +87,15 @@ class BaseResource(UuidModel):
super().delete() super().delete()
class BaseObject(BaseResource): class DeletableObjectMixin(models.Model):
""" """ Wraps deleted field and related functionality
A basic object model, which specifies BaseResource.
Mainly used for intervention, compensation, ecoaccount
""" """
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+') deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta: class Meta:
abstract = True abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted """ Mark an entry as deleted
@ -140,6 +130,25 @@ class BaseObject(BaseResource):
self.save() self.save()
class BaseObject(BaseResource, DeletableObjectMixin):
"""
A basic object model, which specifies BaseResource.
Mainly used for intervention, compensation, ecoaccount
"""
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta:
abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None): def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated """ In case the object or a related object changed the log history needs to be updated
@ -484,8 +493,8 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
""" """
directly_shared = self.users.filter(id=user.id).exists() directly_shared = self.shared_users.filter(id=user.id).exists()
team_shared = self.teams.filter( team_shared = self.shared_teams.filter(
users__in=[user] users__in=[user]
).exists() ).exists()
is_shared = directly_shared or team_shared is_shared = directly_shared or team_shared
@ -622,7 +631,9 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
teams (QuerySet) teams (QuerySet)
""" """
return self.teams.all() return self.teams.filter(
deleted__isnull=True
)
@abstractmethod @abstractmethod
def get_share_url(self): def get_share_url(self):

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from user.models import UserNotification, UserActionLogEntry, User, Team from konova.admin import DeletableObjectMixinAdmin
from user.models import User, Team
class UserNotificationAdmin(admin.ModelAdmin): class UserNotificationAdmin(admin.ModelAdmin):
@ -64,10 +65,11 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
] ]
class TeamAdmin(admin.ModelAdmin): class TeamAdmin(DeletableObjectMixinAdmin, admin.ModelAdmin):
list_display = [ list_display = [
"name", "name",
"description", "description",
"deleted",
] ]
search_fields = [ search_fields = [
"name", "name",
@ -78,6 +80,13 @@ class TeamAdmin(admin.ModelAdmin):
"admins", "admins",
] ]
readonly_fields = [
"deleted"
]
actions = [
"restore_deleted_data"
]
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(Team, TeamAdmin) admin.site.register(Team, TeamAdmin)

@ -230,8 +230,8 @@ class NewTeamModalForm(BaseModalForm):
team = Team.objects.create( team = Team.objects.create(
name=self.cleaned_data.get("name", None), name=self.cleaned_data.get("name", None),
description=self.cleaned_data.get("description", None), description=self.cleaned_data.get("description", None),
admins__in=[self.user],
) )
team.admins.add(self.user)
members = self.cleaned_data.get("members", User.objects.none()) members = self.cleaned_data.get("members", User.objects.none())
if self.user.id not in members: if self.user.id not in members:
members = members.union( members = members.union(
@ -335,6 +335,10 @@ class RemoveTeamModalForm(RemoveModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_caption = _("ATTENTION!\n\nRemoving the team means all members will lose their access to data, based on this team! \n\nAre you sure to remove this team?") self.form_caption = _("ATTENTION!\n\nRemoving the team means all members will lose their access to data, based on this team! \n\nAre you sure to remove this team?")
def save(self):
self.instance.mark_as_deleted(self.user)
return self.instance
class LeaveTeamModalForm(RemoveModalForm): class LeaveTeamModalForm(RemoveModalForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-05-30 12:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user', '0004_auto_20220530_1105'),
]
operations = [
migrations.AddField(
model_name='team',
name='deleted',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry'),
),
]

@ -1,10 +1,11 @@
from django.db import models from django.db import models
from konova.models import UuidModel from konova.models import UuidModel, DeletableObjectMixin
from konova.utils.mailer import Mailer from konova.utils.mailer import Mailer
from user.models import UserActionLogEntry
class Team(UuidModel): class Team(UuidModel, DeletableObjectMixin):
""" Groups users in self managed teams. Can be used for multi-sharing of data """ Groups users in self managed teams. Can be used for multi-sharing of data
""" """
@ -16,6 +17,19 @@ class Team(UuidModel):
def __str__(self): def __str__(self):
return self.name return self.name
def mark_as_deleted(self, user):
""" Creates an UserAction entry and stores it in the correct field
Args:
user (User): The performing user
Returns:
"""
delete_action = UserActionLogEntry.get_deleted_action(user, "Team deleted")
self.deleted = delete_action
self.save()
def send_mail_shared_access_given_team(self, obj_identifier, obj_title): def send_mail_shared_access_given_team(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of given shared access """ Sends a mail to the team members in case of given shared access

@ -160,3 +160,15 @@ class User(AbstractUser):
else: else:
token = self.api_token token = self.api_token
return token return token
@property
def shared_teams(self):
""" Wrapper for fetching active teams of this user
Returns:
"""
shared_teams = self.teams.filter(
deleted__isnull=True
)
return shared_teams

@ -163,7 +163,7 @@ def index_team_view(request: HttpRequest):
template = "user/team/index.html" template = "user/team/index.html"
user = request.user user = request.user
context = { context = {
"teams": user.teams.all(), "teams": user.shared_teams,
"tab_title": _("Teams"), "tab_title": _("Teams"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

Loading…
Cancel
Save