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"]
|
||||
|
110
user/forms.py
110
user/forms.py
@ -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
|
||||
|
16
user/models/team.py
Normal file
16
user/models/team.py
Normal file
@ -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>
|
||||
|
67
user/templates/user/team/index.html
Normal file
67
user/templates/user/team/index.html
Normal file
@ -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…
Reference in New Issue
Block a user