#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
This commit is contained in:
		
							parent
							
								
									170e5798ec
								
							
						
					
					
						commit
						5de3f4c24e
					
				@ -109,7 +109,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <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' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
 | 
			
		||||
@ -73,11 +73,11 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% for user in obj.users.all %}
 | 
			
		||||
                            {% for user in obj.user.all %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
 | 
			
		||||
@ -125,7 +125,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <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):
 | 
			
		||||
    fields = [
 | 
			
		||||
        "created",
 | 
			
		||||
@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseObjectAdmin(BaseResourceAdmin):
 | 
			
		||||
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "identifier",
 | 
			
		||||
        "title",
 | 
			
		||||
@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
 | 
			
		||||
            "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
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        if self.request.user.is_anonymous:
 | 
			
		||||
            return Team.objects.none()
 | 
			
		||||
        qs = Team.objects.all()
 | 
			
		||||
        qs = Team.objects.filter(
 | 
			
		||||
            deleted__isnull=True
 | 
			
		||||
        )
 | 
			
		||||
        if self.q:
 | 
			
		||||
            # Due to privacy concerns only a full username match will return the proper user entry
 | 
			
		||||
            qs = qs.filter(
 | 
			
		||||
 | 
			
		||||
@ -87,25 +87,15 @@ class BaseResource(UuidModel):
 | 
			
		||||
        super().delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseObject(BaseResource):
 | 
			
		||||
    """
 | 
			
		||||
    A basic object model, which specifies BaseResource.
 | 
			
		||||
class DeletableObjectMixin(models.Model):
 | 
			
		||||
    """ Wraps deleted field and related functionality
 | 
			
		||||
 | 
			
		||||
    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='+')
 | 
			
		||||
    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_deleted(self, user, send_mail: bool = True):
 | 
			
		||||
        """ Mark an entry as deleted
 | 
			
		||||
 | 
			
		||||
@ -140,6 +130,25 @@ class BaseObject(BaseResource):
 | 
			
		||||
 | 
			
		||||
            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):
 | 
			
		||||
        """ 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:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        directly_shared = self.users.filter(id=user.id).exists()
 | 
			
		||||
        team_shared = self.teams.filter(
 | 
			
		||||
        directly_shared = self.shared_users.filter(id=user.id).exists()
 | 
			
		||||
        team_shared = self.shared_teams.filter(
 | 
			
		||||
            users__in=[user]
 | 
			
		||||
        ).exists()
 | 
			
		||||
        is_shared = directly_shared or team_shared
 | 
			
		||||
@ -622,7 +631,9 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
            teams (QuerySet)
 | 
			
		||||
        """
 | 
			
		||||
        return self.teams.all()
 | 
			
		||||
        return self.teams.filter(
 | 
			
		||||
            deleted__isnull=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_share_url(self):
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
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):
 | 
			
		||||
@ -64,10 +65,11 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeamAdmin(admin.ModelAdmin):
 | 
			
		||||
class TeamAdmin(DeletableObjectMixinAdmin, admin.ModelAdmin):
 | 
			
		||||
    list_display = [
 | 
			
		||||
        "name",
 | 
			
		||||
        "description",
 | 
			
		||||
        "deleted",
 | 
			
		||||
    ]
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "name",
 | 
			
		||||
@ -78,6 +80,13 @@ class TeamAdmin(admin.ModelAdmin):
 | 
			
		||||
        "admins",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    readonly_fields = [
 | 
			
		||||
        "deleted"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    actions = [
 | 
			
		||||
        "restore_deleted_data"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
admin.site.register(User, UserAdmin)
 | 
			
		||||
admin.site.register(Team, TeamAdmin)
 | 
			
		||||
 | 
			
		||||
@ -230,8 +230,8 @@ class NewTeamModalForm(BaseModalForm):
 | 
			
		||||
            team = Team.objects.create(
 | 
			
		||||
                name=self.cleaned_data.get("name", 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())
 | 
			
		||||
            if self.user.id not in members:
 | 
			
		||||
                members = members.union(
 | 
			
		||||
@ -335,6 +335,10 @@ class RemoveTeamModalForm(RemoveModalForm):
 | 
			
		||||
        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?")
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        self.instance.mark_as_deleted(self.user)
 | 
			
		||||
        return self.instance
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LeaveTeamModalForm(RemoveModalForm):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								user/migrations/0005_team_deleted.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								user/migrations/0005_team_deleted.py
									
									
									
									
									
										Normal file
									
								
							@ -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 konova.models import UuidModel
 | 
			
		||||
from konova.models import UuidModel, DeletableObjectMixin
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
@ -16,6 +17,19 @@ class Team(UuidModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        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):
 | 
			
		||||
        """ Sends a mail to the team members in case of given shared access
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -160,3 +160,15 @@ class User(AbstractUser):
 | 
			
		||||
        else:
 | 
			
		||||
            token = self.api_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"
 | 
			
		||||
    user = request.user
 | 
			
		||||
    context = {
 | 
			
		||||
        "teams": user.teams.all(),
 | 
			
		||||
        "teams": user.shared_teams,
 | 
			
		||||
        "tab_title": _("Teams"),
 | 
			
		||||
    }
 | 
			
		||||
    context = BaseContext(request, context).context
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user