#101 Team settings

* adds first implementation for team managing
pull/122/head
mpeltriaux 3 years ago
parent 2339fcebd1
commit 75c70ff8dc

@ -7,11 +7,11 @@ Created on: 27.09.21
""" """
from dal import autocomplete from dal import autocomplete
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE REVOCATION_EDITED
from user.models import User, UserActionLogEntry from user.models import User
from user.models import UserActionLogEntry
from django.db import transaction from django.db import transaction
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _

@ -16,7 +16,6 @@ from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT,
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \ from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked
from user.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.timezone import now from django.utils.timezone import now
@ -28,7 +27,6 @@ from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_I
from konova.utils import generators from konova.utils import generators
from konova.utils.generators import generate_random_string from konova.utils.generators import generate_random_string
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
from user.models import UserActionLogEntry, UserAction
class UuidModel(models.Model): class UuidModel(models.Model):
@ -50,14 +48,14 @@ class BaseResource(UuidModel):
A basic resource model, which defines attributes for every derived model A basic resource model, which defines attributes for every derived model
""" """
created = models.ForeignKey( created = models.ForeignKey(
UserActionLogEntry, "user.UserActionLogEntry",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='+' related_name='+'
) )
modified = models.ForeignKey( modified = models.ForeignKey(
UserActionLogEntry, "user.UserActionLogEntry",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
@ -94,9 +92,9 @@ class BaseObject(BaseResource):
""" """
identifier = models.CharField(max_length=1000, null=True, blank=True) identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True) title = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.ForeignKey(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) comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField(UserActionLogEntry, blank=True, help_text="Keeps all user actions of an object", editable=False) 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
@ -105,7 +103,7 @@ class BaseObject(BaseResource):
def set_status_messages(self, request: HttpRequest): def set_status_messages(self, request: HttpRequest):
raise NotImplementedError raise NotImplementedError
def mark_as_deleted(self, user: 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
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
@ -116,6 +114,7 @@ class BaseObject(BaseResource):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
if self.deleted: if self.deleted:
# Nothing to do here # Nothing to do here
return return
@ -133,7 +132,7 @@ class BaseObject(BaseResource):
self.save() self.save()
def mark_as_edited(self, performing_user: 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
Args: Args:
@ -144,13 +143,14 @@ class BaseObject(BaseResource):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment) edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = edit_action self.modified = edit_action
self.log.add(edit_action) self.log.add(edit_action)
self.save() self.save()
return edit_action return edit_action
def add_log_entry(self, action: UserAction, user: User, comment: str): def add_log_entry(self, action, user, comment: str):
""" Wraps adding of UserActionLogEntry to log """ Wraps adding of UserActionLogEntry to log
Args: Args:
@ -161,6 +161,7 @@ class BaseObject(BaseResource):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
user_action = UserActionLogEntry.objects.create( user_action = UserActionLogEntry.objects.create(
user=user, user=user,
action=action, action=action,
@ -229,7 +230,7 @@ class RecordableObjectMixin(models.Model):
""" """
# Refers to "verzeichnen" # Refers to "verzeichnen"
recorded = models.OneToOneField( recorded = models.OneToOneField(
UserActionLogEntry, "user.UserActionLogEntry",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
@ -240,7 +241,7 @@ class RecordableObjectMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def set_unrecorded(self, user: User): def set_unrecorded(self, user):
""" Perform unrecording """ Perform unrecording
Args: Args:
@ -249,6 +250,7 @@ class RecordableObjectMixin(models.Model):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
if not self.recorded: if not self.recorded:
return None return None
action = UserActionLogEntry.get_unrecorded_action(user) action = UserActionLogEntry.get_unrecorded_action(user)
@ -262,7 +264,7 @@ class RecordableObjectMixin(models.Model):
return action return action
def set_recorded(self, user: User): def set_recorded(self, user):
""" Perform recording """ Perform recording
Args: Args:
@ -271,6 +273,7 @@ class RecordableObjectMixin(models.Model):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
if self.recorded: if self.recorded:
return None return None
action = UserActionLogEntry.get_recorded_action(user) action = UserActionLogEntry.get_recorded_action(user)
@ -284,7 +287,7 @@ class RecordableObjectMixin(models.Model):
return action return action
def unrecord(self, performing_user: User, request: HttpRequest = None): def unrecord(self, performing_user, request: HttpRequest = None):
""" Unrecords a dataset """ Unrecords a dataset
Args: Args:
@ -318,7 +321,7 @@ class RecordableObjectMixin(models.Model):
class CheckableObjectMixin(models.Model): class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional # Checks - Refers to "Genehmigen" but optional
checked = models.OneToOneField( checked = models.OneToOneField(
UserActionLogEntry, "user.UserActionLogEntry",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
@ -346,7 +349,7 @@ class CheckableObjectMixin(models.Model):
self.save() self.save()
return None return None
def set_checked(self, user: User) -> UserActionLogEntry: def set_checked(self, user):
""" Perform checking """ Perform checking
Args: Args:
@ -355,6 +358,7 @@ class CheckableObjectMixin(models.Model):
Returns: Returns:
""" """
from user.models import UserActionLogEntry
if self.checked: if self.checked:
# Nothing to do # Nothing to do
return return
@ -373,7 +377,7 @@ class CheckableObjectMixin(models.Model):
class ShareableObjectMixin(models.Model): class ShareableObjectMixin(models.Model):
# Users having access on this object # Users having access on this object
users = models.ManyToManyField(User, help_text="Users having access (data shared with)") users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)")
access_token = models.CharField( access_token = models.CharField(
max_length=255, max_length=255,
null=True, null=True,
@ -420,7 +424,7 @@ class ShareableObjectMixin(models.Model):
self.access_token = token self.access_token = token
self.save() self.save()
def is_shared_with(self, user: User): def is_shared_with(self, user):
""" Access check """ Access check
Checks whether a given user has access to this object Checks whether a given user has access to this object
@ -433,7 +437,7 @@ class ShareableObjectMixin(models.Model):
""" """
return self.users.filter(id=user.id) return self.users.filter(id=user.id)
def share_with(self, user: User): def share_with(self, user):
""" Adds user to list of shared access users """ Adds user to list of shared access users
Args: Args:
@ -465,6 +469,7 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
""" """
from user.models import User
form_data = form.cleaned_data form_data = form.cleaned_data
keep_accessing_users = form_data["users"] keep_accessing_users = form_data["users"]

@ -5,17 +5,17 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 08.07.21 Created on: 08.07.21
""" """
from dal import autocomplete
from django import forms from django import forms
from django.db import IntegrityError from django.db import IntegrityError, transaction
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken from api.models import APIUserToken
from intervention.inputs import GenerateInput from intervention.inputs import GenerateInput
from user.models import User from user.models import User, UserNotification, Team
from konova.forms import BaseForm, BaseModalForm from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
from user.models import UserNotification
class UserNotificationForm(BaseForm): class UserNotificationForm(BaseForm):
@ -160,3 +160,105 @@ class UserAPITokenForm(BaseForm):
user.api_token = new_token user.api_token = new_token
user.save() user.save()
return new_token return new_token
class NewTeamModalForm(BaseModalForm):
name = forms.CharField(
label_suffix="",
label=_("Team name"),
max_length=500,
widget=forms.TextInput(
attrs={
"placeholder": _("Team name"),
"class": "form-control",
}
)
)
description = forms.CharField(
label_suffix="",
label=_("Description"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
members = forms.ModelMultipleChoiceField(
label=_("Manage team members"),
label_suffix="",
help_text=_("Multiple selection possible - You can only select users which are not already a team member. Enter the full username or e-mail."),
required=True,
queryset=User.objects.all(),
widget=autocomplete.ModelSelect2Multiple(
url="share-user-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
},
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Create new team")
self.form_caption = _("You will become the administrator for this group by default. You do not need to add yourself to the list of members.")
self.action_url = reverse("user:team-new")
self.cancel_redirect = reverse("user:team-index")
def save(self):
with transaction.atomic():
team = Team.objects.create(
name=self.cleaned_data.get("name", None),
description=self.cleaned_data.get("description", None),
admin=self.user,
)
members = self.cleaned_data.get("members", User.objects.none())
if self.user.id not in members:
members = members.union(
User.objects.filter(
id=self.user.id
)
)
team.users.set(members)
return team
class EditTeamModalForm(NewTeamModalForm):
admin = forms.ModelChoiceField(
label_suffix="",
label=_("Admin"),
help_text=_("Administrators manage team details and members"),
queryset=User.objects.none(),
empty_label=None,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit team")
self.form_caption = None
self.action_url = reverse("user:team-edit", args=(self.instance.id,))
self.cancel_redirect = reverse("user:team-index")
members = self.instance.users.all()
self.fields["admin"].queryset = members
form_data = {
"members": members,
"name": self.instance.name,
"description": self.instance.description,
"admin": self.instance.admin,
}
self.load_initial_data(form_data)
def save(self):
with transaction.atomic():
self.instance.name = self.cleaned_data.get("name", None)
self.instance.description = self.cleaned_data.get("description", None)
self.instance.save()
self.instance.users.set(self.cleaned_data.get("members", []))
return self.instance
class RemoveTeamModalForm(RemoveModalForm):
pass

@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21 Created on: 15.11.21
""" """
from .user_action import * from .user_action import UserActionLogEntry, UserAction
from .user import * from .user import User
from .notification import * from .notification import UserNotification, UserNotificationEnum
from .team import Team

@ -0,0 +1,16 @@
from django.db import models
from konova.models import UuidModel
class Team(UuidModel):
""" Groups users in self managed teams. Can be used for multi-sharing of data
"""
name = models.CharField(max_length=500, null=True, blank=True)
description = models.TextField(null=True, blank=True)
users = models.ManyToManyField("user.User", blank=True, related_name="teams")
admin = models.ForeignKey("user.User", blank=True, null=True, related_name="+", on_delete=models.SET_NULL)
def __str__(self):
return self.name

@ -62,6 +62,14 @@
</button> </button>
</a> </a>
</div> </div>
<div class="row mb-2">
<a href="{% url 'user:team-index' %}" title="{% trans 'Manage teams' %}">
<button class="btn btn-default">
{% fa5_icon 'users' %}
<span>{% trans 'Teams' %}</span>
</button>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% load i18n fontawesome_5 %}
{% block head %}
{% comment %}
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
Therefore the script linkages from form.media have been extracted and put inside dal/scripts.html to ensure
these scripts are loaded when needed.
{% endcomment %}
{% include 'dal/scripts.html' %}
{% endblock %}
{% block body %}
<h4>{% trans 'Teams' %}</h4>
<div class="col-md">
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-new' %}" title="{% trans 'Add new team' %}">
{% fa5_icon 'plus' %}
{% trans 'New' %}
</button>
</div>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="align-middle">{% trans 'Name' %}</th>
<th scope="col" class="align-middle w-20">{% trans 'Description' %}</th>
<th scope="col" class="align-middle">{% trans 'Members' %}</th>
<th scope="col" class="align-middle">{% trans 'Actions' %}</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{team.name}}</td>
<td>
<div class="scroll-150">
{{team.description}}
</div>
</td>
<td>
{% for member in team.users.all %}
<span class="badge badge-pill rlp-r">{{member.username}}</span>
{% endfor %}
</td>
<td>
{% if team.admin == user %}
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
{% fa5_icon 'edit' %}
</button>
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-remove' team.id %}" title="{% trans 'Remove team' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% with 'btn-modal' as btn_class %}
{% include 'modal/modal_form_script.html' %}
{% endwith %}
{% endblock %}

@ -15,5 +15,9 @@ urlpatterns = [
path("notifications/", notifications_view, name="notifications"), path("notifications/", notifications_view, name="notifications"),
path("token/api", api_token_view, name="api-token"), path("token/api", api_token_view, name="api-token"),
path("contact/<id>", contact_view, name="contact"), path("contact/<id>", contact_view, name="contact"),
path("team/", index_team_view, name="team-index"),
path("team/new", new_team_view, name="team-new"),
path("team/<id>/edit", edit_team_view, name="team-edit"),
path("team/<id>/remove", remove_team_view, name="team-remove"),
] ]

@ -1,17 +1,19 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID from konova.utils.message_templates import FORM_INVALID
from user.models import User from user.models import User, Team
from django.http import HttpRequest from django.http import HttpRequest, Http404
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required from konova.decorators import any_group_check, default_group_required
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \
RemoveTeamModalForm
@login_required @login_required
@ -128,4 +130,52 @@ def contact_view(request: HttpRequest, id: str):
request, request,
template, template,
context context
) )
@login_required
def index_team_view(request: HttpRequest):
template = "user/team/index.html"
user = request.user
context = {
"teams": user.teams.all(),
"tab_title": _("Teams"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_team_view(request: HttpRequest):
form = NewTeamModalForm(request.POST or None, request=request)
return form.process_request(
request,
_("New team added"),
redirect_url=reverse("user:team-index")
)
@login_required
def edit_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
if request.user != team.admin:
raise Http404()
form = EditTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Team edited"),
redirect_url=reverse("user:team-index")
)
@login_required
def remove_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
if request.user != team.admin:
raise Http404()
form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Team removed"),
redirect_url=reverse("user:team-index")
)

Loading…
Cancel
Save