From 75c70ff8dcf7d7a6bbb093871dd73a51d5f36e09 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Feb 2022 13:13:32 +0100 Subject: [PATCH 1/8] #101 Team settings * adds first implementation for team managing --- intervention/forms/modalForms.py | 6 +- konova/models/object.py | 41 ++++++----- user/forms.py | 110 +++++++++++++++++++++++++++- user/models/__init__.py | 7 +- user/models/team.py | 16 ++++ user/templates/user/index.html | 8 ++ user/templates/user/team/index.html | 67 +++++++++++++++++ user/urls.py | 4 + user/views.py | 58 ++++++++++++++- 9 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 user/models/team.py create mode 100644 user/templates/user/team/index.html diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index a8fa98d..0b72ae8 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -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 _ diff --git a/konova/models/object.py b/konova/models/object.py index a6164f5..69a0a2e 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -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"] diff --git a/user/forms.py b/user/forms.py index fd66a91..1b509ba 100644 --- a/user/forms.py +++ b/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 diff --git a/user/models/__init__.py b/user/models/__init__.py index 7788d8e..06dbe73 100644 --- a/user/models/__init__.py +++ b/user/models/__init__.py @@ -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 diff --git a/user/models/team.py b/user/models/team.py new file mode 100644 index 0000000..c26af3c --- /dev/null +++ b/user/models/team.py @@ -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 diff --git a/user/templates/user/index.html b/user/templates/user/index.html index 1193340..c31de94 100644 --- a/user/templates/user/index.html +++ b/user/templates/user/index.html @@ -62,6 +62,14 @@ +
+ + + +
diff --git a/user/templates/user/team/index.html b/user/templates/user/team/index.html new file mode 100644 index 0000000..c8ce113 --- /dev/null +++ b/user/templates/user/team/index.html @@ -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 %} +

{% trans 'Teams' %}

+
+ +
+
+ + + + + + + + + + + {% for team in teams %} + + + + + + + {% endfor %} + +
{% trans 'Name' %}{% trans 'Description' %}{% trans 'Members' %}{% trans 'Actions' %}
{{team.name}} +
+ {{team.description}} +
+
+ {% for member in team.users.all %} + {{member.username}} + {% endfor %} + + {% if team.admin == user %} + + + {% endif %} +
+
+ +{% with 'btn-modal' as btn_class %} + {% include 'modal/modal_form_script.html' %} +{% endwith %} + +{% endblock %} \ No newline at end of file diff --git a/user/urls.py b/user/urls.py index e312a63..76a8662 100644 --- a/user/urls.py +++ b/user/urls.py @@ -15,5 +15,9 @@ urlpatterns = [ path("notifications/", notifications_view, name="notifications"), path("token/api", api_token_view, name="api-token"), path("contact/", contact_view, name="contact"), + path("team/", index_team_view, name="team-index"), + path("team/new", new_team_view, name="team-new"), + path("team//edit", edit_team_view, name="team-edit"), + path("team//remove", remove_team_view, name="team-remove"), ] \ No newline at end of file diff --git a/user/views.py b/user/views.py index ee6aee5..afed572 100644 --- a/user/views.py +++ b/user/views.py @@ -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 - ) \ No newline at end of file + ) + + +@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") + ) -- 2.38.5 From 9fec85b68804be115e8de66cd90da33e38a86db6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Feb 2022 13:44:32 +0100 Subject: [PATCH 2/8] #101 Team enhancements * visual enhancement for team index rendering * adds validity check for admin-membership of a team --- locale/de/LC_MESSAGES/django.mo | Bin 37936 -> 39382 bytes locale/de/LC_MESSAGES/django.po | 116 ++++++++++++++++++++++++---- user/forms.py | 20 ++++- user/templates/user/team/index.html | 88 ++++++++++----------- 4 files changed, 163 insertions(+), 61 deletions(-) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 022e9e1a728d186a0e8be95eee8ae29ea5928bee..ca93bc886faec14fea82f27aa2689b03373d5b42 100644 GIT binary patch delta 12271 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 delta 11048 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\n" "Language-Team: LANGUAGE \n" @@ -64,6 +64,7 @@ msgstr "Verantwortliche Stelle" #: compensation/forms/forms.py:165 intervention/forms/forms.py:64 #: intervention/forms/forms.py:81 intervention/forms/forms.py:97 #: intervention/forms/forms.py:113 intervention/forms/modalForms.py:49 +#: user/forms.py:196 msgid "Click for selection" msgstr "Auswählen..." @@ -751,7 +752,7 @@ msgstr "Menge" #: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/payments.html:39 #: intervention/templates/intervention/detail/includes/revocation.html:43 -#: templates/log.html:10 +#: templates/log.html:10 user/templates/user/team/index.html:33 msgid "Action" msgstr "Aktionen" @@ -1150,12 +1151,12 @@ msgstr "Kompensation {} bearbeitet" msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:240 compensation/views/eco_account.py:349 +#: compensation/views/compensation.py:240 compensation/views/eco_account.py:351 #: ema/views.py:194 intervention/views.py:531 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:584 compensation/views/eco_account.py:716 +#: compensation/views/compensation.py:584 compensation/views/eco_account.py:719 #: ema/views.py:551 intervention/views.py:677 msgid "Report {}" msgstr "Bericht {}" @@ -1176,32 +1177,32 @@ msgstr "Ökokonto {} bearbeitet" msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:370 ema/views.py:275 +#: compensation/views/eco_account.py:372 ema/views.py:275 #: intervention/views.py:630 msgid "{} unrecorded" msgstr "{} entzeichnet" -#: compensation/views/eco_account.py:370 ema/views.py:275 +#: compensation/views/eco_account.py:372 ema/views.py:275 #: intervention/views.py:630 msgid "{} recorded" msgstr "{} verzeichnet" -#: compensation/views/eco_account.py:789 ema/views.py:617 +#: compensation/views/eco_account.py:792 ema/views.py:617 #: intervention/views.py:428 msgid "{} has already been shared with you" msgstr "{} wurde bereits für Sie freigegeben" -#: compensation/views/eco_account.py:794 ema/views.py:622 +#: compensation/views/eco_account.py:797 ema/views.py:622 #: intervention/views.py:433 msgid "{} has been shared with you" msgstr "{} ist nun für Sie freigegeben" -#: compensation/views/eco_account.py:801 ema/views.py:629 +#: compensation/views/eco_account.py:804 ema/views.py:629 #: intervention/views.py:440 msgid "Share link invalid" msgstr "Freigabelink ungültig" -#: compensation/views/eco_account.py:824 ema/views.py:652 +#: compensation/views/eco_account.py:827 ema/views.py:652 #: intervention/views.py:463 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" @@ -2272,7 +2273,7 @@ msgstr "* sind Pflichtfelder." msgid "New entry" msgstr "Neuer Eintrag" -#: templates/generic_index.html:41 +#: templates/generic_index.html:41 user/templates/user/team/index.html:23 msgid "New" msgstr "Neu" @@ -2401,6 +2402,54 @@ msgstr "Neuen Token generieren" msgid "A new token needs to be validated by an administrator!" msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" +#: user/forms.py:168 user/forms.py:172 +msgid "Team name" +msgstr "Team Name" + +#: user/forms.py:179 user/templates/user/team/index.html:31 +msgid "Description" +msgstr "Beschreibung" + +#: user/forms.py:188 +msgid "Manage team members" +msgstr "Mitglieder verwalten" + +#: user/forms.py:190 +msgid "" +"Multiple selection possible - You can only select users which are not " +"already a team member. Enter the full username or e-mail." +msgstr "" +"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht " +"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an." + +#: user/forms.py:204 +msgid "Create new team" +msgstr "Neues Team anlegen" + +#: user/forms.py:205 +msgid "" +"You will become the administrator for this group by default. You do not need " +"to add yourself to the list of members." +msgstr "" +"Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich " +"selbst nicht zur Liste der Mitglieder hinzufügen." + +#: user/forms.py:230 +msgid "Admin" +msgstr "Administrator" + +#: user/forms.py:231 +msgid "Administrators manage team details and members" +msgstr "Administratoren verwalten die Teamdaten und Mitglieder" + +#: user/forms.py:244 +msgid "Selected admin ({}) needs to be a member of this team." +msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein." + +#: user/forms.py:256 user/templates/user/team/index.html:52 +msgid "Edit team" +msgstr "Team bearbeiten" + #: user/models/user_action.py:22 msgid "Unrecorded" msgstr "Entzeichnet" @@ -2417,7 +2466,7 @@ msgstr "Gelöscht" msgid "Show contact data" msgstr "Zeige Kontaktdaten" -#: user/templates/user/index.html:13 +#: user/templates/user/index.html:13 user/templates/user/team/index.html:30 msgid "Name" msgstr "" @@ -2462,6 +2511,27 @@ msgstr "API token einsehen oder neu generieren" msgid "API" msgstr "" +#: user/templates/user/index.html:66 +msgid "Manage teams" +msgstr "" + +#: user/templates/user/index.html:69 user/templates/user/team/index.html:19 +#: user/views.py:142 +msgid "Teams" +msgstr "" + +#: user/templates/user/team/index.html:21 +msgid "Add new team" +msgstr "Neues Team hinzufügen" + +#: user/templates/user/team/index.html:32 +msgid "Members" +msgstr "Mitglieder" + +#: user/templates/user/team/index.html:55 +msgid "Remove team" +msgstr "Team entfernen" + #: user/templates/user/token.html:6 msgid "API settings" msgstr "API Einstellungen" @@ -2486,26 +2556,38 @@ msgstr "Token noch nicht freigeschaltet" msgid "Valid until" msgstr "Läuft ab am" -#: user/views.py:31 +#: user/views.py:33 msgid "User settings" msgstr "Einstellungen" -#: user/views.py:57 +#: user/views.py:59 msgid "Notifications edited" msgstr "Benachrichtigungen bearbeitet" -#: user/views.py:69 +#: user/views.py:71 msgid "User notifications" msgstr "Benachrichtigungen" -#: user/views.py:92 +#: user/views.py:94 msgid "New token generated. Administrators need to validate." msgstr "Neuer Token generiert. Administratoren sind informiert." -#: user/views.py:103 +#: user/views.py:105 msgid "User API token" msgstr "API Nutzer Token" +#: user/views.py:153 +msgid "New team added" +msgstr "Neues Team hinzugefügt" + +#: user/views.py:166 +msgid "Team edited" +msgstr "Team bearbeitet" + +#: user/views.py:179 +msgid "Team removed" +msgstr "Team gelöscht" + #: venv/lib/python3.7/site-packages/bootstrap4/components.py:17 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 diff --git a/user/forms.py b/user/forms.py index 1b509ba..35c1e82 100644 --- a/user/forms.py +++ b/user/forms.py @@ -233,10 +233,27 @@ class EditTeamModalForm(NewTeamModalForm): empty_label=None, ) + def __is_admin_valid(self): + admin = self.cleaned_data.get("admin", None) + members = self.cleaned_data.get("members", None) + _is_valid = admin in members + + if not _is_valid: + self.add_error( + "members", + _("Selected admin ({}) needs to be a member of this team.").format(admin.username) + ) + + return _is_valid + + def is_valid(self): + super_valid = super().is_valid() + admin_valid = self.__is_admin_valid() + return super_valid and admin_valid + 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") @@ -255,6 +272,7 @@ class EditTeamModalForm(NewTeamModalForm): with transaction.atomic(): self.instance.name = self.cleaned_data.get("name", None) self.instance.description = self.cleaned_data.get("description", None) + self.instance.admin = self.cleaned_data.get("admin", None) self.instance.save() self.instance.users.set(self.cleaned_data.get("members", [])) return self.instance diff --git a/user/templates/user/team/index.html b/user/templates/user/team/index.html index c8ce113..d2040a3 100644 --- a/user/templates/user/team/index.html +++ b/user/templates/user/team/index.html @@ -13,51 +13,53 @@ {% endblock %} {% block body %} -

{% trans 'Teams' %}

-
- -
-
- - - - - - - - - - - {% for team in teams %} +
+

{% trans 'Teams' %}

+
+ +
+
+
{% trans 'Name' %}{% trans 'Description' %}{% trans 'Members' %}{% trans 'Actions' %}
+ - - - - + + + + - {% endfor %} - -
{{team.name}} -
- {{team.description}} -
-
- {% for member in team.users.all %} - {{member.username}} - {% endfor %} - - {% if team.admin == user %} - - - {% endif %} - {% trans 'Name' %}{% trans 'Description' %}{% trans 'Members' %}{% trans 'Action' %}
+ + + {% for team in teams %} + + {{team.name}} + +
+ {{team.description}} +
+ + + {% for member in team.users.all %} + {{member.username}} + {% endfor %} + + + {% if team.admin == user %} + + + {% endif %} + + + {% endfor %} + + +
{% with 'btn-modal' as btn_class %} -- 2.38.5 From 3878b5dbdb3144aae778cc8298f136ecabaa9745 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Feb 2022 15:07:25 +0100 Subject: [PATCH 3/8] WIP: #101 Team sharing form * adds form for sharing via team --- intervention/forms/modalForms.py | 23 ++++++++++++++++++++++- konova/autocompletes.py | 22 +++++++++++++++++++--- konova/urls.py | 3 ++- user/migrations/0003_team.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 user/migrations/0003_team.py diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 0b72ae8..978360d 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -10,7 +10,7 @@ from django.core.exceptions import ObjectDoesNotExist from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ REVOCATION_EDITED -from user.models import User +from user.models import User, Team from user.models import UserActionLogEntry from django.db import transaction from django import forms @@ -37,6 +37,20 @@ class ShareModalForm(BaseModalForm): } ) ) + team_select = forms.ModelMultipleChoiceField( + label=_("Add team to share with"), + label_suffix="", + help_text=_("Multiple selection possible - You can only select teams which do not already have access."), + required=False, + queryset=Team.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url="share-team-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + }, + ), + ) user_select = forms.ModelMultipleChoiceField( label=_("Add user to share with"), label_suffix="", @@ -97,6 +111,13 @@ class ShareModalForm(BaseModalForm): self.disable_form_field("users") self._add_user_choices_to_field() + self._add_teams_to_field() + + def _add_teams_to_field(self): + form_data = { + "teams": [] + } + self.load_initial_data(form_data) def _add_user_choices_to_field(self): """ Transforms the instance's sharing users into a list for the form field diff --git a/konova/autocompletes.py b/konova/autocompletes.py index f1775b5..9b1be2d 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -11,7 +11,7 @@ from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView from django.core.exceptions import ImproperlyConfigured from konova.utils.message_templates import UNGROUPED -from user.models import User +from user.models import User, Team from django.db.models import Q from codelist.models import KonovaCode @@ -69,9 +69,8 @@ class InterventionAutocomplete(Select2QuerySetView): class ShareUserAutocomplete(Select2QuerySetView): - """ Autocomplete for intervention entries + """ Autocomplete for share with single users - Only returns entries that are accessible for the requesting user """ def get_queryset(self): @@ -93,6 +92,23 @@ class ShareUserAutocomplete(Select2QuerySetView): return qs +class ShareTeamAutocomplete(Select2QuerySetView): + """ Autocomplete for share with teams + + """ + def get_queryset(self): + if self.request.user.is_anonymous: + return Team.objects.none() + if self.q: + # Due to privacy concerns only a full username match will return the proper user entry + qs = Team.objects.filter( + Q(name__icontains=self.q) + ).order_by( + "name" + ) + return qs + + class KonovaCodeAutocomplete(Select2GroupQuerySetView): """ Provides simple autocomplete functionality for codes diff --git a/konova/urls.py b/konova/urls.py index 68256e7..1cd8851 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -20,7 +20,7 @@ from django.urls import path, include from konova.autocompletes import EcoAccountAutocomplete, \ InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \ - ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete + ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, ShareTeamAutocomplete from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG from konova.sso.sso import KonovaSSOClient from konova.views import logout_view, home_view @@ -52,6 +52,7 @@ urlpatterns = [ path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"), path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"), path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), + path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"), ] if DEBUG: diff --git a/user/migrations/0003_team.py b/user/migrations/0003_team.py new file mode 100644 index 0000000..6f7ac90 --- /dev/null +++ b/user/migrations/0003_team.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.3 on 2022-02-17 10:22 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_user_api_token'), + ] + + operations = [ + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=500, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('users', models.ManyToManyField(blank=True, related_name='teams', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] -- 2.38.5 From e152dfd4d70b08cdd983a42d102bcd54794693ad Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 11:02:40 +0100 Subject: [PATCH 4/8] #101 Team sharing form * adds team sharing field to share form * splits sharing logic into user based and teams based * adds TeamAdmin for admin backend * adds validity check on Team name -> only unused names shall be valid --- api/tests/v1/create/test_api_create.py | 4 +- api/tests/v1/delete/test_api_delete.py | 10 +- api/tests/v1/get/test_api_get.py | 10 +- api/tests/v1/update/test_api_update.py | 12 +-- api/views/views.py | 2 +- compensation/forms/forms.py | 2 +- compensation/tests/compensation/test_views.py | 8 +- .../tests/compensation/test_workflow.py | 2 +- compensation/tests/ecoaccount/test_views.py | 8 +- .../tests/ecoaccount/test_workflow.py | 14 +-- compensation/tests/payment/test_views.py | 8 +- compensation/tests/payment/test_workflow.py | 2 +- compensation/views/eco_account.py | 2 +- ema/forms.py | 2 +- ema/tests/test_views.py | 8 +- ema/views.py | 2 +- intervention/forms/forms.py | 2 +- intervention/forms/modalForms.py | 2 +- intervention/tests/test_views.py | 12 +-- intervention/tests/test_workflow.py | 4 +- intervention/views.py | 2 +- konova/models/object.py | 56 ++++++++-- locale/de/LC_MESSAGES/django.mo | Bin 39382 -> 39754 bytes locale/de/LC_MESSAGES/django.po | 102 ++++++++++-------- user/admin.py | 15 ++- user/forms.py | 35 ++++++ 26 files changed, 213 insertions(+), 113 deletions(-) diff --git a/api/tests/v1/create/test_api_create.py b/api/tests/v1/create/test_api_create.py index 72ece97..51a82e7 100644 --- a/api/tests/v1/create/test_api_create.py +++ b/api/tests/v1/create/test_api_create.py @@ -109,8 +109,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase): Returns: """ - self.intervention.share_with(self.superuser) - self.eco_account.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) + self.eco_account.share_with_user(self.superuser) url = reverse("api:v1:deduction") json_file_path = "api/tests/v1/create/deduction_create_post_body.json" diff --git a/api/tests/v1/delete/test_api_delete.py b/api/tests/v1/delete/test_api_delete.py index cd016cf..350fdf2 100644 --- a/api/tests/v1/delete/test_api_delete.py +++ b/api/tests/v1/delete/test_api_delete.py @@ -57,7 +57,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase): """ test_intervention = self.create_dummy_intervention() - test_intervention.share_with(self.superuser) + test_intervention.share_with_user(self.superuser) url = reverse("api:v1:intervention", args=(str(test_intervention.id),)) self._test_delete_object(test_intervention, url) @@ -68,7 +68,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase): """ test_comp = self.create_dummy_compensation() - test_comp.share_with(self.superuser) + test_comp.share_with_user(self.superuser) url = reverse("api:v1:compensation", args=(str(test_comp.id),)) self._test_delete_object(test_comp, url) @@ -79,7 +79,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase): """ test_acc = self.create_dummy_eco_account() - test_acc.share_with(self.superuser) + test_acc.share_with_user(self.superuser) url = reverse("api:v1:ecoaccount", args=(str(test_acc.id),)) self._test_delete_object(test_acc, url) @@ -90,7 +90,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase): """ test_ema = self.create_dummy_ema() - test_ema.share_with(self.superuser) + test_ema.share_with_user(self.superuser) url = reverse("api:v1:ema", args=(str(test_ema.id),)) self._test_delete_object(test_ema, url) @@ -101,7 +101,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase): """ test_deduction = self.create_dummy_deduction() - test_deduction.intervention.share_with(self.superuser) + test_deduction.intervention.share_with_user(self.superuser) url = reverse("api:v1:deduction", args=(str(test_deduction.id),)) response = self._run_delete_request(url) diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py index 3c46454..5bfd67a 100644 --- a/api/tests/v1/get/test_api_get.py +++ b/api/tests/v1/get/test_api_get.py @@ -64,7 +64,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase): Returns: """ - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) url = reverse("api:v1:intervention", args=(str(self.intervention.id),)) geojson = self._test_get_object(self.intervention, url) self._assert_geojson_format(geojson) @@ -91,7 +91,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase): Returns: """ - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) self.compensation.intervention = self.intervention self.compensation.save() @@ -119,7 +119,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase): Returns: """ - self.eco_account.share_with(self.superuser) + self.eco_account.share_with_user(self.superuser) url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),)) geojson = self._test_get_object(self.eco_account, url) @@ -148,7 +148,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase): Returns: """ - self.ema.share_with(self.superuser) + self.ema.share_with_user(self.superuser) url = reverse("api:v1:ema", args=(str(self.ema.id),)) geojson = self._test_get_object(self.ema, url) @@ -172,7 +172,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase): Returns: """ - self.deduction.intervention.share_with(self.superuser) + self.deduction.intervention.share_with_user(self.superuser) url = reverse("api:v1:deduction", args=(str(self.deduction.id),)) _json = self._test_get_object(self.deduction, url) diff --git a/api/tests/v1/update/test_api_update.py b/api/tests/v1/update/test_api_update.py index e371188..8689fbe 100644 --- a/api/tests/v1/update/test_api_update.py +++ b/api/tests/v1/update/test_api_update.py @@ -52,7 +52,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): Returns: """ - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) modified_on = self.intervention.modified url = reverse("api:v1:intervention", args=(str(self.intervention.id),)) json_file_path = "api/tests/v1/update/intervention_update_put_body.json" @@ -79,7 +79,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): """ self.compensation.intervention = self.intervention self.compensation.save() - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) modified_on = self.compensation.modified url = reverse("api:v1:compensation", args=(str(self.compensation.id),)) @@ -108,7 +108,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): Returns: """ - self.eco_account.share_with(self.superuser) + self.eco_account.share_with_user(self.superuser) modified_on = self.eco_account.modified url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),)) @@ -139,7 +139,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): Returns: """ - self.ema.share_with(self.superuser) + self.ema.share_with_user(self.superuser) modified_on = self.ema.modified url = reverse("api:v1:ema", args=(str(self.ema.id),)) @@ -168,8 +168,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): Returns: """ - self.deduction.intervention.share_with(self.superuser) - self.deduction.account.share_with(self.superuser) + self.deduction.intervention.share_with_user(self.superuser) + self.deduction.account.share_with_user(self.superuser) url = reverse("api:v1:deduction", args=(str(self.deduction.id),)) json_file_path = "api/tests/v1/update/deduction_update_put_body.json" diff --git a/api/views/views.py b/api/views/views.py index 35c54fe..db8b8f9 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -292,7 +292,7 @@ class AbstractModelShareAPIView(AbstractAPIView): id__in=obj.shared_users ) new_users_objs = obj.shared_users.union(new_users_to_be_added) - obj.share_with_list(new_users_objs) + obj.share_with_user_list(new_users_objs) return True diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py index 72b1a71..46b235f 100644 --- a/compensation/forms/forms.py +++ b/compensation/forms/forms.py @@ -400,7 +400,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix comment=comment, legal=legal ) - acc.share_with(user) + acc.share_with_user(user) # Add the log entry to the main objects log list acc.log.add(action) diff --git a/compensation/tests/compensation/test_views.py b/compensation/tests/compensation/test_views.py index 27218f2..1174895 100644 --- a/compensation/tests/compensation/test_views.py +++ b/compensation/tests/compensation/test_views.py @@ -103,7 +103,7 @@ class CompensationViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) # Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference # to a user without access, since the important permissions are missing @@ -143,7 +143,7 @@ class CompensationViewTestCase(BaseViewTestCase): client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) # Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference # to a user having shared access, since all important permissions are missing @@ -185,7 +185,7 @@ class CompensationViewTestCase(BaseViewTestCase): group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -221,7 +221,7 @@ class CompensationViewTestCase(BaseViewTestCase): group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) success_urls = [ self.index_url, diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py index 7b73be8..5b7decf 100644 --- a/compensation/tests/compensation/test_workflow.py +++ b/compensation/tests/compensation/test_workflow.py @@ -25,7 +25,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): super().setUp() # Give the user shared access to the dummy intervention -> inherits the access to the compensation - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) # Make sure the intervention itself would be fine with valid data self.intervention = self.fill_out_intervention(self.intervention) diff --git a/compensation/tests/ecoaccount/test_views.py b/compensation/tests/ecoaccount/test_views.py index 617f743..aaa7a4c 100644 --- a/compensation/tests/ecoaccount/test_views.py +++ b/compensation/tests/ecoaccount/test_views.py @@ -78,7 +78,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) - self.eco_account.share_with_list([self.superuser]) + self.eco_account.share_with_user_list([self.superuser]) # Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference # to a user without access, since the important permissions are missing @@ -119,7 +119,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) - self.eco_account.share_with_list([]) + self.eco_account.share_with_user_list([]) # Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference # to a user having shared access, since all important permissions are missing @@ -163,7 +163,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase): group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.eco_account.share_with_list([self.superuser]) + self.eco_account.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -200,7 +200,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase): client.login(username=self.superuser.username, password=self.superuser_pw) group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) - self.eco_account.share_with_list([]) + self.eco_account.share_with_user_list([]) success_urls = [ self.index_url, diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index afc4111..1b350e9 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -27,7 +27,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): # Add user to conservation office group and give shared access to the account self.superuser.groups.add(self.groups.get(name=DEFAULT_GROUP)) self.superuser.groups.add(self.groups.get(name=ETS_GROUP)) - self.eco_account.share_with_list([self.superuser]) + self.eco_account.share_with_user_list([self.superuser]) def test_new(self): """ Test the creation of an EcoAccount @@ -73,7 +73,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): Returns: """ - self.eco_account.share_with(self.superuser) + self.eco_account.share_with_user(self.superuser) url = reverse("compensation:acc:edit", args=(self.eco_account.id,)) pre_edit_log_count = self.eco_account.log.count() @@ -129,7 +129,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): """ # Add proper privilege for the user - self.eco_account.share_with(self.superuser) + self.eco_account.share_with_user(self.superuser) pre_record_log_count = self.eco_account.log.count() # Prepare url and form data @@ -178,7 +178,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): """ # Give user shared access to the dummy intervention, which will be needed here - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) pre_deduction_acc_log_count = self.eco_account.log.count() pre_deduction_int_log_count = self.intervention.log.count() @@ -231,7 +231,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): def test_edit_deduction(self): test_surface = self.eco_account.get_available_rest()[0] self.eco_account.set_recorded(self.superuser) - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) self.eco_account.refresh_from_db() self.assertIn(self.superuser, self.intervention.is_shared_with(self.superuser)) @@ -281,8 +281,8 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): "confirm": True, } - intervention.share_with(self.superuser) - account.share_with(self.superuser) + intervention.share_with_user(self.superuser) + account.share_with_user(self.superuser) pre_edit_intervention_log_count = intervention.log.count() pre_edit_account_log_count = account.log.count() diff --git a/compensation/tests/payment/test_views.py b/compensation/tests/payment/test_views.py index b1eca5a..30ffa00 100644 --- a/compensation/tests/payment/test_views.py +++ b/compensation/tests/payment/test_views.py @@ -64,7 +64,7 @@ class PaymentViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) # Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference # to a user without access, since the important permissions are missing @@ -91,7 +91,7 @@ class PaymentViewTestCase(BaseViewTestCase): client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) # Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference # to a user having shared access, since all important permissions are missing @@ -120,7 +120,7 @@ class PaymentViewTestCase(BaseViewTestCase): group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) success_urls = [ self.new_url, @@ -143,7 +143,7 @@ class PaymentViewTestCase(BaseViewTestCase): group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) success_urls = [ ] diff --git a/compensation/tests/payment/test_workflow.py b/compensation/tests/payment/test_workflow.py index 790fb61..81259b8 100644 --- a/compensation/tests/payment/test_workflow.py +++ b/compensation/tests/payment/test_workflow.py @@ -21,7 +21,7 @@ class PaymentWorkflowTestCase(BaseWorkflowTestCase): def setUp(self) -> None: super().setUp() # Give the user shared access to the dummy intervention - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) self.payment = Payment.objects.get_or_create( intervention=self.intervention, diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index d291218..85b1371 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -796,7 +796,7 @@ def share_view(request: HttpRequest, id: str, token: str): request, _("{} has been shared with you").format(obj.identifier) ) - obj.share_with(user) + obj.share_with_user(user) return redirect("compensation:acc:detail", id=id) else: messages.error( diff --git a/ema/forms.py b/ema/forms.py index 2f19360..8e6faab 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -80,7 +80,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): ) # Add the creating user to the list of shared users - acc.share_with(user) + acc.share_with_user(user) # Add the log entry to the main objects log list acc.log.add(action) diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 9654e8f..b2c23e1 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -110,7 +110,7 @@ class EmaViewTestCase(CompensationViewTestCase): # Sharing does not have any effect in here, since the default group will prohibit further functionality access # to this user - self.ema.share_with_list([self.superuser]) + self.ema.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -160,7 +160,7 @@ class EmaViewTestCase(CompensationViewTestCase): # Sharing does not have any effect in here, since the default group will prohibit further functionality access # to this user - self.ema.share_with_list([]) + self.ema.share_with_user_list([]) success_urls = [ self.index_url, @@ -203,7 +203,7 @@ class EmaViewTestCase(CompensationViewTestCase): groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP)) self.superuser.groups.set(groups) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.ema.share_with_list([self.superuser]) + self.ema.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -243,7 +243,7 @@ class EmaViewTestCase(CompensationViewTestCase): groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP)) self.superuser.groups.set(groups) # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state - self.ema.share_with_list([]) + self.ema.share_with_user_list([]) success_urls = [ self.index_url, diff --git a/ema/views.py b/ema/views.py index e9d0acb..c145511 100644 --- a/ema/views.py +++ b/ema/views.py @@ -621,7 +621,7 @@ def share_view(request: HttpRequest, id: str, token: str): request, _("{} has been shared with you").format(obj.identifier) ) - obj.share_with(user) + obj.share_with_user(user) return redirect("ema:detail", id=id) else: messages.error( diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py index 1c5cf1c..94ff970 100644 --- a/intervention/forms/forms.py +++ b/intervention/forms/forms.py @@ -253,7 +253,7 @@ class NewInterventionForm(BaseForm): intervention.log.add(action) # Add the performing user as the first user having access to the data - intervention.share_with(user) + intervention.share_with_user(user) return intervention diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 978360d..8e44ac2 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -115,7 +115,7 @@ class ShareModalForm(BaseModalForm): def _add_teams_to_field(self): form_data = { - "teams": [] + "team_select": self.instance.teams.all() } self.load_initial_data(form_data) diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py index f12ee7a..a049f3e 100644 --- a/intervention/tests/test_views.py +++ b/intervention/tests/test_views.py @@ -144,7 +144,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to default group default_group = Group.objects.get(name=DEFAULT_GROUP) self.superuser.groups.set([default_group]) - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -190,7 +190,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to default group default_group = Group.objects.get(name=DEFAULT_GROUP) self.superuser.groups.set([default_group]) - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) success_urls = [ self.index_url, @@ -236,7 +236,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to zb group zb_group = self.groups.get(name=ZB_GROUP) self.superuser.groups.set([zb_group]) - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -282,7 +282,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to zb group zb_group = self.groups.get(name=ZB_GROUP) self.superuser.groups.set([zb_group]) - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) success_urls = [ self.index_url, @@ -328,7 +328,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to ets group ets_group = Group.objects.get(name=ETS_GROUP) self.superuser.groups.set([ets_group]) - self.intervention.share_with_list([self.superuser]) + self.intervention.share_with_user_list([self.superuser]) success_urls = [ self.index_url, @@ -374,7 +374,7 @@ class InterventionViewTestCase(BaseViewTestCase): # Add user to default group ets_group = Group.objects.get(name=ETS_GROUP) self.superuser.groups.set([ets_group]) - self.intervention.share_with_list([]) + self.intervention.share_with_user_list([]) success_urls = [ self.index_url, diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py index 1a66245..c529050 100644 --- a/intervention/tests/test_workflow.py +++ b/intervention/tests/test_workflow.py @@ -30,7 +30,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): super().setUp() # Recreate a new (bare minimum) intervention before each test self.intervention = self.create_dummy_intervention() - self.intervention.share_with(self.superuser) + self.intervention.share_with_user(self.superuser) def test_new(self): """ @@ -365,7 +365,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): if self.eco_account.recorded is None: rec_action = UserActionLogEntry.get_recorded_action(self.superuser) self.eco_account.recorded = rec_action - self.eco_account.share_with_list([self.superuser]) + self.eco_account.share_with_user_list([self.superuser]) self.eco_account.save() num_all_deducs = EcoAccountDeduction.objects.count() diff --git a/intervention/views.py b/intervention/views.py index 00440bb..3004a79 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -432,7 +432,7 @@ def share_view(request: HttpRequest, id: str, token: str): request, _("{} has been shared with you").format(intervention.identifier) ) - intervention.share_with(user) + intervention.share_with_user(user) return redirect("intervention:detail", id=id) else: messages.error( diff --git a/konova/models/object.py b/konova/models/object.py index 69a0a2e..e6c27a6 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -378,6 +378,7 @@ class CheckableObjectMixin(models.Model): class ShareableObjectMixin(models.Model): # Users having access on this object users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)") + teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)") access_token = models.CharField( max_length=255, null=True, @@ -435,9 +436,36 @@ class ShareableObjectMixin(models.Model): Returns: """ - return self.users.filter(id=user.id) + directly_shared = self.users.filter(id=user.id).exists() + team_shared = self.teams.filter( + users__in=[user] + ).exists() + is_shared = directly_shared or team_shared + return is_shared - def share_with(self, user): + def share_with_team(self, team): + """ Adds team to list of shared access teans + + Args: + team (Team): The team to be added to the object + + Returns: + + """ + self.teams.add(team) + + def share_with_team_list(self, team_list: list): + """ Sets the list of shared access teams + + Args: + team_list (list): The teams to be added to the object + + Returns: + + """ + self.teams.set(team_list) + + def share_with_user(self, user): """ Adds user to list of shared access users Args: @@ -449,7 +477,7 @@ class ShareableObjectMixin(models.Model): if not self.is_shared_with(user): self.users.add(user) - def share_with_list(self, user_list: list): + def share_with_user_list(self, user_list: list): """ Sets the list of shared access users Args: @@ -472,24 +500,32 @@ class ShareableObjectMixin(models.Model): from user.models import User form_data = form.cleaned_data - keep_accessing_users = form_data["users"] + # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent + accessing_teams = form_data["team_select"] + removed_team_users = self.teams.all().exclude( + id__in=accessing_teams + ).values_list("users__id", flat=True) + new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) + keep_accessing_users = form_data["users"] accessing_users = keep_accessing_users + new_accessing_users users = User.objects.filter( id__in=accessing_users ) removed_users = self.users.all().exclude( id__in=accessing_users - ).values("id") + ).values_list("id", flat=True) + removed_users = removed_users.union(removed_team_users) # Send mails - for user in removed_users: - celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user["id"]) - for user in new_accessing_users: - celery_send_mail_shared_access_given.delay(self.identifier, self.title, user) + for user_id in removed_users: + celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id) + for user_id in new_accessing_users: + celery_send_mail_shared_access_given.delay(self.identifier, self.title, user_id) # Set new shared users - self.share_with_list(users) + self.share_with_user_list(users) + self.share_with_team_list(accessing_teams) @property def shared_users(self) -> QuerySet: diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index ca93bc886faec14fea82f27aa2689b03373d5b42..a20ac2daaaa222c759f4d6a6e75bd9e7c9424ee3 100644 GIT binary patch delta 11420 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 U@_a+OM&>omhzQ8boV-8!f8%^NWB>pF diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index d1da918..aba7981 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ #: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63 #: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463 #: intervention/forms/forms.py:54 intervention/forms/forms.py:156 -#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:127 -#: intervention/forms/modalForms.py:140 intervention/forms/modalForms.py:153 +#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:148 +#: intervention/forms/modalForms.py:161 intervention/forms/modalForms.py:174 #: konova/filters/mixins.py:53 konova/filters/mixins.py:54 #: konova/filters/mixins.py:81 konova/filters/mixins.py:82 #: konova/filters/mixins.py:94 konova/filters/mixins.py:95 @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-17 13:42+0100\n" +"POT-Creation-Date: 2022-02-18 09:35+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -64,7 +64,7 @@ msgstr "Verantwortliche Stelle" #: compensation/forms/forms.py:165 intervention/forms/forms.py:64 #: intervention/forms/forms.py:81 intervention/forms/forms.py:97 #: intervention/forms/forms.py:113 intervention/forms/modalForms.py:49 -#: user/forms.py:196 +#: intervention/forms/modalForms.py:63 user/forms.py:196 msgid "Click for selection" msgstr "Auswählen..." @@ -221,7 +221,7 @@ msgstr "Abbuchungen" #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 -#: intervention/forms/modalForms.py:338 +#: intervention/forms/modalForms.py:359 msgid "Surface" msgstr "Fläche" @@ -284,8 +284,8 @@ msgid "Type" msgstr "Typ" #: analysis/templates/analysis/reports/includes/old_data/amount.html:24 -#: compensation/tables.py:89 intervention/forms/modalForms.py:349 -#: intervention/forms/modalForms.py:356 intervention/tables.py:88 +#: compensation/tables.py:89 intervention/forms/modalForms.py:370 +#: intervention/forms/modalForms.py:377 intervention/tables.py:88 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/navbars/navbar.html:22 @@ -295,7 +295,7 @@ msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: compensation/tables.py:266 #: compensation/templates/compensation/detail/eco_account/view.html:20 -#: intervention/forms/modalForms.py:322 intervention/forms/modalForms.py:329 +#: intervention/forms/modalForms.py:343 intervention/forms/modalForms.py:350 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -364,7 +364,7 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/actions.html:34 #: ema/templates/ema/detail/includes/deadlines.html:34 #: ema/templates/ema/detail/includes/documents.html:34 -#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:152 +#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:173 #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 @@ -484,7 +484,7 @@ msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357 -#: intervention/forms/modalForms.py:154 konova/forms.py:395 +#: intervention/forms/modalForms.py:175 konova/forms.py:395 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -512,7 +512,7 @@ msgstr "Zusatzbezeichnung" msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:340 +#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:361 msgid "in m²" msgstr "" @@ -540,7 +540,7 @@ msgstr "Fristart wählen" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31 -#: intervention/forms/modalForms.py:126 +#: intervention/forms/modalForms.py:147 msgid "Date" msgstr "Datum" @@ -752,7 +752,7 @@ msgstr "Menge" #: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/payments.html:39 #: intervention/templates/intervention/detail/includes/revocation.html:43 -#: templates/log.html:10 user/templates/user/team/index.html:33 +#: templates/log.html:10 user/templates/user/team/index.html:32 msgid "Action" msgstr "Aktionen" @@ -1000,14 +1000,14 @@ msgstr "Zuletzt bearbeitet" #: compensation/templates/compensation/detail/compensation/view.html:100 #: compensation/templates/compensation/detail/eco_account/view.html:83 -#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:56 +#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:70 #: intervention/templates/intervention/detail/view.html:116 msgid "Shared with" msgstr "Freigegeben für" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15 -#: intervention/forms/modalForms.py:70 +#: intervention/forms/modalForms.py:84 #: intervention/templates/intervention/detail/includes/controls.html:15 msgid "Share" msgstr "Freigabe" @@ -1325,10 +1325,22 @@ msgid "Send this link to users who you want to have writing access on the data" msgstr "Andere Nutzer erhalten über diesen Link Zugriff auf die Daten" #: intervention/forms/modalForms.py:41 +msgid "Add team to share with" +msgstr "Team hinzufügen" + +#: intervention/forms/modalForms.py:43 +msgid "" +"Multiple selection possible - You can only select teams which do not already " +"have access." +msgstr "" +"Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag " +"noch nicht freigegeben wurde." + +#: intervention/forms/modalForms.py:55 msgid "Add user to share with" msgstr "Nutzer direkt hinzufügen" -#: intervention/forms/modalForms.py:43 +#: intervention/forms/modalForms.py:57 msgid "" "Multiple selection possible - You can only select users which do not already " "have access. Enter the full username." @@ -1336,46 +1348,46 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." -#: intervention/forms/modalForms.py:59 +#: intervention/forms/modalForms.py:73 msgid "Remove check to remove access for this user" msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen" -#: intervention/forms/modalForms.py:71 +#: intervention/forms/modalForms.py:85 msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" -#: intervention/forms/modalForms.py:128 +#: intervention/forms/modalForms.py:149 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modalForms.py:139 +#: intervention/forms/modalForms.py:160 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modalForms.py:142 +#: intervention/forms/modalForms.py:163 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modalForms.py:167 +#: intervention/forms/modalForms.py:188 #: intervention/templates/intervention/detail/includes/revocation.html:18 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modalForms.py:224 +#: intervention/forms/modalForms.py:245 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:230 +#: intervention/forms/modalForms.py:251 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:239 +#: intervention/forms/modalForms.py:260 #: intervention/templates/intervention/detail/includes/controls.html:19 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:240 konova/forms.py:514 +#: intervention/forms/modalForms.py:261 konova/forms.py:514 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1383,23 +1395,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:324 +#: intervention/forms/modalForms.py:345 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:351 +#: intervention/forms/modalForms.py:372 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:364 +#: intervention/forms/modalForms.py:385 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:365 +#: intervention/forms/modalForms.py:386 msgid "Enter the information for a new deduction from a chosen eco-account" msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." -#: intervention/forms/modalForms.py:408 +#: intervention/forms/modalForms.py:429 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1407,7 +1419,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:418 +#: intervention/forms/modalForms.py:439 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -2273,7 +2285,7 @@ msgstr "* sind Pflichtfelder." msgid "New entry" msgstr "Neuer Eintrag" -#: templates/generic_index.html:41 user/templates/user/team/index.html:23 +#: templates/generic_index.html:41 user/templates/user/team/index.html:22 msgid "New" msgstr "Neu" @@ -2406,7 +2418,7 @@ msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" msgid "Team name" msgstr "Team Name" -#: user/forms.py:179 user/templates/user/team/index.html:31 +#: user/forms.py:179 user/templates/user/team/index.html:30 msgid "Description" msgstr "Beschreibung" @@ -2434,19 +2446,23 @@ msgstr "" "Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich " "selbst nicht zur Liste der Mitglieder hinzufügen." -#: user/forms.py:230 +#: user/forms.py:218 user/forms.py:279 +msgid "Name already taken. Try another." +msgstr "Name bereits vergeben. Probieren Sie einen anderen." + +#: user/forms.py:249 msgid "Admin" msgstr "Administrator" -#: user/forms.py:231 +#: user/forms.py:250 msgid "Administrators manage team details and members" msgstr "Administratoren verwalten die Teamdaten und Mitglieder" -#: user/forms.py:244 +#: user/forms.py:263 msgid "Selected admin ({}) needs to be a member of this team." msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein." -#: user/forms.py:256 user/templates/user/team/index.html:52 +#: user/forms.py:291 user/templates/user/team/index.html:51 msgid "Edit team" msgstr "Team bearbeiten" @@ -2466,7 +2482,7 @@ msgstr "Gelöscht" msgid "Show contact data" msgstr "Zeige Kontaktdaten" -#: user/templates/user/index.html:13 user/templates/user/team/index.html:30 +#: user/templates/user/index.html:13 user/templates/user/team/index.html:29 msgid "Name" msgstr "" @@ -2515,20 +2531,20 @@ msgstr "" msgid "Manage teams" msgstr "" -#: user/templates/user/index.html:69 user/templates/user/team/index.html:19 +#: user/templates/user/index.html:69 user/templates/user/team/index.html:18 #: user/views.py:142 msgid "Teams" msgstr "" -#: user/templates/user/team/index.html:21 +#: user/templates/user/team/index.html:20 msgid "Add new team" msgstr "Neues Team hinzufügen" -#: user/templates/user/team/index.html:32 +#: user/templates/user/team/index.html:31 msgid "Members" msgstr "Mitglieder" -#: user/templates/user/team/index.html:55 +#: user/templates/user/team/index.html:54 msgid "Remove team" msgstr "Team entfernen" diff --git a/user/admin.py b/user/admin.py index 3e60861..1aeacee 100644 --- a/user/admin.py +++ b/user/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from user.models import UserNotification, UserActionLogEntry, User +from user.models import UserNotification, UserActionLogEntry, User, Team class UserNotificationAdmin(admin.ModelAdmin): @@ -64,7 +64,20 @@ class UserActionLogEntryAdmin(admin.ModelAdmin): ] +class TeamAdmin(admin.ModelAdmin): + list_display = [ + "name", + "description", + "admin", + ] + search_fields = [ + "name", + "description", + ] + + admin.site.register(User, UserAdmin) +admin.site.register(Team, TeamAdmin) # Outcommented for a cleaner admin backend on production #admin.site.register(UserNotification, UserNotificationAdmin) diff --git a/user/forms.py b/user/forms.py index 35c1e82..01ec54b 100644 --- a/user/forms.py +++ b/user/forms.py @@ -206,6 +206,25 @@ class NewTeamModalForm(BaseModalForm): self.action_url = reverse("user:team-new") self.cancel_redirect = reverse("user:team-index") + def _is_name_valid(self): + name = self.cleaned_data.get("name", None) + teams_with_same_name = Team.objects.filter( + name=name + ) + name_valid = not teams_with_same_name.exists() + if not name_valid: + self.add_error( + "name", + _("Name already taken. Try another.") + ) + + return name_valid + + def is_valid(self): + super_valid = super().is_valid() + name_valid = self._is_name_valid() + return super_valid and name_valid + def save(self): with transaction.atomic(): team = Team.objects.create( @@ -246,6 +265,22 @@ class EditTeamModalForm(NewTeamModalForm): return _is_valid + def _is_name_valid(self): + name = self.cleaned_data.get("name", None) + teams_with_same_name = Team.objects.filter( + name=name + ).exclude( + id=self.instance.id + ) + name_valid = not teams_with_same_name.exists() + if not name_valid: + self.add_error( + "name", + _("Name already taken. Try another.") + ) + + return name_valid + def is_valid(self): super_valid = super().is_valid() admin_valid = self.__is_admin_valid() -- 2.38.5 From edcf7b3c7832942f5f7fafc6f2acef4d96a4ea46 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 13:52:27 +0100 Subject: [PATCH 5/8] #101 Team sharing tests * adds tests for team sharing * extends the API for team sharing support * adds shared_teams property shortcut for ShareableObjectMixin * adds full support for team-based sharing to all views and functions * simplifies ShareModalForm * adds/updates translations --- api/tests/v1/get/test_api_get.py | 20 ++++ .../intervention_share_update_put_body.json | 8 ++ api/tests/v1/update/test_api_update.py | 21 ++++ api/views/views.py | 48 ++++++++- compensation/filters.py | 23 +---- compensation/models/compensation.py | 40 +++++++- .../tests/ecoaccount/test_workflow.py | 2 +- intervention/forms/modalForms.py | 92 +++++++++--------- konova/autocompletes.py | 19 ++-- konova/filters/mixins.py | 5 +- konova/models/object.py | 34 +++++-- konova/tests/test_autocompletes.py | 1 + konova/tests/test_views.py | 21 +++- konova/utils/message_templates.py | 8 +- locale/de/LC_MESSAGES/django.mo | Bin 39754 -> 39816 bytes locale/de/LC_MESSAGES/django.po | 69 +++++++------ 16 files changed, 278 insertions(+), 133 deletions(-) create mode 100644 api/tests/v1/update/intervention_share_update_put_body.json diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py index 5bfd67a..953b0f6 100644 --- a/api/tests/v1/get/test_api_get.py +++ b/api/tests/v1/get/test_api_get.py @@ -85,6 +85,26 @@ class APIV1GetTestCase(BaseAPIV1TestCase): except KeyError as e: self.fail(e) + def test_get_shared(self): + """ Tests api GET on shared info of the intervention + + Returns: + + """ + self.intervention.share_with_user(self.superuser) + self.intervention.share_with_team(self.team) + url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),)) + response = self._run_get_request(url) + content = json.loads(response.content) + self.assertIn("users", content) + self.assertIn(self.superuser.username, content["users"]) + self.assertEqual(1, len(content["users"])) + self.assertIn("teams", content) + self.assertEqual(1, len(content["teams"])) + for team in content["teams"]: + self.assertEqual(team["id"], str(self.team.id)) + self.assertEqual(team["name"], self.team.name) + def test_get_compensation(self): """ Tests api GET diff --git a/api/tests/v1/update/intervention_share_update_put_body.json b/api/tests/v1/update/intervention_share_update_put_body.json new file mode 100644 index 0000000..c160aee --- /dev/null +++ b/api/tests/v1/update/intervention_share_update_put_body.json @@ -0,0 +1,8 @@ +{ + "users": [ + "CHANGE_ME" + ], + "teams": [ + "CHANGE_ME" + ] +} \ No newline at end of file diff --git a/api/tests/v1/update/test_api_update.py b/api/tests/v1/update/test_api_update.py index 8689fbe..500aec2 100644 --- a/api/tests/v1/update/test_api_update.py +++ b/api/tests/v1/update/test_api_update.py @@ -184,3 +184,24 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id)) self.assertEqual(put_body["eco_account"], str(self.deduction.account.id)) self.assertEqual(put_body["surface"], self.deduction.surface) + + def test_update_share_intervention(self): + self.intervention.share_with_user(self.superuser) + url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),)) + json_file_path = "api/tests/v1/update/intervention_share_update_put_body.json" + with open(json_file_path) as json_file: + put_body = json.load(fp=json_file) + put_body["users"] = [self.user.username] + put_body["teams"] = [self.team.name] + + self.assertFalse(self.intervention.is_shared_with(self.user)) + self.assertEqual(0, self.intervention.shared_teams.count()) + + response = self._run_update_request(url, put_body) + self.assertEqual(response.status_code, 200, msg=response.content) + self.intervention.refresh_from_db() + + self.assertEqual(1, self.intervention.shared_teams.count()) + self.assertEqual(2, self.intervention.shared_users.count()) + self.assertEqual(self.team.name, self.intervention.shared_teams.first().name) + self.assertTrue(self.intervention.is_shared_with(self.user)) diff --git a/api/views/views.py b/api/views/views.py index db8b8f9..75d764e 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -19,7 +19,7 @@ from ema.models import Ema from intervention.models import Intervention from konova.utils.message_templates import DATA_UNSHARED from konova.utils.user_checks import is_default_group_only -from user.models import User +from user.models import User, Team class AbstractAPIView(View): @@ -198,13 +198,21 @@ class AbstractModelShareAPIView(AbstractAPIView): """ try: users = self._get_shared_users_of_object(id) + teams = self._get_shared_teams_of_object(id) except Exception as e: return self._return_error_response(e) data = { "users": [ user.username for user in users - ] + ], + "teams": [ + { + "id": team.id, + "name": team.name, + } + for team in teams + ], } return JsonResponse(data) @@ -258,6 +266,22 @@ class AbstractModelShareAPIView(AbstractAPIView): users = obj.shared_users return users + def _get_shared_teams_of_object(self, id) -> QuerySet: + """ Check permissions and get the teams + + Args: + id (str): The object's id + + Returns: + users (QuerySet) + """ + obj = self.model.objects.get( + id=id + ) + self._check_user_has_shared_access(obj) + teams = obj.shared_teams + return teams + def _process_put_body(self, body: bytes, id: str): """ Reads the body data, performs validity checks and sets the new users @@ -271,19 +295,26 @@ class AbstractModelShareAPIView(AbstractAPIView): obj = self.model.objects.get(id=id) self._check_user_has_shared_access(obj) - new_users = json.loads(body.decode("utf-8")) - new_users = new_users.get("users", []) + content = json.loads(body.decode("utf-8")) + new_users = content.get("users", []) if len(new_users) == 0: raise ValueError("Shared user list must not be empty!") + new_teams = content.get("teams", []) # Eliminate duplicates new_users = list(dict.fromkeys(new_users)) + new_teams = list(dict.fromkeys(new_teams)) # Make sure each of these names exist as a user new_users_objs = [] for user in new_users: new_users_objs.append(User.objects.get(username=user)) + # Make sure each of these names exist as a user + new_teams_objs = [] + for team_name in new_teams: + new_teams_objs.append(Team.objects.get(name=team_name)) + if is_default_group_only(self.user): # Default only users are not allowed to remove other users from having access. They can only add new ones! new_users_to_be_added = User.objects.filter( @@ -292,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView): id__in=obj.shared_users ) new_users_objs = obj.shared_users.union(new_users_to_be_added) + + new_teams_to_be_added = Team.objects.filter( + name__in=new_teams + ).exclude( + id__in=obj.shared_teams + ) + new_teams_objs = obj.shared_teams.union(new_teams_to_be_added) + obj.share_with_user_list(new_users_objs) + obj.share_with_team_list(new_teams_objs) return True diff --git a/compensation/filters.py b/compensation/filters.py index d028e3c..b637709 100644 --- a/compensation/filters.py +++ b/compensation/filters.py @@ -59,8 +59,9 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter): """ if not value: return queryset.filter( - intervention__users__in=[self.user], # requesting user has access - ) + Q(intervention__users__in=[self.user]) | # requesting user has access + Q(intervention__teams__users__in=[self.user]) + ).distinct() else: return queryset @@ -127,24 +128,6 @@ class CheckboxEcoAccountTableFilter(CheckboxTableFilter): ) ) - def filter_show_all(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value of 'show_all' setting - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - if not value: - return queryset.filter( - users__in=[self.user], # requesting user has access - ) - else: - return queryset - def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet: """ Filters queryset depending on value of 'show_recorded' setting diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 7a10aa0..ea69cef 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -8,7 +8,7 @@ Created on: 16.11.21 import shutil from django.contrib import messages -from user.models import User +from user.models import User, Team from django.db import models, transaction from django.db.models import QuerySet, Sum from django.http import HttpRequest @@ -299,7 +299,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): # Compensations inherit their shared state from the interventions return self.intervention.is_shared_with(user) - def share_with(self, user: User): + def share_with_user(self, user: User): """ Adds user to list of shared access users Args: @@ -308,10 +308,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): Returns: """ - if not self.intervention.is_shared_with(user): - self.intervention.users.add(user) + self.intervention.users.add(user) - def share_with_list(self, user_list: list): + def share_with_user_list(self, user_list: list): """ Sets the list of shared access users Args: @@ -322,6 +321,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): """ self.intervention.users.set(user_list) + def share_with_team(self, team: Team): + """ Adds team to list of shared access teams + + Args: + team (Team): The team to be added to the object + + Returns: + + """ + self.intervention.teams.add(team) + + def share_with_team_list(self, team_list: list): + """ Sets the list of shared access teams + + Args: + team_list (list): The teams to be added to the object + + Returns: + + """ + self.intervention.teams.set(team_list) + @property def shared_users(self) -> QuerySet: """ Shortcut for fetching the users which have shared access on this object @@ -331,6 +352,15 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): """ return self.intervention.users.all() + @property + def shared_teams(self) -> QuerySet: + """ Shortcut for fetching the teams which have shared access on this object + + Returns: + users (QuerySet) + """ + return self.intervention.teams.all() + def get_documents(self) -> QuerySet: """ Getter for all documents of a compensation diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index 1b350e9..03b4996 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -233,7 +233,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): self.eco_account.set_recorded(self.superuser) self.intervention.share_with_user(self.superuser) self.eco_account.refresh_from_db() - self.assertIn(self.superuser, self.intervention.is_shared_with(self.superuser)) + self.assertTrue(self.superuser, self.intervention.is_shared_with(self.superuser)) deduction = EcoAccountDeduction.objects.create( intervention=self.intervention, diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 8e44ac2..3947938 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -9,7 +9,7 @@ from dal import autocomplete from django.core.exceptions import ObjectDoesNotExist from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ - REVOCATION_EDITED + REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION from user.models import User, Team from user.models import UserActionLogEntry from django.db import transaction @@ -37,7 +37,7 @@ class ShareModalForm(BaseModalForm): } ) ) - team_select = forms.ModelMultipleChoiceField( + teams = forms.ModelMultipleChoiceField( label=_("Add team to share with"), label_suffix="", help_text=_("Multiple selection possible - You can only select teams which do not already have access."), @@ -51,7 +51,7 @@ class ShareModalForm(BaseModalForm): }, ), ) - user_select = forms.ModelMultipleChoiceField( + users = forms.ModelMultipleChoiceField( label=_("Add user to share with"), label_suffix="", help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), @@ -63,21 +63,8 @@ class ShareModalForm(BaseModalForm): "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, }, - forward=["users"] ), ) - users = forms.MultipleChoiceField( - label=_("Shared with"), - label_suffix="", - required=True, - help_text=_("Remove check to remove access for this user"), - widget=forms.CheckboxSelectMultiple( - attrs={ - "class": "list-unstyled", - } - ), - choices=[] - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -91,6 +78,48 @@ class ShareModalForm(BaseModalForm): self._init_fields() + def _user_team_valid(self): + """ Checks whether users and teams have been removed by the user and if the user is allowed to do so or not + + Returns: + + """ + users = self.cleaned_data.get("users", User.objects.none()) + teams = self.cleaned_data.get("teams", Team.objects.none()) + + _is_valid = True + if is_default_group_only(self.user): + shared_users = self.instance.shared_users + shared_teams = self.instance.shared_teams + + shared_users_are_removed = not set(shared_users).issubset(users) + shared_teams_are_removed = not set(shared_teams).issubset(teams) + + if shared_users_are_removed: + self.add_error( + "users", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + if shared_teams_are_removed: + self.add_error( + "teams", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + return _is_valid + + def is_valid(self): + """ Extended validity check + + Returns: + + """ + super_valid = super().is_valid() + user_team_valid = self._user_team_valid() + _is_valid = super_valid and user_team_valid + return _is_valid + def _init_fields(self): """ Wraps initializing of fields @@ -105,39 +134,12 @@ class ShareModalForm(BaseModalForm): self.share_link ) - # Initialize users field - # Disable field if user is not in registration or conservation group - if is_default_group_only(self.request.user): - self.disable_form_field("users") - - self._add_user_choices_to_field() - self._add_teams_to_field() - - def _add_teams_to_field(self): form_data = { - "team_select": self.instance.teams.all() + "teams": self.instance.teams.all(), + "users": self.instance.users.all(), } self.load_initial_data(form_data) - def _add_user_choices_to_field(self): - """ Transforms the instance's sharing users into a list for the form field - - Returns: - - """ - users = self.instance.users.all() - choices = [] - for n in users: - choices.append( - (n.id, n.username) - ) - self.fields["users"].choices = choices - u_ids = list(users.values_list("id", flat=True)) - self.initialize_form_field( - "users", - u_ids - ) - def save(self): self.instance.update_sharing_user(self) diff --git a/konova/autocompletes.py b/konova/autocompletes.py index 9b1be2d..5ecc50d 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -76,19 +76,14 @@ class ShareUserAutocomplete(Select2QuerySetView): def get_queryset(self): if self.request.user.is_anonymous: return User.objects.none() - exclude_user_ids = self.forwarded.get("users", []) - _exclude = {"id__in": exclude_user_ids} - qs = User.objects.all().exclude( - **_exclude - ).order_by( - "username" - ) + qs = User.objects.all() if self.q: # Due to privacy concerns only a full username match will return the proper user entry qs = qs.filter( Q(username=self.q) | Q(email=self.q) ).distinct() + qs = qs.order_by("username") return qs @@ -99,13 +94,15 @@ class ShareTeamAutocomplete(Select2QuerySetView): def get_queryset(self): if self.request.user.is_anonymous: return Team.objects.none() + qs = Team.objects.all() if self.q: # Due to privacy concerns only a full username match will return the proper user entry - qs = Team.objects.filter( - Q(name__icontains=self.q) - ).order_by( - "name" + qs = qs.filter( + name__icontains=self.q ) + qs = qs.order_by( + "name" + ) return qs diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py index 5625ff3..5a07e3c 100644 --- a/konova/filters/mixins.py +++ b/konova/filters/mixins.py @@ -297,8 +297,9 @@ class ShareableTableFilterMixin(django_filters.FilterSet): """ if not value: return queryset.filter( - users__in=[self.user], # requesting user has access - ) + Q(users__in=[self.user]) | # requesting user has access + Q(teams__users__in=[self.user]) + ).distinct() else: return queryset diff --git a/konova/models/object.py b/konova/models/object.py index e6c27a6..92b3cca 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -501,30 +501,37 @@ class ShareableObjectMixin(models.Model): form_data = form.cleaned_data # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent - accessing_teams = form_data["team_select"] + accessing_teams = form_data["teams"] removed_team_users = self.teams.all().exclude( id__in=accessing_teams ).values_list("users__id", flat=True) - - new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) - keep_accessing_users = form_data["users"] - accessing_users = keep_accessing_users + new_accessing_users - users = User.objects.filter( - id__in=accessing_users + accessing_team_users = User.objects.filter( + id__in=accessing_teams.values_list("users", flat=True) ) + new_team_users = accessing_team_users.exclude( + id__in=self.shared_users + ).values_list("id", flat=True) + + # Fetch selected users + accessing_users = form_data["users"] removed_users = self.users.all().exclude( id__in=accessing_users ).values_list("id", flat=True) + new_users = accessing_users.exclude( + id__in=self.shared_users + ).values_list("id", flat=True) + + new_users = new_users.union(new_team_users) removed_users = removed_users.union(removed_team_users) # Send mails for user_id in removed_users: celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id) - for user_id in new_accessing_users: + for user_id in new_users: celery_send_mail_shared_access_given.delay(self.identifier, self.title, user_id) # Set new shared users - self.share_with_user_list(users) + self.share_with_user_list(accessing_users) self.share_with_team_list(accessing_teams) @property @@ -536,6 +543,15 @@ class ShareableObjectMixin(models.Model): """ return self.users.all() + @property + def shared_teams(self) -> QuerySet: + """ Shortcut for fetching the teams which have shared access on this object + + Returns: + teams (QuerySet) + """ + return self.teams.all() + @abstractmethod def get_share_url(self): """ Returns the share url for the object diff --git a/konova/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py index 7d9e508..95a3508 100644 --- a/konova/tests/test_autocompletes.py +++ b/konova/tests/test_autocompletes.py @@ -71,6 +71,7 @@ class AutocompleteTestCase(BaseTestCase): "codes-registration-office-autocomplete", "codes-conservation-office-autocomplete", "share-user-autocomplete", + "share-team-autocomplete", ] for test in tests: self.client.login(username=self.superuser.username, password=self.superuser_pw) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 6218a6c..ff99ea3 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -9,7 +9,7 @@ import datetime from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from ema.models import Ema -from user.models import User +from user.models import User, Team from django.contrib.auth.models import Group from django.contrib.gis.geos import MultiPolygon, Polygon from django.core.exceptions import ObjectDoesNotExist @@ -65,6 +65,7 @@ class BaseTestCase(TestCase): self.create_dummy_states() self.create_dummy_action() self.codes = self.create_dummy_codes() + self.team = self.create_dummy_team() # Set the default group as only group for the user default_group = self.groups.get(name=DEFAULT_GROUP) @@ -251,6 +252,24 @@ class BaseTestCase(TestCase): ]) return codes + def create_dummy_team(self): + """ Creates a dummy team + + Returns: + + """ + if self.superuser is None: + self.create_users() + + team = Team.objects.get_or_create( + name="Testteam", + description="Testdescription", + admin=self.superuser, + )[0] + team.users.add(self.superuser) + + return team + @staticmethod def create_dummy_geometry() -> MultiPolygon: """ Creates some geometry diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 5809b3b..6c0c614 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -12,12 +12,14 @@ FORM_INVALID = _("There was an error on this form.") PARAMS_INVALID = _("Invalid parameters") INTERVENTION_INVALID = _("There are errors in this intervention.") IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier") -DATA_UNSHARED = _("This data is not shared with you") -DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.") +ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") - CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") +# SHARE +DATA_UNSHARED = _("This data is not shared with you") +DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.") + # FILES FILE_TYPE_UNSUPPORTED = _("Unsupported file type") FILE_SIZE_TOO_LARGE = _("File too large") diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index a20ac2daaaa222c759f4d6a6e75bd9e7c9424ee3..cbd6f0b09ac2c43691e3e31e27cb6e98f171d3df 100644 GIT binary patch delta 6607 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 delta 6562 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index aba7981..bb96bab 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ #: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63 #: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463 #: intervention/forms/forms.py:54 intervention/forms/forms.py:156 -#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:148 -#: intervention/forms/modalForms.py:161 intervention/forms/modalForms.py:174 +#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:154 +#: intervention/forms/modalForms.py:167 intervention/forms/modalForms.py:180 #: konova/filters/mixins.py:53 konova/filters/mixins.py:54 #: konova/filters/mixins.py:81 konova/filters/mixins.py:82 #: konova/filters/mixins.py:94 konova/filters/mixins.py:95 @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-18 09:35+0100\n" +"POT-Creation-Date: 2022-02-18 12:35+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -221,7 +221,7 @@ msgstr "Abbuchungen" #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 -#: intervention/forms/modalForms.py:359 +#: intervention/forms/modalForms.py:365 msgid "Surface" msgstr "Fläche" @@ -284,8 +284,8 @@ msgid "Type" msgstr "Typ" #: analysis/templates/analysis/reports/includes/old_data/amount.html:24 -#: compensation/tables.py:89 intervention/forms/modalForms.py:370 -#: intervention/forms/modalForms.py:377 intervention/tables.py:88 +#: compensation/tables.py:89 intervention/forms/modalForms.py:376 +#: intervention/forms/modalForms.py:383 intervention/tables.py:88 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/navbars/navbar.html:22 @@ -295,7 +295,7 @@ msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: compensation/tables.py:266 #: compensation/templates/compensation/detail/eco_account/view.html:20 -#: intervention/forms/modalForms.py:343 intervention/forms/modalForms.py:350 +#: intervention/forms/modalForms.py:349 intervention/forms/modalForms.py:356 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -364,7 +364,7 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/actions.html:34 #: ema/templates/ema/detail/includes/deadlines.html:34 #: ema/templates/ema/detail/includes/documents.html:34 -#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:173 +#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:179 #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 @@ -484,7 +484,7 @@ msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357 -#: intervention/forms/modalForms.py:175 konova/forms.py:395 +#: intervention/forms/modalForms.py:181 konova/forms.py:395 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -512,7 +512,7 @@ msgstr "Zusatzbezeichnung" msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:361 +#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:367 msgid "in m²" msgstr "" @@ -540,7 +540,7 @@ msgstr "Fristart wählen" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31 -#: intervention/forms/modalForms.py:147 +#: intervention/forms/modalForms.py:153 msgid "Date" msgstr "Datum" @@ -1000,14 +1000,14 @@ msgstr "Zuletzt bearbeitet" #: compensation/templates/compensation/detail/compensation/view.html:100 #: compensation/templates/compensation/detail/eco_account/view.html:83 -#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:70 +#: ema/templates/ema/detail/view.html:76 #: intervention/templates/intervention/detail/view.html:116 msgid "Shared with" msgstr "Freigegeben für" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15 -#: intervention/forms/modalForms.py:84 +#: intervention/forms/modalForms.py:71 #: intervention/templates/intervention/detail/includes/controls.html:15 msgid "Share" msgstr "Freigabe" @@ -1348,46 +1348,48 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." -#: intervention/forms/modalForms.py:73 -msgid "Remove check to remove access for this user" -msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen" - -#: intervention/forms/modalForms.py:85 +#: intervention/forms/modalForms.py:72 msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" -#: intervention/forms/modalForms.py:149 +#: intervention/forms/modalForms.py:105 intervention/forms/modalForms.py:111 +msgid "" +"Only conservation or registration office users are allowed to remove entries." +msgstr "" +"Nur Mitarbeiter der Naturschutz- oder Zulassungsbehördengruppe dürfen Einträge entfernen" + +#: intervention/forms/modalForms.py:155 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modalForms.py:160 +#: intervention/forms/modalForms.py:166 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modalForms.py:163 +#: intervention/forms/modalForms.py:169 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modalForms.py:188 +#: intervention/forms/modalForms.py:194 #: intervention/templates/intervention/detail/includes/revocation.html:18 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modalForms.py:245 +#: intervention/forms/modalForms.py:251 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:251 +#: intervention/forms/modalForms.py:257 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:260 +#: intervention/forms/modalForms.py:266 #: intervention/templates/intervention/detail/includes/controls.html:19 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:261 konova/forms.py:514 +#: intervention/forms/modalForms.py:267 konova/forms.py:514 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1395,23 +1397,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:345 +#: intervention/forms/modalForms.py:351 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:372 +#: intervention/forms/modalForms.py:378 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:385 +#: intervention/forms/modalForms.py:391 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:386 +#: intervention/forms/modalForms.py:392 msgid "Enter the information for a new deduction from a chosen eco-account" msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." -#: intervention/forms/modalForms.py:429 +#: intervention/forms/modalForms.py:435 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1419,7 +1421,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:439 +#: intervention/forms/modalForms.py:445 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -4107,6 +4109,9 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ msgid "Remove check to remove access for this user" +#~ msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen" + #~ msgid "Select the action type" #~ msgstr "Maßnahmentyp wählen" -- 2.38.5 From 87a93b9a7fd7ca00437a2a0d44cf584c74dabdd2 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 14:06:51 +0100 Subject: [PATCH 6/8] #101 Team data view * adds overview of shared teams on object detail view * adds team data view if button is clicked --- .../detail/compensation/view.html | 4 + .../compensation/detail/eco_account/view.html | 4 + ema/templates/ema/detail/view.html | 4 + .../templates/intervention/detail/view.html | 4 + locale/de/LC_MESSAGES/django.mo | Bin 39816 -> 39892 bytes locale/de/LC_MESSAGES/django.po | 191 +++++++++--------- user/forms.py | 43 ++++ .../user/includes/contact_modal_button.html | 2 +- .../user/includes/team_data_modal_button.html | 6 + user/urls.py | 1 + user/views.py | 27 ++- 11 files changed, 193 insertions(+), 93 deletions(-) create mode 100644 user/templates/user/includes/team_data_modal_button.html diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index c5ff41d..e387151 100644 --- a/compensation/templates/compensation/detail/compensation/view.html +++ b/compensation/templates/compensation/detail/compensation/view.html @@ -98,6 +98,10 @@ {% trans 'Shared with' %} + {% for team in obj.intervention.teams.all %} + {% include 'user/includes/team_data_modal_button.html' %} + {% endfor %} +
{% for user in obj.intervention.users.all %} {% include 'user/includes/contact_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 288b971..ee4a0f7 100644 --- a/compensation/templates/compensation/detail/eco_account/view.html +++ b/compensation/templates/compensation/detail/eco_account/view.html @@ -81,6 +81,10 @@ {% trans 'Shared with' %} + {% for team in obj.teams.all %} + {% include 'user/includes/team_data_modal_button.html' %} + {% endfor %} +
{% for user in obj.users.all %} {% include 'user/includes/contact_modal_button.html' %} {% endfor %} diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html index 32ddd66..7b56703 100644 --- a/ema/templates/ema/detail/view.html +++ b/ema/templates/ema/detail/view.html @@ -74,6 +74,10 @@ {% trans 'Shared with' %} + {% for team in obj.teams.all %} + {% include 'user/includes/team_data_modal_button.html' %} + {% endfor %} +
{% for user in obj.users.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 8e7fc6c..f5680cc 100644 --- a/intervention/templates/intervention/detail/view.html +++ b/intervention/templates/intervention/detail/view.html @@ -114,6 +114,10 @@ {% trans 'Shared with' %} + {% for team in obj.teams.all %} + {% include 'user/includes/team_data_modal_button.html' %} + {% endfor %} +
{% for user in obj.users.all %} {% include 'user/includes/contact_modal_button.html' %} {% endfor %} diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index cbd6f0b09ac2c43691e3e31e27cb6e98f171d3df..ced4ab1ce70235e9c2f809480b12816984ba7a18 100644 GIT binary patch delta 9721 zcmZA5d3;Y-{>SnAjgUnWK_U`Skw_wfG$Pj6LM@Fgjit3riKJ*NW2>$BQhv0Sq8DSS zZBSdIG+I@nq}0%o=@ji)o0igQYMCyhlzF|K(|>;V@wk1SbMC$8e9q^bd-K&-_xhaP z>*Kvt&3Bo@5#r-G(YPSgaf&J5k5Q}R+)8$wDjvtVjV^UhBgdJ8%kUM9ZtOVya2*!l zuhbl;CF-SE8PD4GZ%_~Zf)mgu z)jT&DL#WTSE=N7L8RKxbZ9juy)URMD-o>hn?*uk64bfPI3yrWkwn06RjT-nl)Ij5~ z2EJlliILQIVNE=Ny6;=-9jr#(ljb-H7={(GH8y5^rvn8&Fd8eM7s;A43zdl?+=d@w zb$DuNuj0&t7`eG;4#JhRzg+ZtYpGS2x z85QYksI^>$3S<*zVIeZ6a~J1hYD@E6G3vRKr~oga0=R|h{~l_>fvrq{-gpX{K_lDH z6t#94r~$iLvr#D@fa+*8Do`&fz(uHjR-y)8Z?C_L>aPT~w5L#+IB%|do$o1V4e#0; zLR*^;QEk*7XoVWM7pjANREJ|x&rQcfT!vbjVr+vNqsJA0>jZ?@Bah} zN|_hc@nX~r-b6*Z(|W+RpFpkM1yn#cQJJ`pN_9{hlaUD29;l1TWD+VvO;CZh#EyFZ zdr;7TOHdz%ji?*9p&s0YwQwJ5^PE8idJZ*{?@;&sg8F_uKt1o%*4!U}8ZZR4RI#YQ z8=_Z*mJ~FD&KQCHP#qSa-uH>94p&(>pfa-+70~+L3==Stp{s<2;Q$u>iN=V_2huwqWSn}$xtcXh#II6wFLWZ z`$wqfkE8a+DSQ3jsEl6pQc$X|paLmJ&CJu$3>1RuFbow?P1OA{ww{FAys6eq)Sk#e z1(=HsaXdD{4Ojt7Q2}_rpr8oPp*GoNdlsI}|*gjtedSb_R%)XZN+ z1-Klwm)4?Yyb~4RKGeicpfdKQY4NKDQo zTQ5ef{TbARZlD6Wg&NrDVg?RI1r&k$`{UH7pa-8t&EzH29+-t%k`<`H-bO#%k4o*w zsF|EW1#}s;$$mox8rs#Eh`Qbudtg7*1UF%lQhbhr2KMb{BCdu>)ML>PyCdHgrw=}f zOKkfEtVsO|M&b`x34OUq0ar%#8)dDJ>aUrtw?(gR>`p;zKMMx-Bnbe*9t79(>8fZSMz7&ISJ!&Zm zQ7JD$Ey-!DgV%8_`jc06xCOPgJ5e+L2zCES)P%~cw^0-K^d$f4IJBo3FcuX+3aZ1_ z7=W4hXem&s?t>br!0N?7>WfhWt+wquQ2if3eRxix`Y*Hfay_U4^e0NMQy?l6A*fv( zfeIuV^+k+B%_tMK$@zbs88)Q z)UI8P>Zl0Y;b-=GP&U7isW-qXI0BpDB;*Bf-a$>|H`FF|o-!G#gvvx!&vJ;un2YECRCvNQ3D@CW$H_;iWg7=|Acz(Hw?z09JA(8sCp76>HTj` zK@SeaARLDpV1}(PKs~V1*0Nga%1;7^fiA@2ciZlw4TNo>i4YC{mhcJ!Fsg!#ilp~_5FAUlki7; z7Hjk;|B8451*L2XDzaIqHCu@K^tzac%Tb%?puK((_58P}nO;N9^d_pGpKbjCs{aa4 z+oeFgEm2RCf30CW4dIxM>L3f1+TN(8c^0eT7}QeCMs@fGDy6G!`xaCn@1X)KMg?*h zWDg(LrXy&M;n2lPRxmXhyq5^*lHS_JZ{;u_X z+x{VHbC=lmQdEE5FDN8aIBWG8WMbxgo=tcYPFi5{a+9hIUs@ikN+zo7#3f8I=>4r(b|pfcMLwQ2i5PyRLVKzm`N zbs7fHz67-dt8Kl|x)-&X4x<9Qh#JuM1+!GOFrIonRDe&Sc7HBL<6lwZE_i|b`%zd$ zg9iLNY6fqk1};WDSc-vo0hNJks9jx-y5H|b({WAI5~QP+t~cs69*GKg5~`o2sKC~F zZDB8Jz*0=YGJFgxk1`!KKn2JXZ`O`Q0gmpgNEy*lY$FE@sE=NsdGwN?f z5$d`3QJeE1Hph~e$iD{oorX~KonSf$N6n-jhGTQoeLb)l_D6O2I_hm$jv8SG2KftKT=C9-zm`MGhttU<~ z0q3CJhFsJ{UcfM%iZyWw*1+wki5#}|PrVehmfxT@%db}dm(6dxC~IrfOb4N6Hq1H# zHRDmJ0mh*+H_y5T_1rGhID1i<`xF(Z_X`SIlPjnhT(=i)qh{bU)eI0}t&O@r4)t~< zqBc_#d%Z1c4|GE9i5%+?)FvE-%J?*FtoMH_1#OZqQIVcQ&9DrWy6^Bse27}27pL*N z5AR|vTrl1IP}zzK>;fu)uTc}Yi5l3KrB-_wD${WosP{jeQ=6wf>IVIrQ1!tWf%+TK z9A}|sx*rwbG1LH`VI-DePrPUAPtGuVDG&MXI9pIlunm>^VhrJ4=M)94-9_w*Kcm*L z%}ir&)cZXQ72r(Nj25HTay@EOZAAt6E;hu2*a&Z6V~l*o`~c~S`XEh0{r+^eQ|OG9 zX0eXg0~O&6R3J;RJ#In`bQK@Ro7fIxW}Ce;#5xkAXdjEdxDb`{H&D-SLiN9MHu={K zKB7UXJcb(Z0xI=4@iFw{T~r4NsF}AwU+jw7Gd(Z~`ykuInT~JbJtRhF#T@f5sOY(7 zX$w%_m8o;dzXp7R2BmH@YM|Y={xK@$rPfPWhk7|CVDPKv`KGA$E_f1$qd%s|k)N?zKrS&@hprBMuks0SyaX0{O3@jA@JU8ouT2U9U{ zq1huDsLk0O+v5TZ#*?VboyQ=(|5qregWIUh=Ks3+$6_L?o`w3*JdcWej&0v)EkOlT zhDzxj)Bt{qOeRAyhW)m+m zehfqL6l!TMV>SE{gYh9M;Na!vdQH^*@u&ftp#p1*3akUB;*(wqttd>j?nS*WcTf+6 z{>`McIx2-tu{91xy)`RwIvzr;X}cB1PS$SLo~TUZ*m{3dAl^X~6k$GUrlaf)T@v$*I->dh;jJ!BRewaYO@*YV>}m{ zVhRpGZLWE!&9fb~Y4+LrQS?yX;FiTy5BP!N1!|YvKVl-n-=X#swNp5Qvyk$4Zc^Rm zp8vW->n3`>bziGnCwvQ65;zZXrg8qA^EbD+ZdSknN;)pPRby*~ze_EPS~=$i&b8D( zb34RVul)<9WLhTR8q_h0vW_BmOl-2}4|h#$t)wfokx%F5qbue=Q)&B}vli9OxSI32 zTNaxbyo1txT94qr-0-+Mo{MgJ-0=AKsHf9Dmh(R4T>SHBMO{Z__i$XY=dfEImt4cc z4!&mF%~gKrdfdkKo{#k5(h%C7R)mW;r#QsMR^V94>a7ym7IZ$u02*vS;uqu zt9v*;&a=_I8s9PhB=tVDM{o}1{DOK3XFBH=>Zdq2asHe7C43Wgd~dx)c^YR0#_5VT zICY%hdJ*p8{D-?RAvxe2q zI%?t+yoc-Y8_qP&F4WJtgA-FC^#N%|iytk&ppH_?d)>{6X)TXZilt>ezRwxPIi7PZ zZQbxH=Sa%>x&+gc4tBhA)~#AUE%-E7^Qle5AUC^ya@D((bQI!N{MlVtzn$j;_hkLJ z+GVtDrfm}EDtq5NET$gf1}CM&e?)Dhy>bZ4xT1rX^wDvT|88}MCZ*NBz}5ZK^!57l zcwh?;-Q7uvZT`a@lR3jVb=<_qIcIZj;|#E~F<1F_Fl{dSZ)|KFH2J1e-}4MXiy z$J|EAIl<>`O?T{e7bds!+$cJk+$1qzeE!f;F?mDB4|VT8b*<=p&dKV2`6COaPw;^Jk;C(2`tS%(xMg`^MH!2#c#1kN&aC`DXs;Yk delta 9658 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, YEAR. # -#: compensation/filters.py:122 compensation/forms/modalForms.py:36 +#: compensation/filters.py:123 compensation/forms/modalForms.py:36 #: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63 #: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463 #: intervention/forms/forms.py:54 intervention/forms/forms.py:156 -#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:154 -#: intervention/forms/modalForms.py:167 intervention/forms/modalForms.py:180 +#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:150 +#: intervention/forms/modalForms.py:163 intervention/forms/modalForms.py:176 #: konova/filters/mixins.py:53 konova/filters/mixins.py:54 #: konova/filters/mixins.py:81 konova/filters/mixins.py:82 #: konova/filters/mixins.py:94 konova/filters/mixins.py:95 #: konova/filters/mixins.py:107 konova/filters/mixins.py:108 #: konova/filters/mixins.py:120 konova/filters/mixins.py:121 #: konova/filters/mixins.py:134 konova/filters/mixins.py:135 -#: konova/filters/mixins.py:270 konova/filters/mixins.py:315 -#: konova/filters/mixins.py:353 konova/filters/mixins.py:354 -#: konova/filters/mixins.py:385 konova/filters/mixins.py:386 +#: konova/filters/mixins.py:270 konova/filters/mixins.py:316 +#: konova/filters/mixins.py:354 konova/filters/mixins.py:355 +#: konova/filters/mixins.py:386 konova/filters/mixins.py:387 #: konova/forms.py:143 konova/forms.py:244 konova/forms.py:315 #: konova/forms.py:359 konova/forms.py:369 konova/forms.py:382 #: konova/forms.py:394 konova/forms.py:412 user/forms.py:42 @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-18 12:35+0100\n" +"POT-Creation-Date: 2022-02-18 14:01+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -52,7 +52,7 @@ msgstr "Bis" #: intervention/forms/forms.py:102 #: intervention/templates/intervention/detail/view.html:56 #: intervention/templates/intervention/report/report.html:37 -#: intervention/utils/quality.py:49 konova/filters/mixins.py:395 +#: intervention/utils/quality.py:49 konova/filters/mixins.py:396 msgid "Conservation office" msgstr "Eintragungsstelle" @@ -221,7 +221,7 @@ msgstr "Abbuchungen" #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 -#: intervention/forms/modalForms.py:365 +#: intervention/forms/modalForms.py:361 msgid "Surface" msgstr "Fläche" @@ -284,8 +284,8 @@ msgid "Type" msgstr "Typ" #: analysis/templates/analysis/reports/includes/old_data/amount.html:24 -#: compensation/tables.py:89 intervention/forms/modalForms.py:376 -#: intervention/forms/modalForms.py:383 intervention/tables.py:88 +#: compensation/tables.py:89 intervention/forms/modalForms.py:372 +#: intervention/forms/modalForms.py:379 intervention/tables.py:88 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/navbars/navbar.html:22 @@ -295,7 +295,7 @@ msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: compensation/tables.py:266 #: compensation/templates/compensation/detail/eco_account/view.html:20 -#: intervention/forms/modalForms.py:349 intervention/forms/modalForms.py:356 +#: intervention/forms/modalForms.py:345 intervention/forms/modalForms.py:352 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -309,7 +309,7 @@ msgstr "Altfälle" msgid "Before" msgstr "Vor" -#: compensation/filters.py:121 +#: compensation/filters.py:122 msgid "Show only unrecorded" msgstr "Nur unverzeichnete anzeigen" @@ -364,7 +364,7 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/actions.html:34 #: ema/templates/ema/detail/includes/deadlines.html:34 #: ema/templates/ema/detail/includes/documents.html:34 -#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:179 +#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:175 #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 @@ -484,7 +484,7 @@ msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357 -#: intervention/forms/modalForms.py:181 konova/forms.py:395 +#: intervention/forms/modalForms.py:177 konova/forms.py:395 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -512,7 +512,7 @@ msgstr "Zusatzbezeichnung" msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:367 +#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:363 msgid "in m²" msgstr "" @@ -540,7 +540,7 @@ msgstr "Fristart wählen" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31 -#: intervention/forms/modalForms.py:153 +#: intervention/forms/modalForms.py:149 msgid "Date" msgstr "Datum" @@ -1142,7 +1142,7 @@ msgstr "Daten zu den verantwortlichen Stellen" msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation.py:151 konova/utils/message_templates.py:31 +#: compensation/views/compensation.py:151 konova/utils/message_templates.py:33 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" @@ -1269,7 +1269,7 @@ msgstr "Mehrfachauswahl möglich" #: intervention/forms/forms.py:86 #: intervention/templates/intervention/detail/view.html:48 #: intervention/templates/intervention/report/report.html:29 -#: intervention/utils/quality.py:46 konova/filters/mixins.py:363 +#: intervention/utils/quality.py:46 konova/filters/mixins.py:364 msgid "Registration office" msgstr "Zulassungsbehörde" @@ -1322,7 +1322,7 @@ msgstr "Freigabelink" #: intervention/forms/modalForms.py:31 msgid "Send this link to users who you want to have writing access on the data" -msgstr "Andere Nutzer erhalten über diesen Link Zugriff auf die Daten" +msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten" #: intervention/forms/modalForms.py:41 msgid "Add team to share with" @@ -1338,7 +1338,7 @@ msgstr "" #: intervention/forms/modalForms.py:55 msgid "Add user to share with" -msgstr "Nutzer direkt hinzufügen" +msgstr "Nutzer einzeln hinzufügen" #: intervention/forms/modalForms.py:57 msgid "" @@ -1352,44 +1352,38 @@ msgstr "" msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" -#: intervention/forms/modalForms.py:105 intervention/forms/modalForms.py:111 -msgid "" -"Only conservation or registration office users are allowed to remove entries." -msgstr "" -"Nur Mitarbeiter der Naturschutz- oder Zulassungsbehördengruppe dürfen Einträge entfernen" - -#: intervention/forms/modalForms.py:155 +#: intervention/forms/modalForms.py:151 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modalForms.py:166 +#: intervention/forms/modalForms.py:162 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modalForms.py:169 +#: intervention/forms/modalForms.py:165 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modalForms.py:194 +#: intervention/forms/modalForms.py:190 #: intervention/templates/intervention/detail/includes/revocation.html:18 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modalForms.py:251 +#: intervention/forms/modalForms.py:247 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:257 +#: intervention/forms/modalForms.py:253 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:266 +#: intervention/forms/modalForms.py:262 #: intervention/templates/intervention/detail/includes/controls.html:19 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:267 konova/forms.py:514 +#: intervention/forms/modalForms.py:263 konova/forms.py:514 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1397,23 +1391,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:351 +#: intervention/forms/modalForms.py:347 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:378 +#: intervention/forms/modalForms.py:374 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:391 +#: intervention/forms/modalForms.py:387 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:392 +#: intervention/forms/modalForms.py:388 msgid "Enter the information for a new deduction from a chosen eco-account" msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." -#: intervention/forms/modalForms.py:435 +#: intervention/forms/modalForms.py:431 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1421,7 +1415,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:445 +#: intervention/forms/modalForms.py:441 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -1616,15 +1610,15 @@ msgstr "Nach Flurstücknenner suchen" msgid "Show unshared" msgstr "Nicht freigegebene anzeigen" -#: konova/filters/mixins.py:314 +#: konova/filters/mixins.py:315 msgid "Show recorded" msgstr "Verzeichnete anzeigen" -#: konova/filters/mixins.py:364 +#: konova/filters/mixins.py:365 msgid "Search for registration office" msgstr "Nach Zulassungsbehörde suchen" -#: konova/filters/mixins.py:396 +#: konova/filters/mixins.py:397 msgid "Search for conservation office" msgstr "Nch Eintragungsstelle suchen" @@ -1851,10 +1845,25 @@ msgstr "" "der Zwischenzeit angelegt wurde, welcher diese Kennung nun bereits verwendet" #: konova/utils/message_templates.py:15 +msgid "" +"Only conservation or registration office users are allowed to remove entries." +msgstr "" +"Nur Mitarbeiter der Naturschutz- oder Zulassungsbehördengruppe dürfen " +"Einträge entfernen" + +#: konova/utils/message_templates.py:16 +msgid "You need to be part of another user group." +msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!" + +#: konova/utils/message_templates.py:17 +msgid "Status of Checked and Recorded reseted" +msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt" + +#: konova/utils/message_templates.py:20 msgid "This data is not shared with you" msgstr "Diese Daten sind für Sie nicht freigegeben" -#: konova/utils/message_templates.py:16 +#: konova/utils/message_templates.py:21 msgid "" "Remember: This data has not been shared with you, yet. This means you can " "only read but can not edit or perform any actions like running a check or " @@ -1864,23 +1873,15 @@ msgstr "" "bedeutet, dass Sie nur lesenden Zugriff hierauf haben und weder bearbeiten, " "noch Prüfungen durchführen oder verzeichnen können." -#: konova/utils/message_templates.py:17 -msgid "You need to be part of another user group." -msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!" - -#: konova/utils/message_templates.py:19 -msgid "Status of Checked and Recorded reseted" -msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt" - -#: konova/utils/message_templates.py:22 +#: konova/utils/message_templates.py:24 msgid "Unsupported file type" msgstr "Dateiformat nicht unterstützt" -#: konova/utils/message_templates.py:23 +#: konova/utils/message_templates.py:25 msgid "File too large" msgstr "Datei zu groß" -#: konova/utils/message_templates.py:26 +#: konova/utils/message_templates.py:28 msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." @@ -1888,119 +1889,119 @@ msgstr "" "Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen " "vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen." -#: konova/utils/message_templates.py:29 +#: konova/utils/message_templates.py:31 msgid "Compensation {} added" msgstr "Kompensation {} hinzugefügt" -#: konova/utils/message_templates.py:30 +#: konova/utils/message_templates.py:32 msgid "Compensation {} removed" msgstr "Kompensation {} entfernt" -#: konova/utils/message_templates.py:32 +#: konova/utils/message_templates.py:34 msgid "Added compensation action" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:33 +#: konova/utils/message_templates.py:35 msgid "Added compensation state" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:36 +#: konova/utils/message_templates.py:38 msgid "State removed" msgstr "Zustand gelöscht" -#: konova/utils/message_templates.py:37 +#: konova/utils/message_templates.py:39 msgid "State edited" msgstr "Zustand bearbeitet" -#: konova/utils/message_templates.py:38 +#: konova/utils/message_templates.py:40 msgid "State added" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:41 +#: konova/utils/message_templates.py:43 msgid "Action added" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:42 +#: konova/utils/message_templates.py:44 msgid "Action edited" msgstr "Maßnahme bearbeitet" -#: konova/utils/message_templates.py:43 +#: konova/utils/message_templates.py:45 msgid "Action removed" msgstr "Maßnahme entfernt" -#: konova/utils/message_templates.py:46 +#: konova/utils/message_templates.py:48 msgid "Deduction added" msgstr "Abbuchung hinzugefügt" -#: konova/utils/message_templates.py:47 +#: konova/utils/message_templates.py:49 msgid "Deduction edited" msgstr "Abbuchung bearbeitet" -#: konova/utils/message_templates.py:48 +#: konova/utils/message_templates.py:50 msgid "Deduction removed" msgstr "Abbuchung entfernt" -#: konova/utils/message_templates.py:51 +#: konova/utils/message_templates.py:53 msgid "Deadline added" msgstr "Frist/Termin hinzugefügt" -#: konova/utils/message_templates.py:52 +#: konova/utils/message_templates.py:54 msgid "Deadline edited" msgstr "Frist/Termin bearbeitet" -#: konova/utils/message_templates.py:53 +#: konova/utils/message_templates.py:55 msgid "Deadline removed" msgstr "Frist/Termin gelöscht" -#: konova/utils/message_templates.py:56 +#: konova/utils/message_templates.py:58 msgid "Payment added" msgstr "Zahlung hinzugefügt" -#: konova/utils/message_templates.py:57 +#: konova/utils/message_templates.py:59 msgid "Payment edited" msgstr "Zahlung bearbeitet" -#: konova/utils/message_templates.py:58 +#: konova/utils/message_templates.py:60 msgid "Payment removed" msgstr "Zahlung gelöscht" -#: konova/utils/message_templates.py:61 +#: konova/utils/message_templates.py:63 msgid "Revocation added" msgstr "Widerspruch hinzugefügt" -#: konova/utils/message_templates.py:62 +#: konova/utils/message_templates.py:64 msgid "Revocation edited" msgstr "Widerspruch bearbeitet" -#: konova/utils/message_templates.py:63 +#: konova/utils/message_templates.py:65 msgid "Revocation removed" msgstr "Widerspruch entfernt" -#: konova/utils/message_templates.py:66 +#: konova/utils/message_templates.py:68 msgid "Document '{}' deleted" msgstr "Dokument '{}' gelöscht" -#: konova/utils/message_templates.py:67 +#: konova/utils/message_templates.py:69 msgid "Document added" msgstr "Dokument hinzugefügt" -#: konova/utils/message_templates.py:68 +#: konova/utils/message_templates.py:70 msgid "Document edited" msgstr "Dokument bearbeitet" -#: konova/utils/message_templates.py:71 +#: konova/utils/message_templates.py:73 msgid "Edited general data" msgstr "Allgemeine Daten bearbeitet" -#: konova/utils/message_templates.py:72 +#: konova/utils/message_templates.py:74 msgid "Added deadline" msgstr "Frist/Termin hinzugefügt" -#: konova/utils/message_templates.py:75 +#: konova/utils/message_templates.py:77 msgid "Geometry conflict detected with {}" msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" -#: konova/utils/message_templates.py:78 +#: konova/utils/message_templates.py:80 msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" @@ -2416,11 +2417,11 @@ msgstr "Neuen Token generieren" msgid "A new token needs to be validated by an administrator!" msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" -#: user/forms.py:168 user/forms.py:172 +#: user/forms.py:168 user/forms.py:172 user/forms.py:323 user/forms.py:328 msgid "Team name" msgstr "Team Name" -#: user/forms.py:179 user/templates/user/team/index.html:30 +#: user/forms.py:179 user/forms.py:336 user/templates/user/team/index.html:30 msgid "Description" msgstr "Beschreibung" @@ -2468,6 +2469,10 @@ msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein." msgid "Edit team" msgstr "Team bearbeiten" +#: user/forms.py:347 +msgid "Team" +msgstr "Team" + #: user/models/user_action.py:22 msgid "Unrecorded" msgstr "Entzeichnet" @@ -2484,6 +2489,10 @@ msgstr "Gelöscht" msgid "Show contact data" msgstr "Zeige Kontaktdaten" +#: user/templates/user/includes/team_data_modal_button.html:3 +msgid "Show team data" +msgstr "Zeige Teamdaten" + #: user/templates/user/index.html:13 user/templates/user/team/index.html:29 msgid "Name" msgstr "" @@ -2534,7 +2543,7 @@ msgid "Manage teams" msgstr "" #: user/templates/user/index.html:69 user/templates/user/team/index.html:18 -#: user/views.py:142 +#: user/views.py:167 msgid "Teams" msgstr "" @@ -2594,15 +2603,15 @@ msgstr "Neuer Token generiert. Administratoren sind informiert." msgid "User API token" msgstr "API Nutzer Token" -#: user/views.py:153 +#: user/views.py:178 msgid "New team added" msgstr "Neues Team hinzugefügt" -#: user/views.py:166 +#: user/views.py:191 msgid "Team edited" msgstr "Team bearbeitet" -#: user/views.py:179 +#: user/views.py:204 msgid "Team removed" msgstr "Team gelöscht" diff --git a/user/forms.py b/user/forms.py index 01ec54b..34c8fab 100644 --- a/user/forms.py +++ b/user/forms.py @@ -315,3 +315,46 @@ class EditTeamModalForm(NewTeamModalForm): class RemoveTeamModalForm(RemoveModalForm): pass + + +class TeamDataForm(BaseModalForm): + name = forms.CharField( + label_suffix="", + label=_("Team name"), + max_length=500, + required=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("Team name"), + "class": "form-control", + } + ) + ) + description = forms.CharField( + label_suffix="", + required=False, + label=_("Description"), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control" + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Team") + self.form_caption = "" + self.render_submit = False + form_data = { + "name": self.instance.name, + "description": self.instance.description, + } + self.load_initial_data( + form_data, + [ + "name", + "description" + ] + ) \ No newline at end of file diff --git a/user/templates/user/includes/contact_modal_button.html b/user/templates/user/includes/contact_modal_button.html index 68ae53a..5cbd87f 100644 --- a/user/templates/user/includes/contact_modal_button.html +++ b/user/templates/user/includes/contact_modal_button.html @@ -1,6 +1,6 @@ {% load fontawesome_5 i18n %} \ No newline at end of file diff --git a/user/templates/user/includes/team_data_modal_button.html b/user/templates/user/includes/team_data_modal_button.html new file mode 100644 index 0000000..b41e63a --- /dev/null +++ b/user/templates/user/includes/team_data_modal_button.html @@ -0,0 +1,6 @@ +{% load fontawesome_5 i18n %} + + \ No newline at end of file diff --git a/user/urls.py b/user/urls.py index 76a8662..5c33427 100644 --- a/user/urls.py +++ b/user/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ path("token/api", api_token_view, name="api-token"), path("contact/", contact_view, name="contact"), path("team/", index_team_view, name="team-index"), + path("team/", data_team_view, name="team-data"), path("team/new", new_team_view, name="team-new"), path("team//edit", edit_team_view, name="team-edit"), path("team//remove", remove_team_view, name="team-remove"), diff --git a/user/views.py b/user/views.py index afed572..9afede1 100644 --- a/user/views.py +++ b/user/views.py @@ -13,7 +13,7 @@ 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, NewTeamModalForm, EditTeamModalForm, \ - RemoveTeamModalForm + RemoveTeamModalForm, TeamDataForm @login_required @@ -133,6 +133,31 @@ def contact_view(request: HttpRequest, id: str): ) +@login_required +def data_team_view(request: HttpRequest, id: str): + """ Renders team data + + Args: + request (HttpRequest): The incoming request + id (str): The team's id + + Returns: + + """ + team = get_object_or_404(Team, id=id) + form = TeamDataForm(request.POST or None, instance=team, request=request) + template = "modal/modal_form.html" + context = { + "form": form, + } + context = BaseContext(request, context).context + return render( + request, + template, + context + ) + + @login_required def index_team_view(request: HttpRequest): template = "user/team/index.html" -- 2.38.5 From 50def040f2e0f8cc0213bbfb708594c36795d064 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 14:09:45 +0100 Subject: [PATCH 7/8] #101 Datatype team migrations * adds migration files for ShareableObject data models --- .../migrations/0006_ecoaccount_teams.py | 19 +++++++++++++++++++ ema/migrations/0003_ema_teams.py | 19 +++++++++++++++++++ .../migrations/0003_intervention_teams.py | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 compensation/migrations/0006_ecoaccount_teams.py create mode 100644 ema/migrations/0003_ema_teams.py create mode 100644 intervention/migrations/0003_intervention_teams.py diff --git a/compensation/migrations/0006_ecoaccount_teams.py b/compensation/migrations/0006_ecoaccount_teams.py new file mode 100644 index 0000000..14cf509 --- /dev/null +++ b/compensation/migrations/0006_ecoaccount_teams.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-02-18 09:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_team'), + ('compensation', '0005_auto_20220218_0917'), + ] + + operations = [ + migrations.AddField( + model_name='ecoaccount', + name='teams', + field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'), + ), + ] diff --git a/ema/migrations/0003_ema_teams.py b/ema/migrations/0003_ema_teams.py new file mode 100644 index 0000000..606781d --- /dev/null +++ b/ema/migrations/0003_ema_teams.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-02-18 09:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_team'), + ('ema', '0002_auto_20220114_0936'), + ] + + operations = [ + migrations.AddField( + model_name='ema', + name='teams', + field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'), + ), + ] diff --git a/intervention/migrations/0003_intervention_teams.py b/intervention/migrations/0003_intervention_teams.py new file mode 100644 index 0000000..8dd7624 --- /dev/null +++ b/intervention/migrations/0003_intervention_teams.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-02-18 09:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_team'), + ('intervention', '0002_auto_20220114_0936'), + ] + + operations = [ + migrations.AddField( + model_name='intervention', + name='teams', + field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'), + ), + ] -- 2.38.5 From 963854652ef9d6df09be8eeedfde9b07be7b9259 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 15:19:37 +0100 Subject: [PATCH 8/8] #101 Team mails * adds mail templates for shared data actions * fixes bug where deleted compensations would be used for checking --- intervention/forms/modalForms.py | 6 +- konova/models/object.py | 87 ++++++++--- konova/tasks.py | 42 ++++++ konova/utils/mailer.py | 138 ++++++++++++++++++ locale/de/LC_MESSAGES/django.mo | Bin 39892 -> 40282 bytes locale/de/LC_MESSAGES/django.po | 43 +++++- .../checking/shared_data_checked_team.html | 28 ++++ .../deleting/shared_data_deleted_team.html | 28 ++++ .../recording/shared_data_recorded_team.html | 33 +++++ .../shared_data_unrecorded_team.html | 33 +++++ .../sharing/shared_access_given_team.html | 34 +++++ .../sharing/shared_access_removed_team.html | 29 ++++ user/models/team.py | 79 ++++++++++ 13 files changed, 551 insertions(+), 29 deletions(-) create mode 100644 templates/email/checking/shared_data_checked_team.html create mode 100644 templates/email/deleting/shared_data_deleted_team.html create mode 100644 templates/email/recording/shared_data_recorded_team.html create mode 100644 templates/email/recording/shared_data_unrecorded_team.html create mode 100644 templates/email/sharing/shared_access_given_team.html create mode 100644 templates/email/sharing/shared_access_removed_team.html diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 3947938..1e5febf 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -141,7 +141,7 @@ class ShareModalForm(BaseModalForm): self.load_initial_data(form_data) def save(self): - self.instance.update_sharing_user(self) + self.instance.update_shared_access(self) class NewRevocationModalForm(BaseModalForm): @@ -290,7 +290,9 @@ class CheckModalForm(BaseModalForm): Returns: """ - comps = self.instance.compensations.all() + comps = self.instance.compensations.filter( + deleted=None, + ) comps_valid = True for comp in comps: checker = comp.quality_check() diff --git a/konova/models/object.py b/konova/models/object.py index 92b3cca..f224b68 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -15,7 +15,10 @@ from django.db.models import QuerySet from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT, LANIS_LINK_TEMPLATE 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 + celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked, \ + celery_send_mail_shared_access_given_team, celery_send_mail_shared_access_removed_team, \ + celery_send_mail_shared_data_checked_team, celery_send_mail_shared_data_deleted_team, \ + celery_send_mail_shared_data_unrecorded_team, celery_send_mail_shared_data_recorded_team from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest from django.utils.timezone import now @@ -130,6 +133,11 @@ class BaseObject(BaseResource): for user_id in shared_users: celery_send_mail_shared_data_deleted.delay(self.identifier, self.title, user_id) + # Send mail + shared_teams = self.shared_teams.values_list("id", flat=True) + for team_id in shared_teams: + celery_send_mail_shared_data_deleted_team.delay(self.identifier, self.title, team_id) + self.save() def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None): @@ -258,10 +266,15 @@ class RecordableObjectMixin(models.Model): self.save() self.log.add(action) - shared_users = self.users.all().values_list("id", flat=True) + shared_users = self.shared_users.values_list("id", flat=True) + shared_teams = self.shared_teams.values_list("id", flat=True) + for user_id in shared_users: celery_send_mail_shared_data_unrecorded.delay(self.identifier, self.title, user_id) + for team_id in shared_teams: + celery_send_mail_shared_data_unrecorded_team.delay(self.identifier, self.title, team_id) + return action def set_recorded(self, user): @@ -281,10 +294,15 @@ class RecordableObjectMixin(models.Model): self.save() self.log.add(action) - shared_users = self.users.all().values_list("id", flat=True) + shared_users = self.shared_users.values_list("id", flat=True) + shared_teams = self.shared_teams.values_list("id", flat=True) + for user_id in shared_users: celery_send_mail_shared_data_recorded.delay(self.identifier, self.title, user_id) + for team_id in shared_teams: + celery_send_mail_shared_data_recorded_team.delay(self.identifier, self.title, team_id) + return action def unrecord(self, performing_user, request: HttpRequest = None): @@ -367,10 +385,15 @@ class CheckableObjectMixin(models.Model): self.save() # Send mail - shared_users = self.users.all().values_list("id", flat=True) + shared_users = self.shared_users.values_list("id", flat=True) for user_id in shared_users: celery_send_mail_shared_data_checked.delay(self.identifier, self.title, user_id) + # Send mail + shared_teams = self.shared_teams.values_list("id", flat=True) + for team_id in shared_teams: + celery_send_mail_shared_data_checked_team.delay(self.identifier, self.title, team_id) + self.log.add(action) return action @@ -488,8 +511,8 @@ class ShareableObjectMixin(models.Model): """ self.users.set(user_list) - def update_sharing_user(self, form): - """ Adds a new user with shared access to the object + def _update_shared_teams(self, form): + """ Updates shared access on the object for teams Args: form (ShareModalForm): The form holding the data @@ -497,33 +520,46 @@ class ShareableObjectMixin(models.Model): Returns: """ - from user.models import User form_data = form.cleaned_data + shared_teams = self.shared_teams # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent accessing_teams = form_data["teams"] - removed_team_users = self.teams.all().exclude( + removed_teams = shared_teams.exclude( id__in=accessing_teams - ).values_list("users__id", flat=True) - accessing_team_users = User.objects.filter( - id__in=accessing_teams.values_list("users", flat=True) - ) - new_team_users = accessing_team_users.exclude( - id__in=self.shared_users ).values_list("id", flat=True) + new_teams = accessing_teams.exclude( + id__in=shared_teams + ).values_list("id", flat=True) + + for team_id in new_teams: + celery_send_mail_shared_access_given_team.delay(self.identifier, self.title, team_id) + for team_id in removed_teams: + celery_send_mail_shared_access_removed_team.delay(self.identifier, self.title, team_id) + + self.share_with_team_list(accessing_teams) + + def _update_shared_users(self, form): + """ Updates shared access on the object for single users + + Args: + form (ShareModalForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + shared_users = self.shared_users # Fetch selected users accessing_users = form_data["users"] - removed_users = self.users.all().exclude( + removed_users = shared_users.exclude( id__in=accessing_users ).values_list("id", flat=True) new_users = accessing_users.exclude( - id__in=self.shared_users + id__in=shared_users ).values_list("id", flat=True) - new_users = new_users.union(new_team_users) - removed_users = removed_users.union(removed_team_users) - # Send mails for user_id in removed_users: celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id) @@ -532,7 +568,18 @@ class ShareableObjectMixin(models.Model): # Set new shared users self.share_with_user_list(accessing_users) - self.share_with_team_list(accessing_teams) + + def update_shared_access(self, form): + """ Updates shared access on the object + + Args: + form (ShareModalForm): The form holding the data + + Returns: + + """ + self._update_shared_teams(form) + self._update_shared_users(form) @property def shared_users(self) -> QuerySet: diff --git a/konova/tasks.py b/konova/tasks.py index c74a2bd..798effb 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -38,6 +38,20 @@ def celery_send_mail_shared_access_given(obj_identifier, obj_title=None, user_id user.send_mail_shared_access_given(obj_identifier, obj_title) +@shared_task +def celery_send_mail_shared_access_removed_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_access_removed(obj_identifier, obj_title) + + +@shared_task +def celery_send_mail_shared_access_given_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_access_given_team(obj_identifier, obj_title) + + @shared_task def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None): from user.models import User @@ -52,6 +66,20 @@ def celery_send_mail_shared_data_unrecorded(obj_identifier, obj_title=None, user user.send_mail_shared_data_unrecorded(obj_identifier, obj_title) +@shared_task +def celery_send_mail_shared_data_recorded_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_data_recorded(obj_identifier, obj_title) + + +@shared_task +def celery_send_mail_shared_data_unrecorded_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_data_unrecorded(obj_identifier, obj_title) + + @shared_task def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None): from user.models import User @@ -64,3 +92,17 @@ def celery_send_mail_shared_data_checked(obj_identifier, obj_title=None, user_id from user.models import User user = User.objects.get(id=user_id) user.send_mail_shared_data_checked(obj_identifier, obj_title) + + +@shared_task +def celery_send_mail_shared_data_deleted_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_data_deleted(obj_identifier, obj_title) + + +@shared_task +def celery_send_mail_shared_data_checked_team(obj_identifier, obj_title=None, team_id=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_shared_data_checked(obj_identifier, obj_title) diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py index 33907f3..dd8eef1 100644 --- a/konova/utils/mailer.py +++ b/konova/utils/mailer.py @@ -92,6 +92,144 @@ class Mailer: msg ) + def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team): + """ Send a mail if a team just got access to the object + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/sharing/shared_access_given_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared access given").format(obj_identifier), + msg + ) + + def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team): + """ Send a mail if a team just lost access to the object + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/sharing/shared_access_removed_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared access removed").format(obj_identifier), + msg + ) + + def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team): + """ Send a mail if data has just been unrecorded + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared data unrecorded").format(obj_identifier), + msg + ) + + def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team): + """ Send a mail if data has just been recorded + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/recording/shared_data_recorded_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared data recorded").format(obj_identifier), + msg + ) + + def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team): + """ Send a mail if data has just been checked + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/checking/shared_data_checked_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared data checked").format(obj_identifier), + msg + ) + + def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team): + """ Send a mail if data has just been deleted + + Args: + obj_identifier (str): The object identifier + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/deleting/shared_data_deleted_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Shared data deleted").format(obj_identifier), + msg + ) + def send_mail_shared_data_recorded(self, obj_identifier, obj_title, user): """ Send a mail if the user's shared data has just been unrecorded diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index ced4ab1ce70235e9c2f809480b12816984ba7a18..9437cb993a50c84e2c397c3f30f0e5d7cf7ba041 100644 GIT binary patch delta 11524 zcmZYF30PKD9>?(uDguHaA|gscispv6f#8B+qKHeNxuT`zQi2PLOD@+fx7;ZelgvuZ zWi)fi+$tS2(_C_?v|KXHur$*w%XGfK_Z;VGo;lC+=X1`v_n!UU*XEg%?|Yxw;q9Ia zDgLTMD&y@qmGJFg$JtN5cC>08r$ueY3G#BB$1$IBMjgkQhL>?NX2&{C7rcRkF}1Ga zbil1R6^q9?P9)C6cwCJEcn(7x$91le38dn_sc-`7IgStI%2*1kAvZepQ4hAlk(h~k z?jV-NFRfQl&;5o~(5t?=9)qEj8)7h~VHx^&df19=4C2CgEQ@nc53E3SybaaSJ`BSz ztUqD}%1#5v35N*OeGRNB7(zJ%tKoB40$;{h`gazP(E}f0F+7T_o^t}V62%%i&KmT` zvbY5`pifaNa0)e`OIRNNL_Z8^q#3fUsCHse&o#29pzBXXM_bVY)xZE-c2N(`Mz-6@ z$4BvF)JlDiCGj>^$9t%isLDob3!0+_+}7F~eJSUno*Ns_`fF*XP@xrAgc{*W48`|Q z4IDrnqO+(O-NkZPD#6S=8a437s18KUSM0L;?b^mbG3Xeq%Y#J8FS5Ol#aP5Wls1fc! zHFOZw(ATKFEJ7{aZA{0%kuf@Hi5w)HgnBM8$vhW{8elAH04-4Mr(*>6Kn>8%BcmCN zw-r-Sdp8@^;S%c#)RM17HS`gx!=tDHo=3IwBdX(H>~-%Zro9l<);@w-iCW0>u9HY6 zjEXdS!!xK4Q4Z=1%tCd%++N>`8sJ{kb4Rc`UPf(AU{gL`7>z1-z>e4#we;_yCh$J` z>iz$OjF$Q+s^JT$8T^DA>3wTJGgBXd+Pm7Q86~4uq8)0fvrsEC0JX%!P%AkGwL+6o z1DlC0_5K%$PD?mvqWcqN(jA4;9&HAoCAZyFbd~mByK{j*iqC3E}{m09d%}W zQp}mDh&m(nPy=p*;n)SWg+o$YGqcH5c=iONxlo8|=tI;VoH`a@&t-6Aqvw~XM z`>2k5Tbiv1L)Ay1p0A2pfg1LDLzj$}wlQkSlTia{gPK_us-x$z7!E-VXgKQrv9>%F zb%W23Z-`sc$bdH5E>K)X2Lczb8(AERM_28&{zA z^lj9h??lGtoUrBa)@JYHP|u~J29l2IxF>2$24b+LmP;m{iYcfM%?{K|j-k%LInk}xHpqz~Q`E7+h_%v$3{ZSLjvra*^w*XyTC?KO7SEKfR8&<+E zPy@MvzW4z3;qgs39fYFZmWuc!HpAgK7dxYOJICpST~SNF4%OdwTmGaS>#vcXprRC> zL3MP+mT%Y_|3YnrFYBfyk3?-rU95~P@pXI_)t+|;bJ+Yaka7g-{>M-gYHDrQf%Pv< zMHUrmcn}8Qc+`Mqp&DF-eprZ}tw1gHM$~I}(0T^_DPKc%bjQ~FbTsXkM}2r|pxSTh z+KM))4tkWq)Ke>iHzp zC)n*yMz7fj)LyPdjqEV$Q~MR_RNp~0P&$KyhSgEm2Oz&w&Py1C`%rJw3FL)w{4>o& zGEs-LCu)WIV2IxTY%+T7T+~cnwdM7w8Sb^7MxB8o)O`<70}bnBI(`(jQuVP6CZJX@ z4fR|m>MRVv(wOHdv;R}c)SzM^>cL&8GjIge!C71W0oA~5TlVINs~m)CFcPa_ZPXTa zLTy=B)S2mx8eo4^yMxe={+&@|)ZrxTiF2_g7U6g-_mue=&PP_&*@o)Kw~MhZMp5o! z9fjJmC0G^T#fEqa_5JYgY9`zo-KVJ-Mn)q(j#{!)sKa;;wP#mRpWf?O9dDuzQH5^i zdSle{%~3N=LCv%+s-2Fu+#S_^Z}e!VK`nz4csfs%)A7u9B3_L>&v0eTBNOaYm!leu~-Z1S$kn^$}_P6Zo@<@ z!dR@@%RJW!_4ahZHaHPG;6c>D!h4$!O+8e(owXlU(EC4uOhqm%L7jyys6F}+>*6JQ zy(}kHdt4WFUlY_BNW~N{esJ(x%8Q@jhYa)j@KwafzK-)69z_jo(6dYicVJigcZNO3 z@=|dS^#z>X&v9Dd8r0qu;bV9YwWl%tO?@Kj@TH>$(%IS}jX=FFlTjo80QGtv#%g#ROW+^41@EKU+5Cbz)Q3<5 zIg1+LPpF9%8)V=AN`uVOR!5!MCa8{+ZGC%dAM~R>54AO}EibUXhB`|tQ3LxJ)!|k2 z#RnLJzS(AgvDvJ@UWZgFDq$y7$3w9sPC#`y4K;)LsE$`)0B%Nq{0OxIhfphe9CiOS zRJ-?3TTmg#%)BA$wQlc{(TKB94UI-E-4yFM16?1qh|Ce>c%7X`ejtZ*HHuY9b&FWq27)J zRD0>z89Q6opx%c2sP+~Q^&CRi*+51k+J~j^GzQ~$sFB}7HCTF>u{>%^qEIufi5h5v zz1|EpuvFA7^C;UkW2|aAp1}a9?UfloW?Sgi>&uh4VE5m8mNkz zNnO;6q@p^0#$F$d>Trs!pM%=Mm8f=)Vm-b87s-TR*%AEf1goJ&I2S{)00VJ@E$^}( zM-AW#s^h=0ESAeN)#qlwQlZoNCMMu2)Y6?porSBY z2JWH;;5*86R1tMwJ&eaDsP=MEZ^MhI4kw`|G!HefrKk_s8>3kNBr+?h=!6$A9UG1| z4Nb(FlxN^<+>9yMVhsP{#VNQQ{a!R*%)MBh@^xDd8*2uhf;z+*s0sDJP#olv2`4iV z!>|A~qjzn22Wn4`qYlkw>+cvv*>9Y&CThm%sF`J2yP{^Cg=)VqY6Ztw-PvUH;Br() zt58e012xjUsI57Hn!%U0{sL+SH&GqLr^oQiaJDfQ4cn=<#t$}at}KC>2xCFg^N&IumrXA>rox^P5ooWU+1T~O}*aGuW9i7BDJcr2` z#H*sS)6v=uBPjQIiS;i|W*ikd-IGuahKV1>Cd5l4ey|yFE`E1 zygI7g1oXkCSOZ%k=hexd)*85B^!k5a1v_iUO{#AhAqE` zTJk;C&#^M)Z?GEPLp@(*rm2s|&nb67U##lRGJ9Uf`Z#K@pF%a9jp}f$bq;D^%WZip z>b?W0l{$_7cn;OUk5~zR#TX2F*|gUPH6gb(nPOx*p0f5AuSLyh zA9~|yRL5VVA6`TsyoR2RQ4Z z_&4fxYCgvdtQ+dN{-}<|q0Y`+Ou^--34Mq4@Gk1@s5MuIll^Z(rUeyaF%Wm5mhLkQ zz!RtjE}#bVJ66H4d8V9%`p|So4Lr}*FSKq#4d@tZMK7Y-zk?z4?>r!*jzZ>}hAN|G z)C7Yt!`An~I+Vww?q6r!j%s*6YHQA*R^n$Yg%42elv-eZD=MPeX^O7)@C7p3!yIcK z>h&3qdT=soZx>=|T!y1@9oEOH3(ddnx?&B=zhDkV#q@3 zf5qI`09#Ye!glyJK7lte8{=O!hwd%ZDPM&e*d|mvJ5h)E05-%!sMpb1WbTVXO}Hj% z3+gOl{q;aR6`ENRY9-Q84Q5~&>}AVC(DS2$Itz!qdMGU>-VGXJBT{HM=%!eVI0<4YW|U$h0Q51Le-x_4b0~? zQ}2(Nl+)1j-~Y?Vgi~=F%VFR$v!qopn(`A^1^ZiPq0Ytz)F*WZ*2N2`!xmg<&Qcue zzGPcYM=#1#`12-Fn@Dq2L;3`-Vhzl;b+NkbjttQ~riRBypGg zSwczw6;+z{Uc@z~%9%+14EYnbyd19)=cu3T#r|I>bDxT>_yVD%SMf_vjz2Vb90n5~ za(y3hl$dSr{WtahBC6^-p$|}5La77ytiatwF}*IN_YKY)9(n&4+J>6quSAS3+xwgk z$h5b05#)nt=yS?(#7%qCDe}LP?}mR6N?#cqU&_mO57i}H`S-CTCI{Nws_)G>dPu5#gXvij_P zMt(THgL%X&^Y%Ga$X_AeC-e#{?J(C}XBLIt z6!gvx#OcILbJ4kJ<@dz#viZa0J@1hu=_@&g*v9o_qLC_XDo&ZV(kY2g+qy{dj}rdG zCNJh+kxY4FDmTorH;GY15%oUAvxHI!LcfSdh+%}%8m{SofNW@rp1+Iv8$w-A;)1RJ zll+r}QW|lQ=%xK{N!%x9>WWSGurjfT`d@6_ci4rpzKTkplD~p|a1fz%0XyS7TmH#Y z%WH}?sr#AWJLgHg`Rk6N;wYK*_JLA3n(`LUExcNk^|@7QPdrKNrmicIO1?f(o#;Wi z11jAis!={h)F%pvxx_9)|39gj2|2Xz1Jm3GVWSUVKhDyf_P6l<$iI0d_ zTPMy?evD{9KG@c`quho34&oW|N`?5N&ELb8)V0RTM26=M&X?xj))o@Dpb6GNrF!U( zdu(~ArnMj1O7GhGZ1VkW-u2>P{*~lO>!{Q( zol-^O8RBcA3~_{OrMUNZ{Ef2GEh3$WA|9ssTvwWbD~UGb#}d@hP^6|uJ@{{oa z@hW+D51C9dAK@=pmdGN%6HlSiQ1Z{?bNDWHBF2z^m>S!BBfLfUa^VMJFX6|^HO0T| zJty&TVgd1(_FpRyOZ4R?r55Bn5l;~x6Jv?e#36I}zkhF2SNp#uKBts>*>ZvP0d+4C z57Th+{tT!uj`UQr{~gJEPfR2Rb73#xM<|stIPXy|$%C^gpSE@B_8t|R5xIn)3N|I6 zj`slKJ)#!zFr6opujh5c+hjIi4?^iBVxK2x{;an7GThhwADwceQi+b zYYZm3lfOg+5+f+D#R|kEd!KudOt>xNaG{XcNclyK$JdBI$#1~cm`UUjaYSe8okUUc}eDjU0-4*=$Wb;I4J+XFVbd|vV`*!6< Zr47vWROJ^Gobsp8FUQ;wwRDTue*k7fY3cv~ delta 11374 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 Its3a{e>B)xF#rGn diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 94efbd7..051894c 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-18 14:01+0100\n" +"POT-Creation-Date: 2022-02-18 14:50+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1796,27 +1796,27 @@ msgstr "In Zwischenablage kopiert" msgid "{} - Shared access removed" msgstr "{} - Zugriff entzogen" -#: konova/utils/mailer.py:91 +#: konova/utils/mailer.py:91 konova/utils/mailer.py:114 msgid "{} - Shared access given" msgstr "{} - Zugriff freigegeben" -#: konova/utils/mailer.py:114 +#: konova/utils/mailer.py:137 msgid "{} - Shared data recorded" msgstr "{} - Freigegebene Daten verzeichnet" -#: konova/utils/mailer.py:137 +#: konova/utils/mailer.py:160 msgid "{} - Shared data unrecorded" msgstr "{} - Freigegebene Daten entzeichnet" -#: konova/utils/mailer.py:160 +#: konova/utils/mailer.py:183 msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:183 +#: konova/utils/mailer.py:206 msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:204 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:227 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" @@ -2092,7 +2092,9 @@ msgstr "" #: templates/email/recording/shared_data_recorded.html:19 #: templates/email/recording/shared_data_unrecorded.html:19 #: templates/email/sharing/shared_access_given.html:20 +#: templates/email/sharing/shared_access_given_team.html:20 #: templates/email/sharing/shared_access_removed.html:20 +#: templates/email/sharing/shared_access_removed_team.html:20 msgid "Best regards" msgstr "Beste Grüße" @@ -2179,6 +2181,7 @@ msgstr "" "zugehörigen Kompensationen automatisch entzeichnet worden sind." #: templates/email/sharing/shared_access_given.html:4 +#: templates/email/sharing/shared_access_given_team.html:4 msgid "Access shared" msgstr "Zugriff freigegeben" @@ -2187,10 +2190,12 @@ msgid "the following dataset has just been shared with you" msgstr "der folgende Datensatz wurde soeben für Sie freigegeben " #: templates/email/sharing/shared_access_given.html:16 +#: templates/email/sharing/shared_access_given_team.html:16 msgid "This means you can now edit this dataset." msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können." #: templates/email/sharing/shared_access_given.html:17 +#: templates/email/sharing/shared_access_given_team.html:17 msgid "" "The shared dataset appears now by default on your overview for this dataset " "type." @@ -2199,6 +2204,7 @@ msgstr "" "Datensatztyp im KSP gelistet." #: templates/email/sharing/shared_access_given.html:27 +#: templates/email/sharing/shared_access_given_team.html:27 msgid "" "Please note: Shared access on an intervention means you automatically have " "editing access to related compensations." @@ -2207,7 +2213,17 @@ msgstr "" "Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten " "haben." +#: templates/email/sharing/shared_access_given_team.html:8 +#: templates/email/sharing/shared_access_removed_team.html:8 +msgid "Hello team" +msgstr "Hallo Team" + +#: templates/email/sharing/shared_access_given_team.html:10 +msgid "the following dataset has just been shared with your team" +msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben " + #: templates/email/sharing/shared_access_removed.html:4 +#: templates/email/sharing/shared_access_removed_team.html:4 msgid "Shared access removed" msgstr "Freigegebener Zugriff entzogen" @@ -2219,10 +2235,12 @@ msgstr "" "entzogen: " #: templates/email/sharing/shared_access_removed.html:16 +#: templates/email/sharing/shared_access_removed_team.html:16 msgid "However, you are still able to view the dataset content." msgstr "Sie können den Datensatz aber immer noch im KSP einsehen." #: templates/email/sharing/shared_access_removed.html:17 +#: templates/email/sharing/shared_access_removed_team.html:17 msgid "" "Please use the provided search filter on the dataset`s overview pages to " "find them." @@ -2230,6 +2248,14 @@ msgstr "" "Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den " "Übersichtsseiten" +#: templates/email/sharing/shared_access_removed_team.html:10 +msgid "" +"your teams shared access, including editing, has been revoked for the " +"dataset " +msgstr "" +"Ihrem Team wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz " +"entzogen: " + #: templates/email/signature.html:6 msgid "Please do not reply on this mail." msgstr "Bitte antworten Sie nicht auf diese Mail." @@ -4118,6 +4144,9 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ msgid "your teams" +#~ msgstr "Team entfernen" + #~ msgid "Remove check to remove access for this user" #~ msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen" diff --git a/templates/email/checking/shared_data_checked_team.html b/templates/email/checking/shared_data_checked_team.html new file mode 100644 index 0000000..ee81381 --- /dev/null +++ b/templates/email/checking/shared_data_checked_team.html @@ -0,0 +1,28 @@ +{% load i18n %} + +
+

{% trans 'Shared data checked' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'the following dataset has just been checked' %} +
+ {{obj_identifier}} +
+ {{obj_title}} +
+ {% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/deleting/shared_data_deleted_team.html b/templates/email/deleting/shared_data_deleted_team.html new file mode 100644 index 0000000..cedb2a4 --- /dev/null +++ b/templates/email/deleting/shared_data_deleted_team.html @@ -0,0 +1,28 @@ +{% load i18n %} + +
+

{% trans 'Shared data deleted' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'the following dataset has just been deleted' %} +
+ {{obj_identifier}} +
+ "{{obj_title}}" +
+ {% trans 'If this should not have been happened, please contact us. See the signature for details.' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/recording/shared_data_recorded_team.html b/templates/email/recording/shared_data_recorded_team.html new file mode 100644 index 0000000..12efa8f --- /dev/null +++ b/templates/email/recording/shared_data_recorded_team.html @@ -0,0 +1,33 @@ +{% load i18n %} + +
+

{% trans 'Shared data recorded' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'the following dataset has just been recorded' %} +
+ {{obj_identifier}} +
+ "{{obj_title}}" +
+ {% trans 'This means the data is now publicly available, e.g. in LANIS' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ + {% trans 'Please note: Recorded intervention means the compensations are recorded as well.' %} + +
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/recording/shared_data_unrecorded_team.html b/templates/email/recording/shared_data_unrecorded_team.html new file mode 100644 index 0000000..6414155 --- /dev/null +++ b/templates/email/recording/shared_data_unrecorded_team.html @@ -0,0 +1,33 @@ +{% load i18n %} + +
+

{% trans 'Shared data unrecorded' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'the following dataset has just been unrecorded' %} +
+ {{obj_identifier}} +
+ "{{obj_title}}" +
+ {% trans 'This means the data is no longer publicly available.' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ + {% trans 'Please note: Unrecorded intervention means the compensations are unrecorded as well.' %} + +
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/sharing/shared_access_given_team.html b/templates/email/sharing/shared_access_given_team.html new file mode 100644 index 0000000..990ba2d --- /dev/null +++ b/templates/email/sharing/shared_access_given_team.html @@ -0,0 +1,34 @@ +{% load i18n %} + +
+

{% trans 'Access shared' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'the following dataset has just been shared with your team' %} +
+ {{obj_identifier}} +
+ "{{obj_title}}" +
+ {% trans 'This means you can now edit this dataset.' %} + {% trans 'The shared dataset appears now by default on your overview for this dataset type.' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ + {% trans 'Please note: Shared access on an intervention means you automatically have editing access to related compensations.' %} + +
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/sharing/shared_access_removed_team.html b/templates/email/sharing/shared_access_removed_team.html new file mode 100644 index 0000000..5472ef7 --- /dev/null +++ b/templates/email/sharing/shared_access_removed_team.html @@ -0,0 +1,29 @@ +{% load i18n %} + +
+

{% trans 'Shared access removed' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+ {% trans 'your teams shared access, including editing, has been revoked for the dataset ' %} +
+ {{obj_identifier}} +
+ "{{obj_title}}" +
+ {% trans 'However, you are still able to view the dataset content.' %} + {% trans 'Please use the provided search filter on the dataset`s overview pages to find them.' %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/user/models/team.py b/user/models/team.py index c26af3c..e36c95b 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -1,6 +1,7 @@ from django.db import models from konova.models import UuidModel +from konova.utils.mailer import Mailer class Team(UuidModel): @@ -14,3 +15,81 @@ class Team(UuidModel): def __str__(self): return self.name + + 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 + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self) + + def send_mail_shared_access_removed(self, obj_identifier, obj_title): + """ Sends a mail to the team members in case of removed shared access + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self) + + def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title): + """ Sends a mail to the team members in case of unrecorded data + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self) + + def send_mail_shared_data_recorded(self, obj_identifier, obj_title): + """ Sends a mail to the team members in case of unrecorded data + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self) + + def send_mail_shared_data_checked(self, obj_identifier, obj_title): + """ Sends a mail to the team members in case of checked data + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self) + + def send_mail_shared_data_deleted(self, obj_identifier, obj_title): + """ Sends a mail to the team members in case of deleted data + + Args: + obj_identifier (): + obj_title (): + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self) -- 2.38.5