Merge pull request 'master' (#123) from master into Docker
Reviewed-on: SGD-Nord/konova#123
This commit is contained in:
@@ -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})"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
40
konova/migrations/0005_auto_20220216_0856.py
Normal file
40
konova/migrations/0005_auto_20220216_0856.py
Normal 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),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
68
konova/templates/konova/widgets/checkbox-tree-select.html
Normal file
68
konova/templates/konova/widgets/checkbox-tree-select.html
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user