19_Tests #40
@ -21,6 +21,8 @@ urlpatterns = [
 | 
			
		||||
    path('<id>/state/new', state_new_view, name='acc-new-state'),
 | 
			
		||||
    path('<id>/action/new', action_new_view, name='acc-new-action'),
 | 
			
		||||
    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
 | 
			
		||||
    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 intervention.models import Intervention, ResponsibilityData, LegalData
 | 
			
		||||
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 user.models import UserActionLogEntry
 | 
			
		||||
 | 
			
		||||
@ -311,28 +311,11 @@ class CompensationDocument(AbstractDocument):
 | 
			
		||||
                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
 | 
			
		||||
    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(
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,9 @@
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% 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 obj.recorded %}
 | 
			
		||||
                <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.models import EcoAccount, EcoAccountDocument
 | 
			
		||||
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.decorators import any_group_check, default_group_required, conservation_office_group_required
 | 
			
		||||
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm
 | 
			
		||||
@ -511,3 +511,63 @@ def report_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 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 ema.managers import EmaManager
 | 
			
		||||
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 user.models import UserActionLogEntry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Ema(AbstractCompensation, RecordableMixin):
 | 
			
		||||
class Ema(AbstractCompensation, ShareableObject, RecordableObject):
 | 
			
		||||
    """
 | 
			
		||||
    EMA = Ersatzzahlungsmaßnahme
 | 
			
		||||
    (compensation actions from payments)
 | 
			
		||||
@ -28,23 +27,6 @@ class Ema(AbstractCompensation, RecordableMixin):
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,9 @@
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% 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 obj.recorded %}
 | 
			
		||||
                <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>/action/new', action_new_view, name='new-action'),
 | 
			
		||||
    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
 | 
			
		||||
    # 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 ema.forms import NewEmaForm, EditEmaForm
 | 
			
		||||
from ema.tables import EmaTable
 | 
			
		||||
from intervention.forms.modalForms import ShareInterventionModalForm
 | 
			
		||||
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 konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm
 | 
			
		||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 | 
			
		||||
@ -460,4 +461,64 @@ def report_view(request:HttpRequest, id: str):
 | 
			
		||||
        "actions": actions,
 | 
			
		||||
    }
 | 
			
		||||
    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
 | 
			
		||||
        url_name = f"{self.instance._meta.app_label}:share"
 | 
			
		||||
        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(
 | 
			
		||||
            "url",
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVA
 | 
			
		||||
from intervention.managers import InterventionManager
 | 
			
		||||
from intervention.utils.quality import InterventionQualityChecker
 | 
			
		||||
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.utils import generators
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Intervention(BaseObject, RecordableMixin, CheckableMixin):
 | 
			
		||||
class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject):
 | 
			
		||||
    """
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    # 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()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        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):
 | 
			
		||||
        """ 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 intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
 | 
			
		||||
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
 | 
			
		||||
from konova.utils import generators
 | 
			
		||||
from konova.utils.generators import generate_random_string
 | 
			
		||||
from user.models import UserActionLogEntry, UserAction
 | 
			
		||||
 | 
			
		||||
@ -315,12 +316,23 @@ class Geometry(BaseResource):
 | 
			
		||||
    geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecordableMixin:
 | 
			
		||||
    """ Mixin to be combined with BaseObject class
 | 
			
		||||
 | 
			
		||||
    Provides functionality related to un/recording of data
 | 
			
		||||
class RecordableObject(models.Model):
 | 
			
		||||
    """ Wraps record related fields and functionality
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    # 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):
 | 
			
		||||
        """ Perform unrecording
 | 
			
		||||
 | 
			
		||||
@ -370,12 +382,19 @@ class RecordableMixin:
 | 
			
		||||
            self.set_unrecorded(user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckableMixin:
 | 
			
		||||
    """ Mixin to be combined with BaseObject class
 | 
			
		||||
class CheckableObject(models.Model):
 | 
			
		||||
    # 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):
 | 
			
		||||
        """ Perform unrecording
 | 
			
		||||
 | 
			
		||||
@ -417,3 +436,53 @@ class CheckableMixin:
 | 
			
		||||
            self.set_checked(user)
 | 
			
		||||
        else:
 | 
			
		||||
            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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user