#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 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, \
REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE
from user.models import User, UserActionLogEntry
REVOCATION_EDITED
from user.models import User
from user.models import UserActionLogEntry
from django.db import transaction
from django import forms
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, \
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
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.http import HttpRequest
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.generators import generate_random_string
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
from user.models import UserActionLogEntry, UserAction
class UuidModel(models.Model):
@ -50,14 +48,14 @@ class BaseResource(UuidModel):
A basic resource model, which defines attributes for every derived model
"""
created = models.ForeignKey(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+'
)
modified = models.ForeignKey(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -94,9 +92,9 @@ class BaseObject(BaseResource):
"""
identifier = 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)
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:
abstract = True
@ -105,7 +103,7 @@ class BaseObject(BaseResource):
def set_status_messages(self, request: HttpRequest):
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
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:
"""
from user.models import UserActionLogEntry
if self.deleted:
# Nothing to do here
return
@ -133,7 +132,7 @@ class BaseObject(BaseResource):
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
Args:
@ -144,13 +143,14 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = edit_action
self.log.add(edit_action)
self.save()
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
Args:
@ -161,6 +161,7 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
user_action = UserActionLogEntry.objects.create(
user=user,
action=action,
@ -229,7 +230,7 @@ class RecordableObjectMixin(models.Model):
"""
# Refers to "verzeichnen"
recorded = models.OneToOneField(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -240,7 +241,7 @@ class RecordableObjectMixin(models.Model):
class Meta:
abstract = True
def set_unrecorded(self, user: User):
def set_unrecorded(self, user):
""" Perform unrecording
Args:
@ -249,6 +250,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if not self.recorded:
return None
action = UserActionLogEntry.get_unrecorded_action(user)
@ -262,7 +264,7 @@ class RecordableObjectMixin(models.Model):
return action
def set_recorded(self, user: User):
def set_recorded(self, user):
""" Perform recording
Args:
@ -271,6 +273,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.recorded:
return None
action = UserActionLogEntry.get_recorded_action(user)
@ -284,7 +287,7 @@ class RecordableObjectMixin(models.Model):
return action
def unrecord(self, performing_user: User, request: HttpRequest = None):
def unrecord(self, performing_user, request: HttpRequest = None):
""" Unrecords a dataset
Args:
@ -318,7 +321,7 @@ class RecordableObjectMixin(models.Model):
class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional
checked = models.OneToOneField(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -346,7 +349,7 @@ class CheckableObjectMixin(models.Model):
self.save()
return None
def set_checked(self, user: User) -> UserActionLogEntry:
def set_checked(self, user):
""" Perform checking
Args:
@ -355,6 +358,7 @@ class CheckableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.checked:
# Nothing to do
return
@ -373,7 +377,7 @@ class CheckableObjectMixin(models.Model):
class ShareableObjectMixin(models.Model):
# 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(
max_length=255,
null=True,
@ -420,7 +424,7 @@ class ShareableObjectMixin(models.Model):
self.access_token = token
self.save()
def is_shared_with(self, user: User):
def is_shared_with(self, user):
""" Access check
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)
def share_with(self, user: User):
def share_with(self, user):
""" Adds user to list of shared access users
Args:
@ -465,6 +469,7 @@ class ShareableObjectMixin(models.Model):
Returns:
"""
from user.models import User
form_data = form.cleaned_data
keep_accessing_users = form_data["users"]

@ -5,17 +5,17 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 08.07.21
"""
from dal import autocomplete
from django import forms
from django.db import IntegrityError
from django.db import IntegrityError, transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from intervention.inputs import GenerateInput
from user.models import User
from user.models import User, UserNotification, Team
from konova.forms import BaseForm, BaseModalForm
from user.models import UserNotification
from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
class UserNotificationForm(BaseForm):
@ -160,3 +160,105 @@ class UserAPITokenForm(BaseForm):
user.api_token = new_token
user.save()
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
"""
from .user_action import *
from .user import *
from .notification import *
from .user_action import UserActionLogEntry, UserAction
from .user import User
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>
</a>
</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>

@ -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("token/api", api_token_view, name="api-token"),
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.auth.decorators import login_required
from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID
from user.models import User
from django.http import HttpRequest
from user.models import User, Team
from django.http import HttpRequest, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
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
@ -128,4 +130,52 @@ def contact_view(request: HttpRequest, id: str):
request,
template,
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