Merge pull request 'master' (#123) from master into Docker

Reviewed-on: SGD-Nord/konova#123
This commit is contained in:
2022-02-18 15:21:29 +01:00
85 changed files with 2336 additions and 572 deletions

View File

@@ -5,8 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
import collections
from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView
from user.models import User
from django.core.exceptions import ImproperlyConfigured
from konova.utils.message_templates import UNGROUPED
from user.models import User, Team
from django.db.models import Q
from codelist.models import KonovaCode
@@ -20,7 +25,7 @@ from intervention.models import Intervention
class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are accessible for the requesting user and already are recorded
Only returns entries that are already recorded and not deleted
"""
def get_queryset(self):
@@ -29,7 +34,6 @@ class EcoAccountAutocomplete(Select2QuerySetView):
qs = EcoAccount.objects.filter(
deleted=None,
recorded__isnull=False,
users__in=[self.request.user],
).order_by(
"identifier"
)
@@ -65,27 +69,40 @@ 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):
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
class ShareTeamAutocomplete(Select2QuerySetView):
""" Autocomplete for share with teams
"""
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 = qs.filter(
name__icontains=self.q
)
qs = qs.order_by(
"name"
)
return qs
@@ -139,6 +156,8 @@ class KonovaCodeAutocomplete(Select2GroupQuerySetView):
q_or |= Q(short_name__icontains=keyword)
q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
q_or |= Q(parent__parent__long_name__icontains=keyword)
q_or |= Q(parent__parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs
@@ -181,7 +200,7 @@ class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
"long_name"
)
@@ -214,6 +233,41 @@ class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
super_groups = collections.OrderedDict()
object_list = context['object_list']
for result in object_list:
group = result.parent if result.parent else None
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
super_group = result.parent.parent if result.parent else None
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
super_groups.setdefault(super_group_name, {})
super_groups[super_group_name].setdefault(group_name, [])
super_groups[super_group_name][group_name].append(result)
return [{
'id': None,
'text': super_group,
'children': [{
"id": None,
"text": group,
"children": [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
} for super_group, groups in super_groups.items()]
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
"""
@@ -239,7 +293,7 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"parent__long_name",
"long_name",
)
def get_result_label(self, result):
@@ -284,6 +338,11 @@ class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
self.c = CODELIST_REGISTRATION_OFFICE_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
@@ -297,4 +356,4 @@ class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
return f"{result.long_name} ({result.short_name})"

View File

@@ -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

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.1.3 on 2022-02-16 07:56
from django.db import migrations, transaction
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES__ID
def migrate_biotopes_from_974_to_654(apps, schema_editor):
KonovaCode = apps.get_model("codelist", "KonovaCode")
CompensationState = apps.get_model("compensation", "CompensationState")
all_states = CompensationState.objects.all()
with transaction.atomic():
for state in all_states:
new_biotope_code = KonovaCode.objects.get(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_selectable=True,
is_archived=False,
short_name=state.biotope_type.short_name,
)
state.biotope_type = new_biotope_code
state.save()
all_states = CompensationState.objects.all()
after_state_list_elements = all_states.filter(
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES__ID]
)
if after_state_list_elements.count() > 0:
raise Exception("Still states with wrong codelist entries!")
class Migration(migrations.Migration):
dependencies = [
('konova', '0004_auto_20220209_0839'),
]
operations = [
migrations.RunPython(migrate_biotopes_from_974_to_654),
]

View File

@@ -15,8 +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
from user.models import User
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
@@ -28,7 +30,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 +51,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 +95,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 +106,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 +117,7 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
if self.deleted:
# Nothing to do here
return
@@ -131,9 +133,14 @@ 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: 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 +151,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 +169,7 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
user_action = UserActionLogEntry.objects.create(
user=user,
action=action,
@@ -229,7 +238,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 +249,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 +258,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if not self.recorded:
return None
action = UserActionLogEntry.get_unrecorded_action(user)
@@ -256,13 +266,18 @@ 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: User):
def set_recorded(self, user):
""" Perform recording
Args:
@@ -271,6 +286,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.recorded:
return None
action = UserActionLogEntry.get_recorded_action(user)
@@ -278,13 +294,18 @@ 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: User, request: HttpRequest = None):
def unrecord(self, performing_user, request: HttpRequest = None):
""" Unrecords a dataset
Args:
@@ -318,7 +339,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 +367,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 +376,7 @@ class CheckableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.checked:
# Nothing to do
return
@@ -363,17 +385,23 @@ 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
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)")
teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)")
access_token = models.CharField(
max_length=255,
null=True,
@@ -420,7 +448,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
@@ -431,9 +459,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: 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:
@@ -445,7 +500,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:
@@ -456,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
@@ -466,25 +521,65 @@ class ShareableObjectMixin(models.Model):
"""
form_data = form.cleaned_data
shared_teams = self.shared_teams
keep_accessing_users = form_data["users"]
new_accessing_users = list(form_data["user_select"].values_list("id", flat=True))
accessing_users = keep_accessing_users + new_accessing_users
users = User.objects.filter(
# Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent
accessing_teams = form_data["teams"]
removed_teams = shared_teams.exclude(
id__in=accessing_teams
).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 = shared_users.exclude(
id__in=accessing_users
)
removed_users = self.users.all().exclude(
id__in=accessing_users
).values("id")
).values_list("id", flat=True)
new_users = accessing_users.exclude(
id__in=shared_users
).values_list("id", flat=True)
# 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_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(accessing_users)
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:
@@ -495,6 +590,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

View File

@@ -242,15 +242,24 @@ Similar to bootstraps 'shadow-lg'
.select2-results__option--highlighted{
background-color: var(--rlp-red) !important;
}
/*
.select2-container--default .select2-results__group{
background-color: var(--rlp-gray-light);
}
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em !important;
padding-left: 1em !important;
}
*/
.select2-results__options--nested{
padding-left: 1em !important;
}
.select2-container--default .select2-results > .select2-results__options{
max-height: 500px !important;
}
/*
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em;
}
}
*/

View File

@@ -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)

View File

@@ -0,0 +1,21 @@
{% load l10n fontawesome_5 %}
{% for code in codes %}
<div class="ml-4 tree-element">
<label class="tree-label" role="{% if not code.is_leaf%}button{% endif %}" for="input_{{code.pk|unlocalize}}" id="{{code.pk|unlocalize}}" data-toggle="collapse" data-target="#children_{{code.pk|unlocalize}}" aria-expanded="true" aria-controls="children_{{code.pk|unlocalize}}">
{% if code.is_leaf%}
<input class="tree-input" id="input_{{code.pk|unlocalize}}" name="{{ widget.name }}" type="checkbox" value="{{code.pk|unlocalize}}" {% if code.pk|unlocalize in widget.value %}checked{% endif %}/>
{% else %}
{% fa5_icon 'angle-right' %}
{% endif %}
{{code.long_name}}
</label>
{% if not code.is_leaf %}
<div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children">
{% with code.children as codes %}
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
{% endwith %}
</div>
{% endif %}
</div>
{% endfor %}

View File

@@ -0,0 +1,68 @@
{% load i18n %}
<div class="ml-4 mb-4">
<input id="tree-search-input" class="form-control" type="text" placeholder="{% trans 'Search' %}"/>
</div>
<div id="tree-root">
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
</div>
<script>
function toggleSelectedCssClass(element){
element = $(element);
var cssClass = "badge rlp-r"
var directParent = element.closest(".tree-element-children")
var root = element.parents(".tree-element-children")
var otherCheckedInputsOfParent = directParent.find('.tree-input:checked');
var otherCheckedInputsOfRoot = root.find('.tree-input:checked');
if(otherCheckedInputsOfParent.length == 0){
var parentLabel = directParent.siblings(".tree-label");
parentLabel.removeClass(cssClass)
if(otherCheckedInputsOfRoot.length == 0){
var rootLabel = root.siblings(".tree-label")
rootLabel.removeClass(cssClass)
}
}else{
var rootAndParentLabel = root.siblings(".tree-label");
rootAndParentLabel.addClass(cssClass);
}
}
function changeHandler(event){
toggleSelectedCssClass(this);
}
function searchInputHandler(event){
var elem = $(this);
var val = elem.val()
var allTreeElements = $(".tree-element")
var allTreeElementsContain = $(".tree-element").filter(function(){
var reg = new RegExp(val, "i");
return reg.test($(this).text());
}
);
if(val.length > 0){
allTreeElements.hide()
allTreeElementsContain.show()
}else{
allTreeElements.show()
}
}
// Add event listener on search input
$("#tree-search-input").keyup(searchInputHandler)
// Add event listener on changed checkboxes
$(".tree-input").change(changeHandler);
// initialize all pre-checked checkboxes (e.g. on an edit form)
var preCheckedElements = $(".tree-input:checked");
preCheckedElements.each(function (index, element){
toggleSelectedCssClass(element);
})
</script>

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -7,16 +7,18 @@ Created on: 02.08.21
"""
from django.utils.translation import gettext_lazy as _
UNGROUPED = _("Ungrouped")
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")
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.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
# FILES
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")

View File

@@ -9,9 +9,12 @@ from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, FileResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
from compensation.models import Compensation, EcoAccount
from intervention.models import Intervention
from konova.contexts import BaseContext