master #172
@ -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):
|
||||||
|
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 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…
Reference in New Issue
Block a user