Access url for interventions

* adds access_token as new attribute
* adds generating of access_token
* adds new form
* adds two new routes for sharing an intervention
* adds translation
* adds render_submit to BaseModalForm which triggers rendering the modal footer
This commit is contained in:
mipel
2021-07-30 13:30:42 +02:00
parent b7ab9f6f55
commit 2f33e5fba9
9 changed files with 345 additions and 169 deletions

View File

@@ -15,7 +15,7 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.forms import BaseForm
from konova.forms import BaseForm, BaseModalForm
from konova.models import Document
from konova.settings import DEFAULT_LAT, DEFAULT_LON, DEFAULT_ZOOM
from organisation.models import Organisation
@@ -220,6 +220,42 @@ class OpenInterventionForm(EditInterventionForm):
self.disable_form_field(field)
class ShareInterventionForm(BaseModalForm):
url = forms.CharField(
label=_("Share link"),
label_suffix="",
disabled=True,
required=False,
widget=forms.TextInput(
attrs={
"style": "width:100%",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Share")
self.form_caption = _("Send this link to users who you want to have writing access on the data.")
self.template = "modal/modal_form.html"
self.render_submit = False
# Make sure an access_token is set
if self.instance.access_token is None:
self.instance.generate_access_token()
self.share_link = self.request.build_absolute_uri(
reverse("intervention:share", args=(self.instance.id, self.instance.access_token,))
)
self.initialize_form_field(
"url",
self.share_link
)
def save(self):
i = 0
class DummyFilterInput(forms.HiddenInput):
""" A dummy input widget

View File

@@ -13,6 +13,7 @@ from django.utils.timezone import now
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.models import BaseObject, Geometry, UuidModel
from konova.utils import generators
from konova.utils.generators import generate_random_string
from organisation.models import Organisation
from user.models import UserActionLogEntry
@@ -92,6 +93,12 @@ class Intervention(BaseObject):
# Users having access on this object
users = models.ManyToManyField(User)
access_token = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Used for sharing access",
)
def __str__(self):
return "{} ({})".format(self.identifier, self.title)
@@ -139,6 +146,40 @@ class Intervention(BaseObject):
_str = "{}{}{}".format(curr_month, curr_year, rand_str)
return INTERVENTION_IDENTIFIER_TEMPLATE.format(_str)
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)
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):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier

View File

@@ -24,11 +24,9 @@
</button>
</a>
{% if has_access %}
<a href="{% url 'home' %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Share' %}">
{% fa5_icon 'share-alt' %}
</button>
</a>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'intervention:share-create' intervention.id %}">
{% fa5_icon 'share-alt' %}
</button>
<a href="{% url 'home' %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Run check' %}">
{% fa5_icon 'star' %}

View File

@@ -7,7 +7,8 @@ Created on: 30.11.20
"""
from django.urls import path
from intervention.views import index_view, new_view, open_view, edit_view, remove_view, new_document_view
from intervention.views import index_view, new_view, open_view, edit_view, remove_view, new_document_view, share_view, \
create_share_view
app_name = "intervention"
urlpatterns = [
@@ -17,4 +18,6 @@ urlpatterns = [
path('<id>', open_view, name='open'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
]

View File

@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest
from django.shortcuts import render, get_object_or_404
from intervention.forms import NewInterventionForm, EditInterventionForm
from intervention.forms import NewInterventionForm, EditInterventionForm, ShareInterventionForm
from intervention.models import Intervention
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
@@ -198,3 +198,68 @@ def remove_view(request: HttpRequest, id: str):
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def share_view(request: HttpRequest, id: str, token: str):
""" Performs sharing of an intervention
If token given in url is not valid, the user will be redirected to the dashboard
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
token (str): Access token for intervention
Returns:
"""
user = request.user
intervention = get_object_or_404(Intervention, id=id)
# Check tokens
if intervention.access_token == token:
# Send different messages in case user has already been added to list of sharing users
if intervention.has_access(user):
messages.info(
request,
_("{} has already been shared with you").format(intervention.identifier)
)
else:
messages.success(
request,
_("{} has been shared with you").format(intervention.identifier)
)
intervention.users.add(user)
return redirect("intervention:open", id=id)
else:
messages.error(
request,
_("Share link invalid"),
extra_tags="danger",
)
return redirect("home")
@login_required
def create_share_view(request: HttpRequest, id: str):
""" Renders sharing form for an intervention
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
Returns:
"""
user = request.user
intervention = get_object_or_404(Intervention, id=id)
form = ShareInterventionForm(request.POST or None, instance=intervention, request=request)
if request.method != "GET":
raise NotImplementedError
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, form.template, context)