diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index be8dd3b..4a1177a 100644 --- a/compensation/templates/compensation/detail/compensation/view.html +++ b/compensation/templates/compensation/detail/compensation/view.html @@ -109,7 +109,7 @@ {% trans 'Shared with' %} - {% for team in obj.intervention.teams.all %} + {% for team in obj.intervention.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html index aa76fbf..f4e8da4 100644 --- a/compensation/templates/compensation/detail/eco_account/view.html +++ b/compensation/templates/compensation/detail/eco_account/view.html @@ -87,7 +87,7 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html index adc120f..7d60473 100644 --- a/ema/templates/ema/detail/view.html +++ b/ema/templates/ema/detail/view.html @@ -73,11 +73,11 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
- {% for user in obj.users.all %} + {% for user in obj.user.all %} {% include 'user/includes/contact_modal_button.html' %} {% endfor %} diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html index 55d57f6..1a596bb 100644 --- a/intervention/templates/intervention/detail/view.html +++ b/intervention/templates/intervention/detail/view.html @@ -125,7 +125,7 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/konova/admin.py b/konova/admin.py index 213120e..b30f4b1 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -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 diff --git a/konova/autocompletes.py b/konova/autocompletes.py index 288ee02..fbd92f7 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -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( diff --git a/konova/models/object.py b/konova/models/object.py index 325762f..b468932 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -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): diff --git a/user/admin.py b/user/admin.py index d564066..bf5f5f8 100644 --- a/user/admin.py +++ b/user/admin.py @@ -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) diff --git a/user/forms.py b/user/forms.py index f6831ff..cfb6d72 100644 --- a/user/forms.py +++ b/user/forms.py @@ -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): diff --git a/user/migrations/0005_team_deleted.py b/user/migrations/0005_team_deleted.py new file mode 100644 index 0000000..b3a5c68 --- /dev/null +++ b/user/migrations/0005_team_deleted.py @@ -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'), + ), + ] diff --git a/user/models/team.py b/user/models/team.py index 7908115..5e728e7 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -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 diff --git a/user/models/user.py b/user/models/user.py index df63dd7..b40a3b1 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -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 \ No newline at end of file diff --git a/user/views.py b/user/views.py index b6e7f09..2c5b86a 100644 --- a/user/views.py +++ b/user/views.py @@ -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