Compare commits
3 Commits
25cccee5d6
...
8f40162974
Author | SHA1 | Date | |
---|---|---|---|
8f40162974 | |||
b86202ba98 | |||
b13e67e061 |
@ -5,4 +5,4 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 21.01.22
|
Created on: 21.01.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken"
|
KSP_TOKEN_HEADER_IDENTIFIER = "Ksptoken"
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
7
api/tests/v1/__init__.py
Normal file
7
api/tests/v1/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||||
|
Created on: 27.01.22
|
||||||
|
|
||||||
|
"""
|
150
api/tests/v1/test_api_sharing.py
Normal file
150
api/tests/v1/test_api_sharing.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from konova.settings import DEFAULT_GROUP
|
||||||
|
from konova.tests.test_views import BaseTestCase
|
||||||
|
from konova.utils.user_checks import is_default_group_only
|
||||||
|
|
||||||
|
|
||||||
|
class APIV1SharingTestCase(BaseTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
""" Creates a test file in memory for using in further tests
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().setUpTestData()
|
||||||
|
cls.superuser.get_API_token()
|
||||||
|
cls.superuser.api_token.is_active = True
|
||||||
|
cls.superuser.api_token.save()
|
||||||
|
default_group = cls.groups.get(name=DEFAULT_GROUP)
|
||||||
|
cls.superuser.groups.add(default_group)
|
||||||
|
|
||||||
|
cls.header_data = {
|
||||||
|
"HTTP_ksptoken": cls.superuser.api_token.token,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run_share_request(self, url, user_list: list):
|
||||||
|
data = {
|
||||||
|
"users": user_list
|
||||||
|
}
|
||||||
|
data = json.dumps(data)
|
||||||
|
response = self.client.put(
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
**self.header_data
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _test_api_sharing(self, obj, url):
|
||||||
|
""" Generic test for testing sharing of a ShareableObjectMixin object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (ShareableObjectMixin): The object
|
||||||
|
url (str): The url to be used for a request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.assertEqual(obj.users.count(), 0)
|
||||||
|
user_list = [
|
||||||
|
self.superuser.username,
|
||||||
|
self.user.username,
|
||||||
|
]
|
||||||
|
|
||||||
|
response = self._run_share_request(url, user_list)
|
||||||
|
|
||||||
|
# Must fail, since performing user has no access on requested object
|
||||||
|
self.assertEqual(response.status_code, 500)
|
||||||
|
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
|
||||||
|
|
||||||
|
# Add performing user to shared access users and rerun the request
|
||||||
|
obj.users.add(self.superuser)
|
||||||
|
response = self._run_share_request(url, user_list)
|
||||||
|
|
||||||
|
shared_users = obj.shared_users
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(shared_users.count(), 2)
|
||||||
|
self.assertIn(self.superuser, shared_users)
|
||||||
|
self.assertIn(self.user, shared_users)
|
||||||
|
|
||||||
|
def test_api_token_invalid(self):
|
||||||
|
""" Tests that a request with an invalid token won't be successfull
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||||
|
# Expect the first request to work properly
|
||||||
|
self.intervention.users.add(self.superuser)
|
||||||
|
response = self._run_share_request(share_url, [self.superuser.username])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Change the token
|
||||||
|
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
|
||||||
|
|
||||||
|
# Expect the request to fail now
|
||||||
|
response = self._run_share_request(share_url, [self.superuser.username])
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_api_intervention_sharing(self):
|
||||||
|
""" Tests proper sharing of intervention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||||
|
self._test_api_sharing(self.intervention, share_url)
|
||||||
|
|
||||||
|
def test_api_eco_account_sharing(self):
|
||||||
|
""" Tests proper sharing of eco account
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
share_url = reverse("api:v1:ecoaccount-share", args=(self.eco_account.id,))
|
||||||
|
self._test_api_sharing(self.eco_account, share_url)
|
||||||
|
|
||||||
|
def test_api_ema_sharing(self):
|
||||||
|
""" Tests proper sharing of ema
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
share_url = reverse("api:v1:ema-share", args=(self.ema.id,))
|
||||||
|
self._test_api_sharing(self.ema, share_url)
|
||||||
|
|
||||||
|
def test_api_sharing_as_default_group_only(self):
|
||||||
|
""" Tests that sharing using the API as an only default group user works as expected.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
Default only user can only add new users, having shared access. Removing them from the list of users
|
||||||
|
having shared access is only possible if the user has further rights, e.g. being part of a registration
|
||||||
|
or conservation office group.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||||
|
|
||||||
|
# Give the user only default group rights
|
||||||
|
default_group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
|
self.superuser.groups.set([default_group])
|
||||||
|
self.assertTrue(is_default_group_only(self.superuser))
|
||||||
|
|
||||||
|
# Add only him as shared_users an object
|
||||||
|
self.intervention.users.set([self.superuser])
|
||||||
|
self.assertEqual(self.intervention.users.count(), 1)
|
||||||
|
|
||||||
|
# Try to add another user via API -> must work!
|
||||||
|
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(self.intervention.users.count(), 2)
|
||||||
|
|
||||||
|
# Now try to remove the user again -> expect no changes at all to the shared user list
|
||||||
|
response = self._run_share_request(share_url, [self.superuser.username])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(self.intervention.users.count(), 2)
|
@ -7,8 +7,11 @@ Created on: 21.01.22
|
|||||||
"""
|
"""
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from api.views.method_views import generate_new_token_view
|
||||||
|
|
||||||
app_name = "api"
|
app_name = "api"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v1/", include("api.urls.v1.urls", namespace="v1")),
|
path("v1/", include("api.urls.v1.urls", namespace="v1")),
|
||||||
|
path("token/generate", generate_new_token_view, name="generate-new-token"),
|
||||||
]
|
]
|
35
api/views/method_views.py
Normal file
35
api/views/method_views.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||||
|
Created on: 27.01.22
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpRequest, JsonResponse
|
||||||
|
|
||||||
|
from api.models import APIUserToken
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def generate_new_token_view(request: HttpRequest):
|
||||||
|
""" Handles request for fetching
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
token = APIUserToken()
|
||||||
|
while APIUserToken.objects.filter(token=token.token).exists():
|
||||||
|
token = APIUserToken()
|
||||||
|
return JsonResponse(
|
||||||
|
data={
|
||||||
|
"gen_data": token.token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
@ -18,6 +18,7 @@ from compensation.models import EcoAccount
|
|||||||
from ema.models import Ema
|
from ema.models import Ema
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.utils.message_templates import DATA_UNSHARED
|
from konova.utils.message_templates import DATA_UNSHARED
|
||||||
|
from konova.utils.user_checks import is_default_group_only
|
||||||
from user.models import User
|
from user.models import User
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ class AbstractAPIView(View):
|
|||||||
try:
|
try:
|
||||||
# Fetch the proper user from the given request header token
|
# Fetch the proper user from the given request header token
|
||||||
self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None))
|
self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None))
|
||||||
|
if not self.user.is_default_user():
|
||||||
|
raise PermissionError("Default permissions required")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
return self.return_error_response(e, 403)
|
return self.return_error_response(e, 403)
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -240,6 +243,15 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
new_users_objs = []
|
new_users_objs = []
|
||||||
for user in new_users:
|
for user in new_users:
|
||||||
new_users_objs.append(User.objects.get(username=user))
|
new_users_objs.append(User.objects.get(username=user))
|
||||||
|
|
||||||
|
if is_default_group_only(self.user):
|
||||||
|
# Default only users are not allowed to remove other users from having access. They can only add new ones!
|
||||||
|
new_users_to_be_added = User.objects.filter(
|
||||||
|
username__in=new_users
|
||||||
|
).exclude(
|
||||||
|
id__in=obj.shared_users
|
||||||
|
)
|
||||||
|
new_users_objs = obj.shared_users.union(new_users_to_be_added)
|
||||||
obj.share_with_list(new_users_objs)
|
obj.share_with_list(new_users_objs)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -253,4 +265,4 @@ class EcoAccountAPIShareView(AbstractModelShareAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class EmaAPIShareView(AbstractModelShareAPIView):
|
class EmaAPIShareView(AbstractModelShareAPIView):
|
||||||
model = Ema
|
model = Ema
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp_intervention.generate_new_identifier()
|
identifier = tmp_intervention.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -213,6 +213,7 @@ EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
|
|||||||
DEFAULT_FROM_EMAIL = "service@ksp.de" # The default email address for the 'from' element
|
DEFAULT_FROM_EMAIL = "service@ksp.de" # The default email address for the 'from' element
|
||||||
EMAIL_HOST = "localhost"
|
EMAIL_HOST = "localhost"
|
||||||
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
||||||
|
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
|
||||||
EMAIL_PORT = "25"
|
EMAIL_PORT = "25"
|
||||||
#EMAIL_HOST_USER = ""
|
#EMAIL_HOST_USER = ""
|
||||||
#EMAIL_HOST_PASSWORD = ""
|
#EMAIL_HOST_PASSWORD = ""
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{% load i18n fontawesome_5 %}
|
{% load i18n fontawesome_5 %}
|
||||||
|
|
||||||
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
|
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
|
||||||
<input id="gen-id-input" aria-describedby="gen-id-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
|
<input id="gen-data-input" aria-describedby="gen-data-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
|
||||||
<div class="input-group-append" onclick="fetchNewIdentifier()">
|
<div class="input-group-append" onclick="fetchNewIdentifier()">
|
||||||
<span id="gen-id-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
|
<span id="gen-data-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function(data){
|
.then(function(data){
|
||||||
document.getElementById("gen-id-input").value = data["identifier"];
|
document.getElementById("gen-data-input").value = data["gen_data"];
|
||||||
})
|
})
|
||||||
.catch(function(error){
|
.catch(function(error){
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -7,6 +7,7 @@ Created on: 26.10.21
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from ema.models import Ema
|
||||||
from user.models import User
|
from user.models import User
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||||
@ -52,6 +53,7 @@ class BaseTestCase(TestCase):
|
|||||||
cls.intervention = cls.create_dummy_intervention()
|
cls.intervention = cls.create_dummy_intervention()
|
||||||
cls.compensation = cls.create_dummy_compensation()
|
cls.compensation = cls.create_dummy_compensation()
|
||||||
cls.eco_account = cls.create_dummy_eco_account()
|
cls.eco_account = cls.create_dummy_eco_account()
|
||||||
|
cls.ema = cls.create_dummy_ema()
|
||||||
cls.create_dummy_states()
|
cls.create_dummy_states()
|
||||||
cls.create_dummy_action()
|
cls.create_dummy_action()
|
||||||
cls.codes = cls.create_dummy_codes()
|
cls.codes = cls.create_dummy_codes()
|
||||||
@ -168,6 +170,30 @@ class BaseTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
return eco_account
|
return eco_account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_dummy_ema(cls):
|
||||||
|
""" Creates an ema which can be used for tests
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create dummy data
|
||||||
|
# Create log entry
|
||||||
|
action = UserActionLogEntry.get_created_action(cls.superuser)
|
||||||
|
geometry = Geometry.objects.create()
|
||||||
|
# Create responsible data object
|
||||||
|
responsible_data = Responsibility.objects.create()
|
||||||
|
# Finally create main object, holding the other objects
|
||||||
|
ema = Ema.objects.create(
|
||||||
|
identifier="TEST",
|
||||||
|
title="Test_title",
|
||||||
|
responsible=responsible_data,
|
||||||
|
created=action,
|
||||||
|
geometry=geometry,
|
||||||
|
comment="Test",
|
||||||
|
)
|
||||||
|
return ema
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_dummy_states(cls):
|
def create_dummy_states(cls):
|
||||||
""" Creates an intervention which can be used for tests
|
""" Creates an intervention which can be used for tests
|
||||||
|
@ -11,7 +11,7 @@ from django.core.mail import send_mail
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO
|
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO, SUPPORT_MAIL_RECIPIENT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -181,3 +181,24 @@ class Mailer:
|
|||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_mail_verify_api_token(self, user):
|
||||||
|
""" Send a mail if a user creates a new token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): The user, having a new api token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"user": user,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/api/verify_token.html", context)
|
||||||
|
user_mail_address = [SUPPORT_MAIL_RECIPIENT]
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("Request for new API token"),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
Binary file not shown.
@ -20,13 +20,13 @@
|
|||||||
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
|
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
|
||||||
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
|
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
|
||||||
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
|
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
|
||||||
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:38
|
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:42
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-01-20 12:30+0100\n"
|
"POT-Creation-Date: 2022-01-27 11:44+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -317,6 +317,7 @@ msgid "Identifier"
|
|||||||
msgstr "Kennung"
|
msgstr "Kennung"
|
||||||
|
|
||||||
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
|
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
|
||||||
|
#: user/forms.py:126
|
||||||
msgid "Generated automatically"
|
msgid "Generated automatically"
|
||||||
msgstr "Automatisch generiert"
|
msgstr "Automatisch generiert"
|
||||||
|
|
||||||
@ -1716,11 +1717,11 @@ msgstr "Kontrolle am"
|
|||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr "Sonstige"
|
msgstr "Sonstige"
|
||||||
|
|
||||||
#: konova/sub_settings/django_settings.py:154
|
#: konova/sub_settings/django_settings.py:155
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: konova/sub_settings/django_settings.py:155
|
#: konova/sub_settings/django_settings.py:156
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1803,6 +1804,10 @@ msgstr "{} - Freigegebene Daten gelöscht"
|
|||||||
msgid "{} - Shared data checked"
|
msgid "{} - Shared data checked"
|
||||||
msgstr "{} - Freigegebene Daten geprüft"
|
msgstr "{} - Freigegebene Daten geprüft"
|
||||||
|
|
||||||
|
#: konova/utils/mailer.py:201 templates/email/api/verify_token.html:4
|
||||||
|
msgid "Request for new API token"
|
||||||
|
msgstr "Anfrage für neuen API Token"
|
||||||
|
|
||||||
#: konova/utils/message_templates.py:11
|
#: konova/utils/message_templates.py:11
|
||||||
msgid "There was an error on this form."
|
msgid "There was an error on this form."
|
||||||
msgstr "Es gab einen Fehler im Formular."
|
msgstr "Es gab einen Fehler im Formular."
|
||||||
@ -1937,6 +1942,32 @@ msgstr ""
|
|||||||
msgid "Something happened. We are working on it!"
|
msgid "Something happened. We are working on it!"
|
||||||
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
|
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:7
|
||||||
|
msgid "Hello support"
|
||||||
|
msgstr "Hallo Support"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:9
|
||||||
|
msgid "you need to verify the API token for user"
|
||||||
|
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:13
|
||||||
|
msgid ""
|
||||||
|
"If unsure, please contact the user. The API token can not be used until you "
|
||||||
|
"activated it in the admin backend."
|
||||||
|
msgstr ""
|
||||||
|
"Falls Sie sich unsicher sind, kontaktieren Sie den Nutzer vorher. Der API Token kann so lange nicht verwendet werden, "
|
||||||
|
"wie er noch nicht von Ihnen im Admin Backend aktiviert worden ist."
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:16
|
||||||
|
#: templates/email/checking/shared_data_checked.html:17
|
||||||
|
#: templates/email/deleting/shared_data_deleted.html:17
|
||||||
|
#: templates/email/recording/shared_data_recorded.html:17
|
||||||
|
#: templates/email/recording/shared_data_unrecorded.html:17
|
||||||
|
#: templates/email/sharing/shared_access_given.html:18
|
||||||
|
#: templates/email/sharing/shared_access_removed.html:18
|
||||||
|
msgid "Best regards"
|
||||||
|
msgstr "Beste Grüße"
|
||||||
|
|
||||||
#: templates/email/checking/shared_data_checked.html:4
|
#: templates/email/checking/shared_data_checked.html:4
|
||||||
msgid "Shared data checked"
|
msgid "Shared data checked"
|
||||||
msgstr "Freigegebene Daten geprüft"
|
msgstr "Freigegebene Daten geprüft"
|
||||||
@ -1962,15 +1993,6 @@ msgstr ""
|
|||||||
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
|
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
|
||||||
"Datensatzes soeben bestätigt hat."
|
"Datensatzes soeben bestätigt hat."
|
||||||
|
|
||||||
#: templates/email/checking/shared_data_checked.html:17
|
|
||||||
#: templates/email/deleting/shared_data_deleted.html:17
|
|
||||||
#: templates/email/recording/shared_data_recorded.html:17
|
|
||||||
#: templates/email/recording/shared_data_unrecorded.html:17
|
|
||||||
#: templates/email/sharing/shared_access_given.html:18
|
|
||||||
#: templates/email/sharing/shared_access_removed.html:18
|
|
||||||
msgid "Best regards"
|
|
||||||
msgstr "Beste Grüße"
|
|
||||||
|
|
||||||
#: templates/email/deleting/shared_data_deleted.html:4
|
#: templates/email/deleting/shared_data_deleted.html:4
|
||||||
msgid "Shared data deleted"
|
msgid "Shared data deleted"
|
||||||
msgstr "Freigegebene Daten gelöscht"
|
msgstr "Freigegebene Daten gelöscht"
|
||||||
@ -2214,39 +2236,51 @@ msgid ""
|
|||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
" Diese Daten sind noch nicht veröffentlicht und "
|
" Diese Daten sind noch nicht veröffentlicht und können daher "
|
||||||
"können daher aktuell nicht eingesehen werden. Schauen Sie zu einem späteren "
|
"aktuell nicht eingesehen werden. Schauen Sie zu einem späteren Zeitpunkt "
|
||||||
"Zeitpunkt wieder vorbei. \n"
|
"wieder vorbei. \n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
#: user/forms.py:23
|
#: user/forms.py:27
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
#: user/forms.py:25
|
#: user/forms.py:29
|
||||||
msgid "Select the situations when you want to receive a notification"
|
msgid "Select the situations when you want to receive a notification"
|
||||||
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
|
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
|
||||||
|
|
||||||
#: user/forms.py:37
|
#: user/forms.py:41
|
||||||
msgid "Edit notifications"
|
msgid "Edit notifications"
|
||||||
msgstr "Benachrichtigungen bearbeiten"
|
msgstr "Benachrichtigungen bearbeiten"
|
||||||
|
|
||||||
#: user/forms.py:72 user/templates/user/index.html:9
|
#: user/forms.py:76 user/templates/user/index.html:9
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr "Nutzername"
|
msgstr "Nutzername"
|
||||||
|
|
||||||
#: user/forms.py:83
|
#: user/forms.py:87
|
||||||
msgid "Person name"
|
msgid "Person name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
#: user/forms.py:94 user/templates/user/index.html:17
|
#: user/forms.py:98 user/templates/user/index.html:17
|
||||||
msgid "E-Mail"
|
msgid "E-Mail"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: user/forms.py:108
|
#: user/forms.py:112
|
||||||
msgid "User contact data"
|
msgid "User contact data"
|
||||||
msgstr "Kontaktdaten"
|
msgstr "Kontaktdaten"
|
||||||
|
|
||||||
|
#: user/forms.py:122
|
||||||
|
msgid "Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: user/forms.py:137
|
||||||
|
msgid "Create new token"
|
||||||
|
msgstr "Neuen Token generieren"
|
||||||
|
|
||||||
|
#: user/forms.py:138
|
||||||
|
msgid "A new token needs to be validated by an administrator!"
|
||||||
|
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
|
||||||
|
|
||||||
#: user/models/user_action.py:20
|
#: user/models/user_action.py:20
|
||||||
msgid "Unrecorded"
|
msgid "Unrecorded"
|
||||||
msgstr "Entzeichnet"
|
msgstr "Entzeichnet"
|
||||||
@ -2300,18 +2334,58 @@ msgstr "Benachrichtigungseinstellungen ändern"
|
|||||||
msgid "Notification settings"
|
msgid "Notification settings"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
#: user/views.py:29
|
#: user/templates/user/index.html:58
|
||||||
|
msgid "See or edit your API token"
|
||||||
|
msgstr "API token einsehen oder neu generieren"
|
||||||
|
|
||||||
|
#: user/templates/user/index.html:61
|
||||||
|
msgid "API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:6
|
||||||
|
msgid "API settings"
|
||||||
|
msgstr "API Einstellungen"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:10
|
||||||
|
msgid "Current token"
|
||||||
|
msgstr "Aktueller Token"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:14
|
||||||
|
msgid "Authenticated by admins"
|
||||||
|
msgstr "Von Admin freigeschaltet"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:16
|
||||||
|
msgid "Token has been verified and can be used"
|
||||||
|
msgstr "Token wurde freigeschaltet und kann verwendet werden"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:18
|
||||||
|
msgid "Token waiting for verification"
|
||||||
|
msgstr "Token noch nicht freigeschaltet"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:22
|
||||||
|
msgid "Valid until"
|
||||||
|
msgstr "Läuft ab am"
|
||||||
|
|
||||||
|
#: user/views.py:31
|
||||||
msgid "User settings"
|
msgid "User settings"
|
||||||
msgstr "Einstellungen"
|
msgstr "Einstellungen"
|
||||||
|
|
||||||
#: user/views.py:55
|
#: user/views.py:57
|
||||||
msgid "Notifications edited"
|
msgid "Notifications edited"
|
||||||
msgstr "Benachrichtigungen bearbeitet"
|
msgstr "Benachrichtigungen bearbeitet"
|
||||||
|
|
||||||
#: user/views.py:67
|
#: user/views.py:69
|
||||||
msgid "User notifications"
|
msgid "User notifications"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
|
#: user/views.py:92
|
||||||
|
msgid "New token generated. Administrators need to validate."
|
||||||
|
msgstr "Neuer Token generiert. Administratoren sind informiert."
|
||||||
|
|
||||||
|
#: user/views.py:103
|
||||||
|
msgid "User API token"
|
||||||
|
msgstr "API Nutzer Token"
|
||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
|
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
|
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
|
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
|
||||||
@ -3056,7 +3130,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54
|
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54
|
||||||
msgid "This field is required."
|
msgid "This field is required."
|
||||||
msgstr ""
|
msgstr "Pflichtfeld"
|
||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
|
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
|
||||||
msgid "Enter a whole number."
|
msgid "Enter a whole number."
|
||||||
|
23
templates/email/api/verify_token.html
Normal file
23
templates/email/api/verify_token.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Request for new API token' %}</h2>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello support' %},
|
||||||
|
<br>
|
||||||
|
{% trans 'you need to verify the API token for user' %}:
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<strong>{{user.username}}</strong>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'If unsure, please contact the user. The API token can not be used until you activated it in the admin backend.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
@ -6,8 +6,12 @@ Created on: 08.07.21
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.urls import reverse
|
from django.db import IntegrityError
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from api.models import APIUserToken
|
||||||
|
from intervention.inputs import GenerateInput
|
||||||
from user.models import User
|
from user.models import User
|
||||||
|
|
||||||
from konova.forms import BaseForm, BaseModalForm
|
from konova.forms import BaseForm, BaseModalForm
|
||||||
@ -113,3 +117,46 @@ class UserContactForm(BaseModalForm):
|
|||||||
self.initialize_form_field("mail", self.instance.email)
|
self.initialize_form_field("mail", self.instance.email)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAPITokenForm(BaseForm):
|
||||||
|
token = forms.CharField(
|
||||||
|
label=_("Token"),
|
||||||
|
label_suffix="",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
help_text=_("Generated automatically"),
|
||||||
|
widget=GenerateInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"url": reverse_lazy("api:generate-new-token"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_title = _("Create new token")
|
||||||
|
self.form_caption = _("A new token needs to be validated by an administrator!")
|
||||||
|
|
||||||
|
self.action_url = reverse("user:api-token")
|
||||||
|
self.cancel_redirect = reverse("user:index")
|
||||||
|
|
||||||
|
# Make direct token editing by user impossible. Instead set the proper url for generating a new token
|
||||||
|
self.initialize_form_field("token", None)
|
||||||
|
self.fields["token"].widget.attrs["readonly"] = True
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
""" Saves the form data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
api_token (APIUserToken)
|
||||||
|
"""
|
||||||
|
user = self.instance
|
||||||
|
new_token = self.cleaned_data["token"]
|
||||||
|
if user.api_token is not None:
|
||||||
|
user.api_token.delete()
|
||||||
|
new_token = APIUserToken.objects.create(
|
||||||
|
token=new_token
|
||||||
|
)
|
||||||
|
user.api_token = new_token
|
||||||
|
user.save()
|
||||||
|
return new_token
|
||||||
|
@ -9,6 +9,7 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from api.models import APIUserToken
|
||||||
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
|
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
|
||||||
from konova.utils.mailer import Mailer
|
from konova.utils.mailer import Mailer
|
||||||
from user.enums import UserNotificationEnum
|
from user.enums import UserNotificationEnum
|
||||||
@ -142,3 +143,19 @@ class User(AbstractUser):
|
|||||||
if notification_set:
|
if notification_set:
|
||||||
mailer = Mailer()
|
mailer = Mailer()
|
||||||
mailer.send_mail_shared_data_checked(obj_identifier, self)
|
mailer.send_mail_shared_data_checked(obj_identifier, self)
|
||||||
|
|
||||||
|
def get_API_token(self):
|
||||||
|
""" Getter for an API token
|
||||||
|
|
||||||
|
Creates a new one if none exists, yet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
token (APIUserToken)
|
||||||
|
"""
|
||||||
|
if self.api_token is None:
|
||||||
|
token = APIUserToken.objects.create()
|
||||||
|
self.api_token = token
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
token = self.api_token
|
||||||
|
return token
|
||||||
|
@ -54,6 +54,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
|
||||||
|
<button class="btn btn-default">
|
||||||
|
{% fa5_icon 'code' %}
|
||||||
|
<span>{% trans 'API' %}</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
33
user/templates/user/token.html
Normal file
33
user/templates/user/token.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n fontawesome_5 %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
<h3>{% trans 'API settings' %}</h3>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans 'Current token' %}</th>
|
||||||
|
<td>{{ user.api_token.token }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans 'Authenticated by admins' %}</th>
|
||||||
|
{% if user.api_token is None %}
|
||||||
|
<td></td>
|
||||||
|
{% elif user.api_token.is_active %}
|
||||||
|
<td class="text-success" title="{% trans 'Token has been verified and can be used' %}">{% fa5_icon 'check-circle' %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="text-primary" title="{% trans 'Token waiting for verification' %}">{% fa5_icon 'hourglass-half' %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans 'Valid until' %}</th>
|
||||||
|
<td>{{ user.api_token.valid_until|default_if_none:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
{% include 'form/table/generic_table_form.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -13,6 +13,7 @@ app_name = "user"
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index_view, name="index"),
|
path("", index_view, name="index"),
|
||||||
path("notifications/", notifications_view, name="notifications"),
|
path("notifications/", notifications_view, name="notifications"),
|
||||||
|
path("token/api", api_token_view, name="api-token"),
|
||||||
path("contact/<id>", contact_view, name="contact"),
|
path("contact/<id>", contact_view, name="contact"),
|
||||||
|
|
||||||
]
|
]
|
@ -2,14 +2,16 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.mailer import Mailer
|
||||||
|
from konova.utils.message_templates import FORM_INVALID
|
||||||
from user.models import User
|
from user.models import User
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import any_group_check
|
from konova.decorators import any_group_check, default_group_required
|
||||||
from user.forms import UserNotificationForm, UserContactForm
|
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -70,6 +72,40 @@ def notifications_view(request: HttpRequest):
|
|||||||
return render(request, template, context)
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@default_group_required
|
||||||
|
def api_token_view(request: HttpRequest):
|
||||||
|
""" Handles the request for user api frontend settings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
template = "user/token.html"
|
||||||
|
user = request.user
|
||||||
|
form = UserAPITokenForm(request.POST or None, instance=user)
|
||||||
|
if request.method == "POST":
|
||||||
|
if form.is_valid():
|
||||||
|
token = form.save()
|
||||||
|
messages.info(request, _("New token generated. Administrators need to validate."))
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_verify_api_token(user)
|
||||||
|
return redirect("user:api-token")
|
||||||
|
else:
|
||||||
|
messages.error(request, FORM_INVALID, extra_tags="danger")
|
||||||
|
elif request.method != "GET":
|
||||||
|
raise NotImplementedError
|
||||||
|
context = {
|
||||||
|
"user": user,
|
||||||
|
"form": form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("User API token"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def contact_view(request: HttpRequest, id: str):
|
def contact_view(request: HttpRequest, id: str):
|
||||||
""" Renders contact modal view of a users contact data
|
""" Renders contact modal view of a users contact data
|
||||||
|
Loading…
Reference in New Issue
Block a user