Merge pull request #658 from nofusscomputing/development

This commit is contained in:
Jon
2025-03-01 20:13:14 +09:30
committed by GitHub
23 changed files with 1186 additions and 72 deletions

View File

@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoObjectPermissions
from access.models.tenancy import TenancyObject from access.models.tenancy import Organization, TenancyObject
from core import exceptions as centurion_exceptions from core import exceptions as centurion_exceptions
@ -116,15 +116,27 @@ class OrganizationPermissionMixin(
try: try:
if ( if (
view.model.__name__ == 'UserSettings' (
and request._user.id == int(view.kwargs.get('pk', 0)) view.model.__name__ == 'UserSettings'
and request._user.id == int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id == int(view.kwargs.get('model_id', 0))
)
): ):
return True return True
elif ( elif (
view.model.__name__ == 'UserSettings' (
and request._user.id != int(view.kwargs.get('pk', 0)) view.model.__name__ == 'UserSettings'
and request._user.id != int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id != int(view.kwargs.get('model_id', 0))
)
): ):
@ -271,8 +283,14 @@ class OrganizationPermissionMixin(
if ( if (
view.model.__name__ == 'UserSettings' (
and request._user.id == int(view.kwargs.get('pk', 0)) view.model.__name__ == 'UserSettings'
and request._user.id == int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id == int(view.kwargs.get('model_id', 0))
)
): ):
return True return True

View File

@ -32,7 +32,7 @@ class AuthTokenForm(CommonModelForm):
if self.prefix + '-gen_token' not in self.data: if self.prefix + '-gen_token' not in self.data:
generated_token = self.instance.generate() generated_token = self.instance.generate
else: else:

View File

@ -0,0 +1,17 @@
# Generated by Django 5.1.5 on 2025-02-28 15:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0002_alter_authtoken_expires_alter_authtoken_id_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='authtoken',
options={'ordering': ['expires'], 'verbose_name': 'Auth Token', 'verbose_name_plural': 'Auth Tokens'},
),
]

View File

@ -3,20 +3,34 @@ import random
import string import string
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import Field
from django.forms import ValidationError from django.forms import ValidationError
from access.fields import * from rest_framework.reverse import reverse
from access.models.tenancy import TenancyObject
from access.fields import (
AutoCreatedField,
AutoLastModifiedField
)
class AuthToken(models.Model): class AuthToken(models.Model):
def validate_note_no_token(self, note, token): class Meta:
ordering = [
'expires'
]
verbose_name = 'Auth Token'
verbose_name_plural = 'Auth Tokens'
def validate_note_no_token(self, note, token, raise_exception = True) -> bool:
""" Ensure plaintext token cant be saved to notes field. """ Ensure plaintext token cant be saved to notes field.
called from app.settings.views.user_settings.TokenAdd.form_valid() called from app.settings.views.user_settings.TokenAdd.form_valid()
@ -41,10 +55,12 @@ class AuthToken(models.Model):
validation = False validation = False
if not validation: if not validation and raise_exception:
raise ValidationError('Token can not be placed in the notes field.') raise ValidationError('Token can not be placed in the notes field.')
return validation
id = models.AutoField( id = models.AutoField(
@ -95,6 +111,7 @@ class AuthToken(models.Model):
modified = AutoLastModifiedField() modified = AutoLastModifiedField()
@property
def generate(self) -> str: def generate(self) -> str:
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest()) return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
@ -115,3 +132,33 @@ class AuthToken(models.Model):
def __str__(self): def __str__(self):
return self.token return self.token
table_fields: list = [
'note',
'created',
'expires',
'-action_delete-',
]
def get_url( self, request = None ) -> str:
if request:
return reverse(f"v2:_api_v2_user_settings_token-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"v2:_api_v2_user_settings_token-detail", kwargs = self.get_url_kwargs() )
def get_url_kwargs(self) -> dict:
"""Fetch the URL kwargs
Returns:
dict: kwargs required for generating the URL with `reverse`
"""
return {
'model_id': self.user.id,
'pk': self.id
}

View File

@ -0,0 +1,136 @@
import datetime
import re
from rest_framework import serializers
from access.serializers.organization import OrganizationBaseSerializer
from api.models.tokens import AuthToken
from api.serializers import common
from core import exceptions as centurion_exception
class AuthTokenBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_v2_cluster-detail",
)
class Meta:
model = AuthToken
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
class AuthTokenModelSerializer(
common.CommonModelSerializer,
AuthTokenBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
get_url = super().get_url( item = item )
del get_url['history']
del get_url['knowledge_base']
del get_url['notes']
return get_url
expires = serializers.DateTimeField(
initial = (datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=90)).replace(microsecond=0).isoformat(),
)
token = serializers.CharField(initial=AuthToken().generate, write_only = True )
class Meta:
model = AuthToken
fields = [
'id',
'display_name',
'note',
'token',
'user',
'expires',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'user',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
if self.context['request'].user.id != int(self.context['view'].kwargs['model_id']):
raise centurion_exception.PermissionDenied()
if not self.Meta.model().validate_note_no_token(attrs['note'], attrs['token']):
raise centurion_exception.ValidationError(
detail = {
"note": "No more than nine chars of token can be contained within the notes field"
},
code = 'note_no_contain_token'
)
if not re.fullmatch(r'[0-9|a-f]{64}', str(attrs['token']).lower()):
raise centurion_exception.ValidationError(
detail = {
"token": "Token appears to have been edited."
},
code = 'token_not_sha256'
)
attrs['token'] = self.Meta.model().token_hash(attrs['token'])
attrs = super().validate(attrs)
attrs['user'] = self.context['request'].user
return attrs
class AuthTokenViewSerializer(AuthTokenModelSerializer):
organization = OrganizationBaseSerializer( many = False, read_only = True )

View File

@ -0,0 +1,162 @@
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError, PermissionDenied
from access.models.organization import Organization
from api.serializers.auth_token import AuthToken, AuthTokenModelSerializer
from app.tests.abstract.mock_view import MockView, User
# from core.serializers.manufacturer import Manufacturer, ManufacturerModelSerializer
class ValidationAPI(
TestCase,
):
model = AuthToken
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.user = User.objects.create_user(username="test_user_view", password="password")
self.valid_data = {
'note': 'a note',
'token': self.model().generate,
'user': self.user.id,
'expires': '2025-02-26T00:09Z'
}
self.mock_view = MockView( user = self.user )
self.mock_view.kwargs = {
'model_id': self.user.id
}
self.item = self.model.objects.create(
note = 'object note',
token = self.model().generate,
user = self.user,
expires = '2025-02-26T00:07Z'
)
def test_serializer_validation_valid_data(self):
"""Serializer Validation Check
Ensure that if creating with valid data, the object is created
"""
serializer = AuthTokenModelSerializer(
context = {
'request': self.mock_view.request,
'view': self.mock_view,
},
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_valid_data_different_user(self):
"""Serializer Validation Check
Ensure that if adding the same manufacturer
it raises a validation error
"""
class MockUser:
id = 99
mock_view = MockView( user = self.user )
mock_view.request.user = MockUser()
mock_view.kwargs = {
'model_id': self.user.id
}
with pytest.raises(PermissionDenied) as err:
serializer = AuthTokenModelSerializer(
context = {
'request': mock_view.request,
'view': mock_view,
},
data = self.valid_data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes() == 'permission_denied'
def test_serializer_validation_valid_data_token_not_sha256_same_length(self):
"""Serializer Validation Check
Ensure that if adding the same manufacturer
it raises a validation error
"""
valid_data = self.valid_data.copy()
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwert'
with pytest.raises(ValidationError) as err:
serializer = AuthTokenModelSerializer(
context = {
'request': self.mock_view.request,
'view': self.mock_view,
},
data = valid_data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['token'][0] == 'token_not_sha256'
def test_serializer_validation_valid_data_token_not_sha256_wrong_length(self):
"""Serializer Validation Check
Ensure that if adding the same manufacturer
it raises a validation error
"""
valid_data = self.valid_data.copy()
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwer'
with pytest.raises(ValidationError) as err:
serializer = AuthTokenModelSerializer(
context = {
'request': self.mock_view.request,
'view': self.mock_view,
},
data = valid_data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['token'][0] == 'token_not_sha256'

View File

@ -0,0 +1,382 @@
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.models.tokens import AuthToken
from api.tests.abstract.api_permissions_viewset import (
APIPermissionAdd,
APIPermissionDelete,
APIPermissionView,
)
from api.tests.abstract.api_serializer_viewset import (
SerializerAdd,
SerializerDelete,
SerializerView,
)
from api.tests.abstract.test_metadata_functional import (
MetadataAttributesFunctionalEndpoint,
MetadataAttributesFunctionalBase,
)
class ViewSetBase:
model = AuthToken
app_namespace = 'v2'
url_name = '_api_v2_user_settings_token'
change_data = {'device_model_is_global': True}
delete_data = {}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.item = self.model.objects.create(
note = 'a note',
token = self.model().generate,
user = self.view_user,
expires = '2025-02-25T23:14Z'
)
self.item_delete = self.model.objects.create(
note = 'a note',
token = self.model().generate,
user = self.delete_user,
expires = '2025-02-25T23:14Z'
)
# self.item.default_organization = self.organization
# self.item.save()
self.url_view_kwargs = {
'model_id': self.view_user.id,
'pk': self.item.id,
}
self.url_kwargs = {
'model_id': self.view_user.id,
}
self.add_data = {
'note': 'a note',
'token': self.model().generate,
'expires': '2025-02-26T23:14Z'
}
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
class PermissionsAPI(
ViewSetBase,
APIPermissionAdd,
APIPermissionDelete,
APIPermissionView,
TestCase,
):
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a tenancy model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with permission
"""
url_kwargs = self.url_kwargs.copy()
url_kwargs['model_id'] = self.add_user.id
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.add_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 201
def test_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
url_kwargs = self.url_kwargs.copy()
url_kwargs['model_id'] = self.add_user.id
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.view_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
url_view_kwargs = self.url_view_kwargs.copy()
url_view_kwargs['model_id'] = self.delete_user.id
url_view_kwargs['pk'] = self.item_delete.id
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 204
def test_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
url_view_kwargs = self.url_view_kwargs.copy()
url_view_kwargs['model_id'] = self.delete_user.id
url_view_kwargs['pk'] = self.item_delete.id
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_returned_results_only_user_orgs(self):
"""Test not required
this test is not required as this model is not a tenancy model
"""
pass
def test_view_no_permission_denied(self):
""" Check correct permission for view
This test case is a duplicate of a test case with the same name.
This test is not required for this model as there are no permissions
assosiated with accessing this model.
Attempt to view with user missing permission
"""
pass
class ViewSet(
ViewSetBase,
SerializerAdd,
SerializerDelete,
SerializerView,
TestCase,
):
pass
class Metadata(
ViewSetBase,
MetadataAttributesFunctionalEndpoint,
MetadataAttributesFunctionalBase,
TestCase
):
viewset_type = 'detail'
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.url_kwargs = self.url_view_kwargs

View File

@ -55,7 +55,7 @@ class APIAuthToken(TestCase):
expires=expires expires=expires
) )
self.api_token_valid = token.generate() self.api_token_valid = token.generate
self.hashed_token = token.token_hash(self.api_token_valid) self.hashed_token = token.token_hash(self.api_token_valid)
token.token = self.hashed_token token.token = self.hashed_token
@ -69,7 +69,7 @@ class APIAuthToken(TestCase):
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z') expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
self.api_token_expired = token.generate() self.api_token_expired = token.generate
self.hashed_token_expired = token.token_hash(self.api_token_expired) self.hashed_token_expired = token.token_hash(self.api_token_expired)

View File

@ -0,0 +1,185 @@
import pytest
import unittest
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models.organization import Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.models.tokens import AuthToken
from api.tests.abstract.api_fields import APIModelFields
from core.models.manufacturer import Manufacturer
class API(
TestCase,
APIModelFields
):
model = AuthToken
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. Create an item
"""
self.organization = Organization.objects.create(name='test_org')
self.view_user = User.objects.create_user(username="test_user_view", password="password")
self.item = self.model.objects.create(
note = 'a note',
token = self.model().generate,
user = self.view_user,
expires = '2099-02-26T00:53Z'
)
self.url_view_kwargs = {
'model_id': self.view_user.id,
'pk': self.item.id
}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = self.organization,
)
view_team.permissions.set([view_permissions])
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
client = Client()
url = reverse('v2:_api_v2_user_settings_token-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
self.api_data = response.data
def test_api_field_exists_model_notes(self):
""" Test for existance of API Field
This test case is a duplicate of a test with the same name. This
model is not a tenancy model and does not require this field.
model_notes field must exist
"""
assert 'model_notes' not in self.api_data
def test_api_field_type_model_notes(self):
""" Test for type for API Field
This test case is a duplicate of a test with the same name. This
model is not a tenancy model and does not require this field.
model_notes field must be str
"""
assert 'model_notes' not in self.api_data
def test_api_field_exists_user(self):
""" Test for existance of API Field
user field must exist
"""
assert 'user' in self.api_data
def test_api_field_type_user(self):
""" Test for type for API Field
normallay an object would be serialized. However in this case only
the owner of the object will access the object and therefore would
be a waste to serialize it.
user field must be int
"""
assert type(self.api_data['user']) is int
def test_api_field_exists_note(self):
""" Test for existance of API Field
note field must exist
"""
assert 'note' in self.api_data
def test_api_field_type_user(self):
""" Test for type for API Field
note field must be str
"""
assert type(self.api_data['note']) is str
def test_api_field_exists_expires(self):
""" Test for existance of API Field
expires field must exist
"""
assert 'expires' in self.api_data
def test_api_field_type_expires(self):
""" Test for type for API Field
expires field must be str
"""
assert type(self.api_data['expires']) is str
# def test_api_field_exists_url_history(self):
# """ Test for existance of API Field
# _urls.history field must exist
# """
# assert 'history' in self.api_data['_urls']
# def test_api_field_type_url_history(self):
# """ Test for type for API Field
# _urls.history field must be str
# """
# assert type(self.api_data['_urls']['history']) is str

View File

@ -0,0 +1,70 @@
import pytest
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.reverse import reverse
from access.models.organization import Organization
from api.tests.abstract.viewsets import ViewSetModel
from settings.viewsets.user_settings import ViewSet
class ViewsetCommon(
ViewSetModel,
):
viewset = ViewSet
route_name = 'v2:_api_v2_user_settings_token'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
self.kwargs = {
'model_id': self.view_user.id
}
class ViewsetList(
ViewsetCommon,
TestCase,
):
@classmethod
def setUpTestData(self):
"""Setup Test
1. make list request
"""
super().setUpTestData()
client = Client()
url = reverse(
self.route_name + '-list',
kwargs = self.kwargs
)
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)

View File

@ -5,6 +5,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from api.viewsets import ( from api.viewsets import (
auth_token,
index as v2 index as v2
) )
@ -224,6 +225,7 @@ router.register('settings/software_category/(?P<model_id>[0-9]+)/notes', softwar
router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category') router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category')
router.register('settings/ticket_comment_category', ticket_comment_category.ViewSet, basename='_api_v2_ticket_comment_category') router.register('settings/ticket_comment_category', ticket_comment_category.ViewSet, basename='_api_v2_ticket_comment_category')
router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings')
router.register('settings/user_settings/(?P<model_id>[0-9]+)/token', auth_token.ViewSet, basename='_api_v2_user_settings_token')
urlpatterns = [ urlpatterns = [

View File

@ -0,0 +1,119 @@
from rest_framework.reverse import reverse
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from api.viewsets.common import (
ModelCreateViewSet,
ModelListRetrieveDeleteViewSet,
)
from api.serializers.auth_token import ( # pylint: disable=W0611:unused-import
AuthToken,
AuthTokenModelSerializer,
AuthTokenViewSerializer,
)
@extend_schema_view(
create=extend_schema(
summary = 'Create an auth token',
description='',
responses = {
201: OpenApiResponse(description='Device created', response=AuthTokenViewSerializer),
400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing create permissions'),
}
),
destroy = extend_schema(
summary = 'Delete an auth token',
description = '',
responses = {
204: OpenApiResponse(description=''),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all auth tokens',
description='',
responses = {
200: OpenApiResponse(description='', response=AuthTokenViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single auth token',
description='',
responses = {
200: OpenApiResponse(description='', response=AuthTokenViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(exclude = True),
)
class ViewSet(
ModelCreateViewSet,
ModelListRetrieveDeleteViewSet,
):
filterset_fields = [
'expires',
]
search_fields = [
'note',
]
model = AuthToken
view_description = 'User Authentication Tokens'
def get_serializer_class(self):
if self.serializer_class is not None:
return self.serializer_class
if (
self.action == 'list'
or self.action == 'retrieve'
):
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer']
else:
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer']
return self.serializer_class
def get_back_url(self) -> str:
if self.back_url is None:
self.back_url = self.get_return_url()
return self.back_url
def get_return_url(self) -> str:
if getattr(self, '_get_return_url', None):
return self._get_return_url
self._get_return_url = reverse(
'v2:_api_v2_user_settings-detail',
kwargs = {
'pk': self.kwargs['model_id']
},
request = self.request,
)
return self._get_return_url

View File

@ -96,6 +96,28 @@ class UserSettings(UserSettingsCommonFields):
verbose_name = 'Your Timezone', verbose_name = 'Your Timezone',
) )
page_layout: list = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "single",
"fields": [
'browser_mode',
'default_organization',
'timezone',
],
},
{
"name": "Auth Tokens",
"layout": "table",
"field": "tokens",
}
]
},
]
def get_organization(self): def get_organization(self):

View File

@ -48,6 +48,13 @@ class UserSettingsModelSerializer(UserSettingsBaseSerializer):
return { return {
'_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), '_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
'tokens': reverse(
"v2:_api_v2_user_settings_token-list",
request=self._context['view'].request,
kwargs={
'model_id': item.user.pk
}
)
} }

View File

@ -127,6 +127,7 @@ RUN pip --disable-pip-version-check list --outdated --format=json | \
python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \ python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \
xargs -n1 pip install --no-cache-dir -U; \ xargs -n1 pip install --no-cache-dir -U; \
apk update --no-cache; \ apk update --no-cache; \
apk upgrade --no-cache; \
apk add --no-cache \ apk add --no-cache \
mariadb-client \ mariadb-client \
mariadb-dev \ mariadb-dev \

View File

@ -30,14 +30,6 @@ Models are tested using the following test cases:
- [View Permission (organization Manager)](./model_permission_view_organization_manager.md) - [View Permission (organization Manager)](./model_permission_view_organization_manager.md)
- [History Entry](./model_history.md)
- [History Entry (Child Item)](./model_history_child_item.md)
- [History Entry (Parent Item)](./model_history_parent_item.md)
- [History Entry Permissions](./model_history_permissions.md)
- [Model Views](./model_views.md) - [Model Views](./model_views.md)
- [Tenancy Objects](./model_tenancy_object.md) - [Tenancy Objects](./model_tenancy_object.md)

View File

@ -1,9 +0,0 @@
---
title: History Entry Test Cases
description: No Fuss Computings model history entry unit test cases
date: 2024-06-15
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
::: app.core.tests.abstract.history_entry.HistoryEntry

View File

@ -1,9 +0,0 @@
---
title: Child Item History Entry Test Cases
description: No Fuss Computings model child item history entry unit test cases
date: 2024-06-15
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
::: app.core.tests.abstract.history_entry_child_model.HistoryEntryChildItem

View File

@ -1,9 +0,0 @@
---
title: Parent Item History Entry Test Cases
description: No Fuss Computings model parent item history entry unit test cases
date: 2024-06-15
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
::: app.core.tests.abstract.history_entry_parent_model.HistoryEntryParentItem

View File

@ -1,11 +0,0 @@
---
title: History Entry Permission Test Cases
description: No Fuss Computings model history entry permission unit test cases
date: 2024-06-16
template: project.html
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
---
::: app.core.tests.abstract.history_permissions.HistoryPermissions
options:
show_source: true

View File

@ -128,7 +128,7 @@ Items to test include, and are not limited to:
- can access global object (still to require model CRUD permission) - can access global object (still to require model CRUD permission)
- history - [History Entries](./api/tests/model_history.md), [History Permissions](./api/tests/model_history_permissions.md) - history
- saves history with parent pk and parent class - saves history with parent pk and parent class

View File

@ -109,14 +109,6 @@ nav:
- projects/centurion_erp/development/api/tests/models.md - projects/centurion_erp/development/api/tests/models.md
- projects/centurion_erp/development/api/tests/model_history.md
- projects/centurion_erp/development/api/tests/model_history_child_item.md
- projects/centurion_erp/development/api/tests/model_history_parent_item.md
- projects/centurion_erp/development/api/tests/model_history_permissions.md
- projects/centurion_erp/development/api/tests/model_permissions.md - projects/centurion_erp/development/api/tests/model_permissions.md
- projects/centurion_erp/development/api/tests/model_permissions_organization_manager.md - projects/centurion_erp/development/api/tests/model_permissions_organization_manager.md