#19 Tests
* refactors CheckableMixin and RecordableMixin into CheckableObject and RecordableObject * adds ShareableObject for wrapping share related fields and functionality * adds share functionality to EcoAccount and EMA, just like Intervention
This commit is contained in:
parent
bfe541f83e
commit
5213c717d9
@ -21,6 +21,8 @@ urlpatterns = [
|
|||||||
path('<id>/state/new', state_new_view, name='acc-new-state'),
|
path('<id>/state/new', state_new_view, name='acc-new-state'),
|
||||||
path('<id>/action/new', action_new_view, name='acc-new-action'),
|
path('<id>/action/new', action_new_view, name='acc-new-action'),
|
||||||
path('<id>/deadline/new', deadline_new_view, name="acc-new-deadline"),
|
path('<id>/deadline/new', deadline_new_view, name="acc-new-deadline"),
|
||||||
|
path('<id>/share/<token>', share_view, name='share'),
|
||||||
|
path('<id>/share', create_share_view, name='share-create'),
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
path('<id>/document/new/', new_document_view, name='acc-new-doc'),
|
path('<id>/document/new/', new_document_view, name='acc-new-doc'),
|
||||||
|
@ -22,7 +22,7 @@ from compensation.managers import CompensationStateManager, EcoAccountDeductionM
|
|||||||
from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker
|
from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker
|
||||||
from intervention.models import Intervention, ResponsibilityData, LegalData
|
from intervention.models import Intervention, ResponsibilityData, LegalData
|
||||||
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
|
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
|
||||||
generate_document_file_upload_path, RecordableMixin
|
generate_document_file_upload_path, RecordableObject, ShareableObject
|
||||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||||
from user.models import UserActionLogEntry
|
from user.models import UserActionLogEntry
|
||||||
|
|
||||||
@ -311,28 +311,11 @@ class CompensationDocument(AbstractDocument):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EcoAccount(AbstractCompensation, RecordableMixin):
|
class EcoAccount(AbstractCompensation, ShareableObject, RecordableObject):
|
||||||
"""
|
"""
|
||||||
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
|
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
|
||||||
with some kind of currency. From this account one is able to deduct currency for current projects.
|
with some kind of currency. From this account one is able to deduct currency for current projects.
|
||||||
"""
|
"""
|
||||||
# Users having access on this object
|
|
||||||
# Not needed in regular Compensation since their access is defined by the linked intervention's access
|
|
||||||
users = models.ManyToManyField(
|
|
||||||
User,
|
|
||||||
help_text="Users having access (shared with)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Refers to "verzeichnen"
|
|
||||||
recorded = models.OneToOneField(
|
|
||||||
UserActionLogEntry,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="Holds data on user and timestamp of this action",
|
|
||||||
related_name="+"
|
|
||||||
)
|
|
||||||
|
|
||||||
deductable_surface = models.FloatField(
|
deductable_surface = models.FloatField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if has_access %}
|
||||||
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:share-create' obj.id %}">
|
||||||
|
{% fa5_icon 'share-alt' %}
|
||||||
|
</button>
|
||||||
{% if is_ets_member %}
|
{% if is_ets_member %}
|
||||||
{% if obj.recorded %}
|
{% if obj.recorded %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
|
||||||
|
@ -18,7 +18,7 @@ from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm
|
|||||||
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
|
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
|
||||||
from compensation.models import EcoAccount, EcoAccountDocument
|
from compensation.models import EcoAccount, EcoAccountDocument
|
||||||
from compensation.tables import EcoAccountTable
|
from compensation.tables import EcoAccountTable
|
||||||
from intervention.forms.modalForms import NewDeductionModalForm
|
from intervention.forms.modalForms import NewDeductionModalForm, ShareInterventionModalForm
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required
|
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required
|
||||||
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm
|
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm
|
||||||
@ -511,3 +511,63 @@ def report_view(request:HttpRequest, id: str):
|
|||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def share_view(request: HttpRequest, id: str, token: str):
|
||||||
|
""" Performs sharing of an eco account
|
||||||
|
|
||||||
|
If token given in url is not valid, the user will be redirected to the dashboard
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): EcoAccount's id
|
||||||
|
token (str): Access token for EcoAccount
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
obj = get_object_or_404(EcoAccount, id=id)
|
||||||
|
# Check tokens
|
||||||
|
if obj.access_token == token:
|
||||||
|
# Send different messages in case user has already been added to list of sharing users
|
||||||
|
if obj.is_shared_with(user):
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
_("{} has already been shared with you").format(obj.identifier)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("{} has been shared with you").format(obj.identifier)
|
||||||
|
)
|
||||||
|
obj.users.add(user)
|
||||||
|
return redirect("compensation:acc-detail", id=id)
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_("Share link invalid"),
|
||||||
|
extra_tags="danger",
|
||||||
|
)
|
||||||
|
return redirect("home")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@default_group_required
|
||||||
|
def create_share_view(request: HttpRequest, id: str):
|
||||||
|
""" Renders sharing form for an eco account
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): EcoAccount's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj = get_object_or_404(EcoAccount, id=id)
|
||||||
|
form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
msg_success=_("Share settings updated")
|
||||||
|
)
|
@ -7,12 +7,11 @@ from django.db.models import QuerySet
|
|||||||
from compensation.models import AbstractCompensation
|
from compensation.models import AbstractCompensation
|
||||||
from ema.managers import EmaManager
|
from ema.managers import EmaManager
|
||||||
from ema.utils.quality import EmaQualityChecker
|
from ema.utils.quality import EmaQualityChecker
|
||||||
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableMixin
|
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObject, ShareableObject
|
||||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||||
from user.models import UserActionLogEntry
|
|
||||||
|
|
||||||
|
|
||||||
class Ema(AbstractCompensation, RecordableMixin):
|
class Ema(AbstractCompensation, ShareableObject, RecordableObject):
|
||||||
"""
|
"""
|
||||||
EMA = Ersatzzahlungsmaßnahme
|
EMA = Ersatzzahlungsmaßnahme
|
||||||
(compensation actions from payments)
|
(compensation actions from payments)
|
||||||
@ -28,23 +27,6 @@ class Ema(AbstractCompensation, RecordableMixin):
|
|||||||
EMA therefore holds data like a compensation: actions, before-/after-states, deadlines, ...
|
EMA therefore holds data like a compensation: actions, before-/after-states, deadlines, ...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Users having access on this object
|
|
||||||
# Not needed in regular Compensation since their access is defined by the linked intervention's access
|
|
||||||
users = models.ManyToManyField(
|
|
||||||
User,
|
|
||||||
help_text="Users having access (shared with)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Refers to "verzeichnen"
|
|
||||||
recorded = models.OneToOneField(
|
|
||||||
UserActionLogEntry,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="Holds data on user and timestamp of this action",
|
|
||||||
related_name="+"
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = EmaManager()
|
objects = EmaManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if has_access %}
|
||||||
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}">
|
||||||
|
{% fa5_icon 'share-alt' %}
|
||||||
|
</button>
|
||||||
{% if is_ets_member %}
|
{% if is_ets_member %}
|
||||||
{% if obj.recorded %}
|
{% if obj.recorded %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
|
||||||
|
@ -22,6 +22,8 @@ urlpatterns = [
|
|||||||
path('<id>/state/new', state_new_view, name='new-state'),
|
path('<id>/state/new', state_new_view, name='new-state'),
|
||||||
path('<id>/action/new', action_new_view, name='new-action'),
|
path('<id>/action/new', action_new_view, name='new-action'),
|
||||||
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
|
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
|
||||||
|
path('<id>/share/<token>', share_view, name='share'),
|
||||||
|
path('<id>/share', create_share_view, name='share-create'),
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
# Document remove route can be found in konova/urls.py
|
# Document remove route can be found in konova/urls.py
|
||||||
|
65
ema/views.py
65
ema/views.py
@ -10,8 +10,9 @@ import compensation
|
|||||||
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
|
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
|
||||||
from ema.forms import NewEmaForm, EditEmaForm
|
from ema.forms import NewEmaForm, EditEmaForm
|
||||||
from ema.tables import EmaTable
|
from ema.tables import EmaTable
|
||||||
|
from intervention.forms.modalForms import ShareInterventionModalForm
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import conservation_office_group_required
|
from konova.decorators import conservation_office_group_required, default_group_required
|
||||||
from ema.models import Ema, EmaDocument
|
from ema.models import Ema, EmaDocument
|
||||||
from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm
|
from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
@ -460,4 +461,64 @@ def report_view(request:HttpRequest, id: str):
|
|||||||
"actions": actions,
|
"actions": actions,
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def share_view(request: HttpRequest, id: str, token: str):
|
||||||
|
""" Performs sharing of an ema
|
||||||
|
|
||||||
|
If token given in url is not valid, the user will be redirected to the dashboard
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): EMA's id
|
||||||
|
token (str): Access token for EMA
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
obj = get_object_or_404(Ema, id=id)
|
||||||
|
# Check tokens
|
||||||
|
if obj.access_token == token:
|
||||||
|
# Send different messages in case user has already been added to list of sharing users
|
||||||
|
if obj.is_shared_with(user):
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
_("{} has already been shared with you").format(obj.identifier)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("{} has been shared with you").format(obj.identifier)
|
||||||
|
)
|
||||||
|
obj.users.add(user)
|
||||||
|
return redirect("ema:detail", id=id)
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_("Share link invalid"),
|
||||||
|
extra_tags="danger",
|
||||||
|
)
|
||||||
|
return redirect("home")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@default_group_required
|
||||||
|
def create_share_view(request: HttpRequest, id: str):
|
||||||
|
""" Renders sharing form for an Ema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): Ema's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj = get_object_or_404(Ema, id=id)
|
||||||
|
form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
msg_success=_("Share settings updated")
|
||||||
|
)
|
@ -68,8 +68,9 @@ class ShareInterventionModalForm(BaseModalForm):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Initialize share_link field
|
# Initialize share_link field
|
||||||
|
url_name = f"{self.instance._meta.app_label}:share"
|
||||||
self.share_link = self.request.build_absolute_uri(
|
self.share_link = self.request.build_absolute_uri(
|
||||||
reverse("intervention:share", args=(self.instance.id, self.instance.access_token,))
|
reverse(url_name, args=(self.instance.id, self.instance.access_token,))
|
||||||
)
|
)
|
||||||
self.initialize_form_field(
|
self.initialize_form_field(
|
||||||
"url",
|
"url",
|
||||||
|
@ -17,7 +17,7 @@ from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVA
|
|||||||
from intervention.managers import InterventionManager
|
from intervention.managers import InterventionManager
|
||||||
from intervention.utils.quality import InterventionQualityChecker
|
from intervention.utils.quality import InterventionQualityChecker
|
||||||
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \
|
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \
|
||||||
generate_document_file_upload_path, RecordableMixin, CheckableMixin
|
generate_document_file_upload_path, RecordableObject, CheckableObject, ShareableObject
|
||||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT
|
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT
|
||||||
from konova.utils import generators
|
from konova.utils import generators
|
||||||
from user.models import UserActionLogEntry
|
from user.models import UserActionLogEntry
|
||||||
@ -171,7 +171,7 @@ class LegalData(UuidModel):
|
|||||||
revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL)
|
revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
|
||||||
class Intervention(BaseObject, RecordableMixin, CheckableMixin):
|
class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject):
|
||||||
"""
|
"""
|
||||||
Interventions are e.g. construction sites where nature used to be.
|
Interventions are e.g. construction sites where nature used to be.
|
||||||
"""
|
"""
|
||||||
@ -191,74 +191,11 @@ class Intervention(BaseObject, RecordableMixin, CheckableMixin):
|
|||||||
)
|
)
|
||||||
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
|
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
# Checks - Refers to "Genehmigen" but optional
|
|
||||||
checked = models.OneToOneField(
|
|
||||||
UserActionLogEntry,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="Holds data on user and timestamp of this action",
|
|
||||||
related_name="+"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Refers to "verzeichnen"
|
|
||||||
recorded = models.OneToOneField(
|
|
||||||
UserActionLogEntry,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="Holds data on user and timestamp of this action",
|
|
||||||
related_name="+"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Users having access on this object
|
|
||||||
users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
|
|
||||||
access_token = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="Used for sharing access",
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = InterventionManager()
|
objects = InterventionManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} ({})".format(self.identifier, self.title)
|
return "{} ({})".format(self.identifier, self.title)
|
||||||
|
|
||||||
def generate_access_token(self, make_unique: bool = False, rec_depth: int = 5):
|
|
||||||
""" Creates a new access token for the intervention
|
|
||||||
|
|
||||||
Tokens are not used for identification of a table row. The share logic checks the intervention id as well
|
|
||||||
as the given token. Therefore two different interventions can hold the same access_token without problems.
|
|
||||||
For (possible) future changes to the share logic, the make_unique parameter may be used for checking whether
|
|
||||||
the access_token is already used in any intervention. If so, tokens will be generated as long as a free token
|
|
||||||
can be found.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
make_unique (bool): Perform check on uniqueness over all intervention entries
|
|
||||||
rec_depth (int): How many tries for generating a free random token (only if make_unique)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Make sure we won't end up in an infinite loop of trying to generate access_tokens
|
|
||||||
rec_depth = rec_depth - 1
|
|
||||||
if rec_depth < 0 and make_unique:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Access token generating for {} does not seem to find a free random token! Aborted!".format(self.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create random token
|
|
||||||
token = generators.generate_random_string(15, True, True, False)
|
|
||||||
token_used_in = Intervention.objects.filter(access_token=token)
|
|
||||||
# Make sure the token is not used anywhere as access_token, yet.
|
|
||||||
# Make use of QuerySet lazy method for checking if it exists or not.
|
|
||||||
if token_used_in and make_unique:
|
|
||||||
self.generate_access_token(make_unique, rec_depth)
|
|
||||||
else:
|
|
||||||
self.access_token = token
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" Custom save functionality
|
""" Custom save functionality
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION
|
|||||||
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
|
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
|
||||||
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
|
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
|
||||||
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
|
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
|
||||||
|
from konova.utils import generators
|
||||||
from konova.utils.generators import generate_random_string
|
from konova.utils.generators import generate_random_string
|
||||||
from user.models import UserActionLogEntry, UserAction
|
from user.models import UserActionLogEntry, UserAction
|
||||||
|
|
||||||
@ -315,12 +316,23 @@ class Geometry(BaseResource):
|
|||||||
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
|
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
|
||||||
|
|
||||||
|
|
||||||
class RecordableMixin:
|
class RecordableObject(models.Model):
|
||||||
""" Mixin to be combined with BaseObject class
|
""" Wraps record related fields and functionality
|
||||||
|
|
||||||
Provides functionality related to un/recording of data
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Refers to "verzeichnen"
|
||||||
|
recorded = models.OneToOneField(
|
||||||
|
UserActionLogEntry,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Holds data on user and timestamp of this action",
|
||||||
|
related_name="+"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
def set_unrecorded(self, user: User):
|
def set_unrecorded(self, user: User):
|
||||||
""" Perform unrecording
|
""" Perform unrecording
|
||||||
|
|
||||||
@ -370,12 +382,19 @@ class RecordableMixin:
|
|||||||
self.set_unrecorded(user)
|
self.set_unrecorded(user)
|
||||||
|
|
||||||
|
|
||||||
class CheckableMixin:
|
class CheckableObject(models.Model):
|
||||||
""" Mixin to be combined with BaseObject class
|
# Checks - Refers to "Genehmigen" but optional
|
||||||
|
checked = models.OneToOneField(
|
||||||
|
UserActionLogEntry,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Holds data on user and timestamp of this action",
|
||||||
|
related_name="+"
|
||||||
|
)
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
Provides functionality related to un/checking of data
|
|
||||||
|
|
||||||
"""
|
|
||||||
def set_unchecked(self, user: User):
|
def set_unchecked(self, user: User):
|
||||||
""" Perform unrecording
|
""" Perform unrecording
|
||||||
|
|
||||||
@ -417,3 +436,53 @@ class CheckableMixin:
|
|||||||
self.set_checked(user)
|
self.set_checked(user)
|
||||||
else:
|
else:
|
||||||
self.set_unchecked(user)
|
self.set_unchecked(user)
|
||||||
|
|
||||||
|
|
||||||
|
class ShareableObject(models.Model):
|
||||||
|
# Users having access on this object
|
||||||
|
users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
|
||||||
|
access_token = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Used for sharing access",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def generate_access_token(self, make_unique: bool = False, rec_depth: int = 5):
|
||||||
|
""" Creates a new access token for the data
|
||||||
|
|
||||||
|
Tokens are not used for identification of a table row. The share logic checks the intervention id as well
|
||||||
|
as the given token. Therefore two different interventions can hold the same access_token without problems.
|
||||||
|
For (possible) future changes to the share logic, the make_unique parameter may be used for checking whether
|
||||||
|
the access_token is already used in any intervention. If so, tokens will be generated as long as a free token
|
||||||
|
can be found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
make_unique (bool): Perform check on uniqueness over all intervention entries
|
||||||
|
rec_depth (int): How many tries for generating a free random token (only if make_unique)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Make sure we won't end up in an infinite loop of trying to generate access_tokens
|
||||||
|
rec_depth = rec_depth - 1
|
||||||
|
if rec_depth < 0 and make_unique:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Access token generating for {} does not seem to find a free random token! Aborted!".format(self.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create random token
|
||||||
|
token = generators.generate_random_string(15, True, True, False)
|
||||||
|
# Check dynamically wheter there is another instance of that model, which holds this random access token
|
||||||
|
_model = self._meta.concrete_model
|
||||||
|
token_used_in = _model.objects.filter(access_token=token)
|
||||||
|
# Make sure the token is not used anywhere as access_token, yet.
|
||||||
|
# Make use of QuerySet lazy method for checking if it exists or not.
|
||||||
|
if token_used_in and make_unique:
|
||||||
|
self.generate_access_token(make_unique, rec_depth)
|
||||||
|
else:
|
||||||
|
self.access_token = token
|
||||||
|
self.save()
|
||||||
|
Loading…
Reference in New Issue
Block a user