Merge pull request #344 from nofusscomputing/feature-v1-3

This commit is contained in:
Jon
2024-11-29 01:13:43 +09:30
committed by GitHub
515 changed files with 58210 additions and 991 deletions

View File

@ -32,6 +32,8 @@
- [ ] :checkered_flag: Milestone assigned
- [ ] :gear: :test_tube: [Functional Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_

17
.vscode/launch.json vendored
View File

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug: Django",
"name": "Centurion",
"type": "debugpy",
"request": "launch",
"args": [
@ -17,6 +17,7 @@
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Gunicorn",
"type": "debugpy",
"request": "launch",
@ -35,9 +36,21 @@
"autoStartBrowser": false,
"cwd": "${workspaceFolder}/app"
},
{
"name": "Migrate",
"type": "debugpy",
"request": "launch",
"args": [
"migrate"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Celery",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",

View File

@ -17,4 +17,5 @@
"ITSM"
],
"cSpell.language": "en-AU",
"jest.enable": false,
}

View File

@ -68,6 +68,14 @@ Included within the root of the repository is a makefile that can be used during
> this doc is yet to receive a re-write
## Docker Container
within the `deploy/` directory there is a docker compose file. running `docker compose up` from this directory will launch a full stack deployment locally containing Centurion API, User Interface, a worker and a RabbitMQ server. once launched you can navigate to `http://127.0.0.1/` to start browsing the site.
You may need to run migrations if your not mounting your own DB. to do this run `docker exec -ti centurion-erp python manage.py migrate`
# Old working docs

View File

@ -1,3 +1,27 @@
## Version 1.4.0
API redesign in preparation for moving the UI out of centurion to it's [own project](https://github.com/nofusscomputing/centurion_erp_ui). This release introduces a **Feature freeze** to the current UI. Only bug fixes will be done for the current UI.
API v2 is a beta release and is subject to change. On completion of the new UI, API v2 will more likely than not be set as stable.
- A large emphasis is being placed upon API stability. This is being achieved by ensuring the following:
- Actions can only be carried out by users whom have the correct permissions
- fields are of the correct type and visible when required as part of the API response
- Data validations work and notify the user of any issue
We are make the above possible by ensuring a more stringent test policy.
- New API will be at path `api/v2`.
- API v1 is now **Feature frozen** with only bug fixes being completed. It's recommended that you move to and start using API v2 as this has feature parity with API v1.
- API v1 is **depreciated**
- Depreciation of **ALL** API urls. API v1 Will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion.
# Version 1.3.0
!!! danger "Security"
@ -14,10 +38,12 @@ This release updates the docker container to be a production setup for deploymen
- To setup container as "Worker", set `IS_WORKER='True'` environmental variable within container. _**Note:** You can still use command `celery -A app worker -l INFO`, although **not** recommended as the container health check will not be functioning_
# Version 1.0.0
## Version 1.0.0
Initial Release of Centurion ERP.
## Breaking changes
### Breaking changes
- Nil

View File

@ -11,12 +11,20 @@ class AutoCreatedField(models.DateTimeField):
"""
help_text = 'Date and time of creation'
verbose_name = 'Created'
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("default", now)
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
@ -28,6 +36,18 @@ class AutoLastModifiedField(AutoCreatedField):
"""
help_text = 'Date and time of last modification'
verbose_name = 'Modified'
def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = now()
@ -45,6 +65,20 @@ class AutoSlugField(models.SlugField):
"""
help_text = 'slug for this field'
verbose_name = 'Slug'
def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
if not model_instance.slug or model_instance.slug == '_':

View File

@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2024-10-13 06:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='team',
options={'ordering': ['team_name'], 'verbose_name': 'Team', 'verbose_name_plural': 'Teams'},
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 5.1.2 on 2024-10-13 15:27
import access.models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_team_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='organization',
name='id',
field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'),
),
migrations.AlterField(
model_name='organization',
name='manager',
field=models.ForeignKey(help_text='Manager for this organization', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Manager'),
),
migrations.AlterField(
model_name='organization',
name='model_notes',
field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='organization',
name='name',
field=models.CharField(help_text='Name of this Organization', max_length=50, unique=True, verbose_name='Name'),
),
migrations.AlterField(
model_name='team',
name='is_global',
field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'),
),
migrations.AlterField(
model_name='team',
name='model_notes',
field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'),
),
migrations.AlterField(
model_name='team',
name='team_name',
field=models.CharField(default='', help_text='Name to give this team', max_length=50, verbose_name='Name'),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 5.1.2 on 2024-10-16 06:54
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_organization_id_alter_organization_manager_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='organization',
options={'ordering': ['name'], 'verbose_name': 'Organization', 'verbose_name_plural': 'Organizations'},
),
migrations.AlterModelOptions(
name='teamusers',
options={'ordering': ['user'], 'verbose_name': 'Team User', 'verbose_name_plural': 'Team Users'},
),
migrations.AlterField(
model_name='teamusers',
name='id',
field=models.AutoField(help_text='ID of this Team User', primary_key=True, serialize=False, unique=True, verbose_name='ID'),
),
migrations.AlterField(
model_name='teamusers',
name='manager',
field=models.BooleanField(blank=True, default=False, help_text='Is this user to be a manager of this team', verbose_name='manager'),
),
migrations.AlterField(
model_name='teamusers',
name='team',
field=models.ForeignKey(help_text='Team user belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='team', to='access.team', verbose_name='Team'),
),
migrations.AlterField(
model_name='teamusers',
name='user',
field=models.ForeignKey(help_text='User who will be added to the team', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-07 06:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0004_alter_organization_options_alter_teamusers_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='team',
name='team_name',
field=models.CharField(help_text='Name to give this team', max_length=50, verbose_name='Name'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2024-11-20 02:41
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0005_alter_team_team_name'),
]
operations = [
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'),
),
]

View File

@ -48,7 +48,7 @@ class OrganizationMixin():
if hasattr(self, '_object_organization'):
return self._object_organization
return int(self._object_organization)
try:
@ -124,9 +124,13 @@ class OrganizationMixin():
is_member = False
if organization in self.user_organizations():
if organization is None:
return True
return False
if int(organization) in self.user_organizations():
is_member = True
return is_member
@ -136,6 +140,10 @@ class OrganizationMixin():
Override of 'PermissionRequiredMixin' method so that this mixin can obtain the required permission.
"""
if not hasattr(self, 'permission_required'):
return []
if self.permission_required is None:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the "
@ -216,6 +224,11 @@ class OrganizationMixin():
organization = self.object_organization()
else:
organization = int(organization)
if self.is_member(organization) or organization == 0:
groups = Group.objects.filter(pk__in=self.user_groups)

View File

@ -3,6 +3,8 @@ from django.db import models
from django.contrib.auth.models import User, Group, Permission
from django.forms import ValidationError
from rest_framework.reverse import reverse
from .fields import *
from core.middleware.get_request import get_request
@ -12,6 +14,7 @@ from core.mixin.history_save import SaveHistory
class Organization(SaveHistory):
class Meta:
verbose_name = "Organization"
verbose_name_plural = "Organizations"
ordering = ['name']
@ -23,28 +26,34 @@ class Organization(SaveHistory):
super().save(*args, **kwargs)
id = models.AutoField(
blank=False,
help_text = 'ID of this item',
primary_key=True,
unique=True,
blank=False
verbose_name = 'ID'
)
name = models.CharField(
blank = False,
help_text = 'Name of this Organization',
max_length = 50,
unique = True,
verbose_name = 'Name'
)
manager = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank = False,
help_text = 'Manager for this organization',
null = True,
help_text = 'Organization Manager'
on_delete=models.SET_NULL,
verbose_name = 'Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
help_text = 'Tid bits of information',
null= True,
verbose_name = 'Notes',
)
@ -62,6 +71,59 @@ class Organization(SaveHistory):
def __str__(self):
return self.name
table_fields: list = [
'nbsp',
'name',
'created',
'modified',
'nbsp'
]
page_layout: list = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'manager',
'created',
'modified',
],
"right": [
'model_notes',
]
}
]
},
{
"name": "Teams",
"slug": "teams",
"sections": [
{
"layout": "table",
"field": "teams"
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
}
]
def get_url( self, request = None ) -> str:
if request:
return reverse("v2:_api_v2_organization-detail", request=request, kwargs={'pk': self.id})
return reverse("v2:_api_v2_organization-detail", kwargs={'pk': self.id})
class TenancyManager(models.Manager):
@ -176,30 +238,77 @@ class TenancyObject(SaveHistory):
raise ValidationError('You must provide an organization')
id = models.AutoField(
blank=False,
help_text = 'ID of the item',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
blank = False,
null = True,
help_text = 'Organization this belongs to',
null = False,
on_delete = models.CASCADE,
validators = [validatate_organization_exists],
verbose_name = 'Organization'
)
is_global = models.BooleanField(
blank = False,
default = False,
blank = False
help_text = 'Is this a global object?',
verbose_name = 'Global Object'
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
help_text = 'Tid bits of information',
null = True,
verbose_name = 'Notes',
)
def get_organization(self) -> Organization:
return self.organization
def get_url( self, request = None ) -> str:
"""Fetch the models URL
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
Args:
request (object, optional): The request object that was made by the end user. Defaults to None.
Returns:
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
"""
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
if request:
return reverse(f"v2:_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"v2:_api_v2_{model_name}-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 {
'pk': self.id
}
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.organization is None:
@ -211,10 +320,14 @@ class TenancyObject(SaveHistory):
class Team(Group, TenancyObject):
class Meta:
# proxy = True
ordering = [ 'team_name' ]
verbose_name = 'Team'
verbose_name_plural = "Teams"
ordering = ['team_name']
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -225,17 +338,79 @@ class Team(Group, TenancyObject):
team_name = models.CharField(
verbose_name = 'Name',
blank = False,
help_text = 'Name to give this team',
max_length = 50,
unique = False,
default = ''
verbose_name = 'Name',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
page_layout: dict = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'team_name',
'created',
'modified',
],
"right": [
'model_notes',
]
},
{
"layout": "table",
"name": "Users",
"field": "user",
},
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
'team_name',
'modified',
'created',
]
def get_url( self, request = None ) -> str:
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
if request:
return reverse(f"v2:_api_v2_organization_team-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"v2:_api_v2_organization_team-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 {
'organization_id': self.organization.id,
'pk': self.id
}
@property
def parent_object(self):
@ -266,36 +441,56 @@ class Team(Group, TenancyObject):
class TeamUsers(SaveHistory):
class Meta:
# proxy = True
verbose_name_plural = "Team Users"
ordering = ['user']
verbose_name = "Team User"
verbose_name_plural = "Team Users"
id = models.AutoField(
blank=False,
help_text = 'ID of this Team User',
primary_key=True,
unique=True,
blank=False
verbose_name = 'ID'
)
team = models.ForeignKey(
Team,
blank = False,
help_text = 'Team user belongs to',
null = False,
on_delete=models.CASCADE,
related_name="team",
on_delete=models.CASCADE)
verbose_name = 'Team'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
blank = False,
help_text = 'User who will be added to the team',
null = False,
on_delete=models.CASCADE,
verbose_name = 'User'
)
manager = models.BooleanField(
verbose_name='manager',
blank=True,
default=False,
blank=True
help_text = 'Is this user to be a manager of this team',
verbose_name='manager',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
page_layout: list = []
table_fields: list = []
def delete(self, using=None, keep_parents=False):
""" Delete Team
@ -317,6 +512,24 @@ class TeamUsers(SaveHistory):
return self.team.organization
def get_url( self, request = None ) -> str:
url_kwargs: dict = {
'organization_id': self.team.organization.id,
'team_id': self.team.id,
'pk': self.id
}
print(f'url kwargs are: {url_kwargs}')
if request:
return reverse(f"v2:_api_v2_organization_team_user-detail", request=request, kwargs = url_kwargs )
return reverse(f"v2:_api_v2_organization_team_user-detail", kwargs = url_kwargs )
def save(self, *args, **kwargs):
""" Save Team

View File

@ -0,0 +1,89 @@
from rest_framework.reverse import reverse
from rest_framework import serializers
from access.models import Organization
from api.serializers import common
from app.serializers.user import UserBaseSerializer
class OrganizationBaseSerializer(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_organization-detail", format="html"
)
class Meta:
model = Organization
fields = [
'id',
'display_name',
'name',
'url',
]
read_only_fields = [
'id',
'display_name',
'name',
'url',
]
class OrganizationModelSerializer(
common.CommonModelSerializer,
OrganizationBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
'teams': reverse("v2:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}),
}
class Meta:
model = Organization
fields = '__all__'
fields = [
'id',
'display_name',
'name',
'model_notes',
'manager',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
class OrganizationViewSerializer(OrganizationModelSerializer):
pass
manager = UserBaseSerializer(many=False, read_only = True)

View File

@ -0,0 +1,99 @@
from rest_framework.reverse import reverse
from rest_framework import serializers
from access.models import TeamUsers
from api.serializers import common
from app.serializers.user import UserBaseSerializer
class TeamUserBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = TeamUsers
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
class TeamUserModelSerializer(
common.CommonModelSerializer,
TeamUserBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request )
}
class Meta:
model = TeamUsers
fields = [
'id',
'display_name',
'manager',
'user',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
def is_valid(self, *, raise_exception=True) -> bool:
is_valid = False
is_valid = super().is_valid(raise_exception=raise_exception)
self.validated_data['team_id'] = int(self._context['view'].kwargs['team_id'])
return is_valid
class TeamUserViewSerializer(TeamUserModelSerializer):
user = UserBaseSerializer(read_only = True)

View File

@ -0,0 +1,126 @@
from rest_framework.reverse import reverse
from rest_framework import serializers
from access.models import Team
from api.serializers import common
from access.serializers.organization import OrganizationBaseSerializer
from app.serializers.permission import PermissionBaseSerializer
from core import fields as centurion_field
class TeamBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = Team
fields = [
'id',
'display_name',
'team_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'team_name',
'url',
]
class TeamModelSerializer(
common.CommonModelSerializer,
TeamBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
'users': reverse(
'v2:_api_v2_organization_team_user-list',
request=self.context['view'].request,
kwargs={
'organization_id': item.organization.id,
'team_id': item.pk
}
)
}
team_name = centurion_field.CharField( autolink = True )
class Meta:
model = Team
fields = '__all__'
fields = [
'id',
'display_name',
'team_name',
'model_notes',
'permissions',
'organization',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'name',
'organization',
'created',
'modified',
'_urls',
]
def is_valid(self, *, raise_exception=True) -> bool:
is_valid = False
is_valid = super().is_valid(raise_exception=raise_exception)
self.validated_data['organization_id'] = int(self._context['view'].kwargs['organization_id'])
return is_valid
class TeamViewSerializer(TeamModelSerializer):
organization = OrganizationBaseSerializer(many=False, read_only=True)
permissions = PermissionBaseSerializer(many = True)

View File

@ -1,14 +1,14 @@
import pytest
# from django.contrib.auth.models import User
from django.contrib.auth.models import User
from django.test import TestCase
# from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import ValidationError
# from access.serializers.organization import (
# Organization,
# OrganizationModelSerializer
# )
from access.serializers.organization import (
Organization,
OrganizationModelSerializer
)
@ -16,8 +16,77 @@ class OrganizationValidationAPI(
TestCase,
):
model = Organization
@pytest.mark.skip( reason = 'tests to be written' )
def test_dummy(self):
@classmethod
def setUpTestData(self):
"""Setup Test
pass
1. Create an org
2. Create an item
"""
self.user = User.objects.create(username = 'org_user', password='random password')
self.valid_data = {
'name': 'valid_org_data',
'manager': self.user.id
}
self.item = self.model.objects.create(
name = 'random title',
)
def test_serializer_valid_data(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
serializer = OrganizationModelSerializer(
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_name(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
data = self.valid_data.copy()
del data['name']
with pytest.raises(ValidationError) as err:
serializer = OrganizationModelSerializer(
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['name'][0] == 'required'
def test_serializer_validation_manager_optional(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
data = self.valid_data.copy()
del data['manager']
serializer = OrganizationModelSerializer(
data = data
)
assert serializer.is_valid(raise_exception = True)

View File

@ -0,0 +1,280 @@
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
class ViewSetBase:
model = Organization
app_namespace = 'v2'
url_name = '_api_v2_organization'
change_data = {'name': 'device'}
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
self.item = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.other_org_item = 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_b = Team.objects.create(
team_name = 'view_team',
organization = different_organization,
)
view_team.permissions.set([view_permissions])
view_team_b.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.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.view_user_b = User.objects.create_user(username="test_user_view_b", password="password")
teamuser = TeamUsers.objects.create(
team = view_team_b,
user = self.view_user_b
)
self.url_view_kwargs = { 'pk': self.item.id }
self.add_data = {
'name': 'team_post',
}
self.super_add_user = User.objects.create_user(username="test_user_add_super", password="password", is_superuser = True)
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.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 OrganizationPermissionsAPI(
ViewSetBase,
APIPermissions,
TestCase
):
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
if self.url_kwargs:
url = reverse( self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs )
else:
url = reverse( self.app_namespace + ':' + self.url_name + '-list' )
client.force_login( self.super_add_user )
response = client.post( url, data = self.add_data )
assert response.status_code == 201
def test_returned_results_only_user_orgs(self):
"""Returned results check
This test case is an override of a test of the same name.
organizations are not tenancy objects and therefor are supposed to
return all items when a user queries them.
Ensure that a query to the viewset endpoint does not return
items that are not part of the users organizations.
"""
# Ensure the other org item exists, without test not able to function
print('Check that the different organization item has been defined')
assert hasattr(self, 'other_org_item')
# ensure that the variables for the two orgs are different orgs
print('checking that the different and user oganizations are different')
assert self.different_organization.id != self.organization.id
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.view_user)
response = client.get(url)
contains_different_org: bool = False
# for item in response.data['results']:
# if int(item['id']) != self.organization.id:
# contains_different_org = True
assert len(response.data['results']) == 2
class OrganizationViewSet(
ViewSetBase,
SerializersTestCases,
TestCase
):
pass

View File

@ -0,0 +1,201 @@
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
class ViewSetBase:
model = Team
app_namespace = 'API'
url_name = '_api_v2_organization_team'
change_data = {'name': 'device'}
delete_data = {'device': 'device'}
@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
self.item = self.model.objects.create(
organization=organization,
name = 'teamone'
)
self.other_org_item = self.model.objects.create(
organization=different_organization,
name = 'teamtwo'
)
self.url_kwargs = {'organization_id': self.organization.id}
self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.add_data = {'team_name': 'team_post'}
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.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
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.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 TeamPermissionsAPI(
ViewSetBase,
APIPermissions,
TestCase,
):
pass
class TeamViewSet(
ViewSetBase,
SerializersTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,180 @@
import pytest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.models import Organization, Permission
from access.serializers.teams import (
Team,
TeamModelSerializer
)
class MockView:
action: str = None
kwargs: dict = {}
class MockRequest:
user = None
class TeamValidationAPI(
TestCase,
):
model = Team
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
self.organization = Organization.objects.create(
name = 'team org serializer test'
)
self.user = User.objects.create(username = 'org_user', password='random password')
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,
)
)
self.valid_data = {
'organization': self.organization.id,
'team_name': 'valid_org_data',
'permissions': [
view_permissions.id,
]
}
self.item = self.model.objects.create(
organization = self.organization,
name = 'random team title',
)
def test_serializer_valid_data(self):
"""Serializer Validation Check
Ensure that if creating an item supplied valid data
creates an item.
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
serializer = TeamModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_name(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
data = self.valid_data.copy()
del data['team_name']
with pytest.raises(ValidationError) as err:
serializer = TeamModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['team_name'][0] == 'required'
def test_serializer_validation_permissions_optional(self):
"""Serializer Validation Check
Ensure that if creating and permissions are not supplied, the item is
still created.
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
data = self.valid_data.copy()
del data['permissions']
serializer = TeamModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = data
)
assert serializer.is_valid(raise_exception = True)

View File

@ -0,0 +1,211 @@
import pytest
import unittest
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
class ViewSetBase:
model = TeamUsers
app_namespace = 'API'
url_name = '_api_v2_organization_team_user'
change_data = {'name': 'device'}
delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and 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_b = Team.objects.create(
team_name = 'view_team',
organization = different_organization,
)
view_team.permissions.set([view_permissions])
view_team_b.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.view_user = User.objects.create_user(username="test_user_view", password="password")
self.view_user_b = User.objects.create_user(username="test_user_view_b", password="password")
self.item = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.other_org_item = TeamUsers.objects.create(
team = view_team_b,
user = self.view_user_b
)
self.url_view_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id, 'pk': self.item.id}
self.url_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id}
random_user = User.objects.create_user(username="random_user", password="password")
self.add_data = {'user': random_user.id}
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.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 TeamUserPermissionsAPI(
ViewSetBase,
APIPermissions,
TestCase
):
def test_returned_results_only_user_orgs(self):
"""This test is not applicable for team_user as users are not tenancy objects
"""
pass
class TeamUserViewSet(
ViewSetBase,
SerializersTestCases,
TestCase
):
pass

View File

@ -0,0 +1,186 @@
import pytest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.models import Organization, Permission, Team
from access.serializers.team_user import (
TeamUsers,
TeamUserModelSerializer
)
class MockView:
action: str = None
kwargs: dict = {}
class MockRequest:
user = None
class TeamValidationAPI(
TestCase,
):
model = TeamUsers
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
self.organization = Organization.objects.create(
name = 'team org serializer test'
)
self.user = User.objects.create(username = 'org_user', password='random password')
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,
)
)
self.team = Team.objects.create(
organization = self.organization,
name = 'random team title',
)
self.valid_data = {
'team': self.team.id,
'user': self.user.id
}
self.item = self.model.objects.create(
team = self.team,
user = self.user,
)
def test_serializer_valid_data(self):
"""Serializer Validation Check
Ensure that if creating an item supplied valid data
creates an item.
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id,
'team_id': self.team.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
serializer = TeamUserModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_team_creates(self):
"""Serializer Validation Check
Ensure that if creating and no team is provided no validation
error occurs as the team id is collected from the view
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id,
'team_id': self.team.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
data = self.valid_data.copy()
del data['team']
serializer = TeamUserModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_user(self):
"""Serializer Validation Check
Ensure that if creating and no user is provided a validation error occurs
"""
mock_view = MockView()
mock_view.action = 'create'
mock_view.kwargs: dict = {
'organization_id': self.organization.id,
'team_id': self.team.id
}
mock_request = MockRequest()
mock_request.user = self.user
mock_view.request = mock_request
data = self.valid_data.copy()
del data['user']
with pytest.raises(ValidationError) as err:
serializer = TeamUserModelSerializer(
context = {
'request': mock_request,
'view': mock_view,
},
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['user'][0] == 'required'

View File

@ -17,7 +17,7 @@ class OrganizationAPI(TestCase):
model = Organization
app_namespace = 'API'
app_namespace = 'v1'
url_name = '_api_organization'

View File

@ -0,0 +1,192 @@
import pytest
import unittest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_fields import APICommonFields
class OrganizationAPI(
TestCase,
APICommonFields
):
model = Organization
app_namespace = 'v2'
url_name = '_api_v2_organization'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create the object
2. create view user
3. add user as org manager
4. make api request
"""
organization = Organization.objects.create(name='test_org', model_notes='random text')
self.organization = organization
self.item = organization
self.url_view_kwargs = {'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 = organization,
)
view_team.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
organization.manager = self.view_user
organization.save()
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-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_name(self):
""" Test for existance of API Field
name field must exist
"""
assert 'name' in self.api_data
def test_api_field_type_name(self):
""" Test for type for API Field
name field must be str
"""
assert type(self.api_data['name']) is str
def test_api_field_exists_manager(self):
""" Test for existance of API Field
manager field must exist
"""
assert 'manager' in self.api_data
def test_api_field_type_manager(self):
""" Test for type for API Field
manager field must be dict
"""
assert type(self.api_data['manager']) is dict
def test_api_field_exists_manager_id(self):
""" Test for existance of API Field
manager.id field must exist
"""
assert 'id' in self.api_data['manager']
def test_api_field_type_manager_id(self):
""" Test for type for API Field
manager.id field must be int
"""
assert type(self.api_data['manager']['id']) is int
def test_api_field_exists_manager_display_name(self):
""" Test for existance of API Field
manager.display_name field must exist
"""
assert 'display_name' in self.api_data['manager']
def test_api_field_type_manager_display_name(self):
""" Test for type for API Field
manager.display_name field must be int
"""
assert type(self.api_data['manager']['display_name']) is str
def test_api_field_exists_manager_url(self):
""" Test for existance of API Field
manager.display_name field must exist
"""
assert 'url' in self.api_data['manager']
def test_api_field_type_manager_url(self):
""" Test for type for API Field
manager.url field must be Hyperlink
"""
assert type(self.api_data['manager']['url']) is Hyperlink
def test_api_field_exists_url_teams(self):
""" Test for existance of API Field
_urls.teams field must exist
"""
assert 'teams' in self.api_data['_urls']
def test_api_field_type_url_teams(self):
""" Test for type for API Field
_urls.teams field must be Hyperlink
"""
assert type(self.api_data['_urls']['teams']) is str

View File

@ -20,7 +20,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
model_name = 'organization'
app_label = 'access'
app_namespace = 'API'
app_namespace = 'v1'
url_name = '_api_organization'

View File

@ -34,7 +34,7 @@ class OrganizationHistory(TestCase):
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
action = int(History.Actions.ADD),
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
@ -44,7 +44,7 @@ class OrganizationHistory(TestCase):
self.item_change.save()
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
action = int(History.Actions.UPDATE),
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@ -72,7 +72,7 @@ class OrganizationHistory(TestCase):
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
assert history['action'] == int(History.Actions.ADD)
# assert type(history['action']) is int
@ -125,7 +125,7 @@ class OrganizationHistory(TestCase):
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
assert history['action'] == int(History.Actions.UPDATE)
# assert type(history['action']) is int

View File

@ -67,4 +67,74 @@ class TeamModel(
@pytest.mark.skip(reason="uses Django group manager")
def test_model_class_tenancy_manager_function_get_queryset_called(self):
pass
pass
def test_model_fields_parameter_not_empty_help_text(self):
"""Test Field called with Parameter
This is a custom test of a test derived of the samae name. It's required
as the team model extends the Group model.
During field creation, paramater `help_text` must not be `None` or empty ('')
"""
group_mode_fields_to_ignore: list = [
'id',
'name',
'group_ptr_id'
]
fields_have_test_value: bool = True
for field in self.model._meta.fields:
if field.attname in group_mode_fields_to_ignore:
continue
print(f'Checking field {field.attname} is not empty')
if (
field.help_text is None
or field.help_text == ''
):
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value
def test_model_fields_parameter_type_verbose_name(self):
"""Test Field called with Parameter
This is a custom test of a test derived of the samae name. It's required
as the team model extends the Group model.
During field creation, paramater `verbose_name` must be of type str
"""
group_mode_fields_to_ignore: list = [
'name',
]
fields_have_test_value: bool = True
for field in self.model._meta.fields:
if field.attname in group_mode_fields_to_ignore:
continue
print(f'Checking field {field.attname} is of type str')
if not type(field.verbose_name) is str:
print(f' Failure on field {field.attname}')
fields_have_test_value = False
assert fields_have_test_value

View File

@ -21,7 +21,7 @@ class TeamAPI(TestCase):
model = Team
app_namespace = 'API'
app_namespace = 'v1'
url_name = '_api_team'

View File

@ -0,0 +1,174 @@
import pytest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_fields import APITenancyObject
class TeamAPI(
TestCase,
APITenancyObject
):
model = Team
app_namespace = 'v2'
url_name = '_api_v2_organization_team'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create the object
2. create view user
3. add user as org manager
4. make api request
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=organization,
team_name = 'teamone',
model_notes = 'random note'
)
self.url_view_kwargs = {'organization_id': self.organization.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,
)
)
self.item.permissions.set([view_permissions])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = self.item,
user = self.view_user
)
organization.manager = self.view_user
organization.save()
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-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_team_name(self):
""" Test for existance of API Field
team_name field must exist
"""
assert 'team_name' in self.api_data
def test_api_field_type_team_name(self):
""" Test for type for API Field
team_name field must be str
"""
assert type(self.api_data['team_name']) is str
def test_api_field_exists_permissions(self):
""" Test for existance of API Field
permissions field must exist
"""
assert 'permissions' in self.api_data
def test_api_field_type_permissions(self):
""" Test for type for API Field
url field must be list
"""
assert type(self.api_data['permissions']) is list
def test_api_field_exists_permissions_id(self):
""" Test for existance of API Field
permissions.id field must exist
"""
assert 'id' in self.api_data['permissions'][0]
def test_api_field_type_permissions_id(self):
""" Test for type for API Field
permissions.id field must be int
"""
assert type(self.api_data['permissions'][0]['id']) is int
def test_api_field_exists_permissions_display_name(self):
""" Test for existance of API Field
permissions.display_name field must exist
"""
assert 'display_name' in self.api_data['permissions'][0]
def test_api_field_type_permissions_display_name(self):
""" Test for type for API Field
permissions.display_name field must be str
"""
assert type(self.api_data['permissions'][0]['display_name']) is str
def test_api_field_exists_permissions_url(self):
""" Test for existance of API Field
permissions.url field must exist
"""
assert 'url' in self.api_data['permissions'][0]
def test_api_field_type_permissions_url(self):
""" Test for type for API Field
permissions.url field must be str
"""
assert type(self.api_data['permissions'][0]['url']) is Hyperlink

View File

@ -39,7 +39,7 @@ class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
action = int(History.Actions.ADD),
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
@ -51,7 +51,7 @@ class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
action = int(History.Actions.UPDATE),
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@ -68,7 +68,7 @@ class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
action = int(History.Actions.DELETE),
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)

View File

@ -18,7 +18,7 @@ class TeamPermissionsAPI(TestCase, APIPermissions):
model = Team
app_namespace = 'API'
app_namespace = 'v1'
url_name = '_api_team'

View File

@ -6,9 +6,13 @@ from django.contrib.auth.models import User
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.models import BaseModel
class TeamUsersModel(TestCase):
class TeamUsersModel(
TestCase,
BaseModel
):
model = TeamUsers

View File

@ -0,0 +1,214 @@
import pytest
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import reverse
from django.test import Client, TestCase
from rest_framework.relations import Hyperlink
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_fields import APICommonFields
class TeamUserAPI(
TestCase,
APICommonFields
):
model = TeamUsers
app_namespace = 'v2'
url_name = '_api_v2_organization_team_user'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create the object
2. create view user
3. add user as org manager
4. make api request
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_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])
self.view_user = User.objects.create_user(username="test_user_view", password="password")
self.item = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.url_view_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id, 'pk': self.item.id}
self.url_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id}
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-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_manager(self):
""" Test for existance of API Field
manager field must exist
"""
assert 'manager' in self.api_data
def test_api_field_type_manager(self):
""" Test for type for API Field
manager field must be bool
"""
assert type(self.api_data['manager']) is bool
def test_api_field_exists_created(self):
""" Test for existance of API Field
created field must exist
"""
assert 'created' in self.api_data
def test_api_field_type_created(self):
""" Test for type for API Field
created field must be str
"""
assert type(self.api_data['created']) is str
def test_api_field_exists_modified(self):
""" Test for existance of API Field
modified field must exist
"""
assert 'modified' in self.api_data
def test_api_field_type_modified(self):
""" Test for type for API Field
modified field must be str
"""
assert type(self.api_data['modified']) is str
# def test_api_field_exists_permissions(self):
# """ Test for existance of API Field
# permissions field must exist
# """
# assert 'permissions' in self.api_data
# def test_api_field_type_permissions(self):
# """ Test for type for API Field
# url field must be list
# """
# assert type(self.api_data['permissions']) is list
# def test_api_field_exists_permissions_id(self):
# """ Test for existance of API Field
# permissions.id field must exist
# """
# assert 'id' in self.api_data['permissions'][0]
# def test_api_field_type_permissions_id(self):
# """ Test for type for API Field
# permissions.id field must be int
# """
# assert type(self.api_data['permissions'][0]['id']) is int
# def test_api_field_exists_permissions_display_name(self):
# """ Test for existance of API Field
# permissions.display_name field must exist
# """
# assert 'display_name' in self.api_data['permissions'][0]
# def test_api_field_type_permissions_display_name(self):
# """ Test for type for API Field
# permissions.display_name field must be str
# """
# assert type(self.api_data['permissions'][0]['display_name']) is str
# def test_api_field_exists_permissions_url(self):
# """ Test for existance of API Field
# permissions.url field must exist
# """
# assert 'url' in self.api_data['permissions'][0]
# def test_api_field_type_permissions_url(self):
# """ Test for type for API Field
# permissions.url field must be str
# """
# assert type(self.api_data['permissions'][0]['url']) is Hyperlink

View File

@ -48,7 +48,7 @@ class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
action = int(History.Actions.ADD),
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
@ -60,7 +60,7 @@ class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.field_after_expected_value = '{"manager": true}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
action = int(History.Actions.UPDATE),
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
@ -81,7 +81,7 @@ class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
action = int(History.Actions.DELETE),
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)

View File

@ -0,0 +1,42 @@
from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization
from api.tests.abstract.viewsets import ViewSetCommon
from access.viewsets.index import Index
class AccessViewset(
TestCase,
ViewSetCommon
):
viewset = Index
route_name = 'API:_api_v2_access_home'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True)
client = Client()
url = reverse(self.route_name + '-list')
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)

View File

@ -0,0 +1,30 @@
from drf_spectacular.utils import extend_schema
from rest_framework.response import Response
from rest_framework.reverse import reverse
from api.viewsets.common import CommonViewSet
@extend_schema(exclude = True)
class Index(CommonViewSet):
allowed_methods: list = [
'GET',
'HEAD',
'OPTIONS'
]
view_description = "Access Module"
view_name = "Access"
def list(self, request, pk=None):
return Response(
{
"organization": reverse('v2:_api_v2_organization-list', request=request)
}
)

View File

@ -0,0 +1,89 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
from access.serializers.organization import (
Organization,
OrganizationModelSerializer,
OrganizationViewSerializer
)
from api.viewsets.common import ModelViewSet
# @extend_schema(tags=['access'])
@extend_schema_view(
create=extend_schema(
summary = 'Create an orgnaization',
description='',
responses = {
# 200: OpenApiResponse(description='Allready exists', response=OrganizationViewSerializer),
201: OpenApiResponse(description='Created', response=OrganizationViewSerializer),
# 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing add permissions'),
}
),
destroy = extend_schema(
summary = 'Delete an orgnaization',
description = '',
responses = {
204: OpenApiResponse(description=''),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all orgnaizations',
description='',
responses = {
200: OpenApiResponse(description='', response=OrganizationViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single orgnaization',
description='',
responses = {
200: OpenApiResponse(description='', response=OrganizationViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update an orgnaization',
description = '',
responses = {
200: OpenApiResponse(description='', response=OrganizationViewSerializer),
# 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer),
# # 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing change permissions'),
}
),
)
class ViewSet( ModelViewSet ):
filterset_fields = [
'name',
'manager',
]
search_fields = [
'name',
]
model = Organization
documentation: str = ''
view_description = 'Centurion Organizations'
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']

148
app/access/viewsets/team.py Normal file
View File

@ -0,0 +1,148 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse
from access.serializers.teams import (
Team,
TeamModelSerializer,
TeamViewSerializer
)
from api.viewsets.common import ModelViewSet
# @extend_schema(tags=['access'])
@extend_schema_view(
create=extend_schema(
summary = 'Create a team within this organization',
description='',
parameters = [
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='Allready exists', response=TeamViewSerializer),
201: OpenApiResponse(description='Created', response=TeamViewSerializer),
# 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing add permissions'),
}
),
destroy = extend_schema(
summary = 'Delete a team from this organization',
description = '',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
],
responses = {
204: OpenApiResponse(description=''),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all teams from this organization',
description='',
parameters = [
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single team from this organization',
description='',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update a team within this organization',
description = '',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamViewSerializer),
# 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer),
# # 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing change permissions'),
}
),
)
class ViewSet( ModelViewSet ):
filterset_fields = [
'team_name',
]
search_fields = [
'team_name',
]
model = Team
documentation: str = ''
view_description = 'Teams belonging to a single organization'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(organization_id=self.kwargs['organization_id'])
self.queryset = queryset
return self.queryset
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']

View File

@ -0,0 +1,174 @@
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse
from access.serializers.team_user import (
TeamUsers,
TeamUserModelSerializer,
TeamUserViewSerializer
)
from api.viewsets.common import ModelViewSet
@extend_schema_view(
create=extend_schema(
summary = 'Create a user within this team',
description='',
parameters = [
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'team_id',
location = 'path',
type = int
),
],
responses = {
# 200: OpenApiResponse(description='Allready exists', response=TeamUserViewSerializer),
201: OpenApiResponse(description='Created', response=TeamUserViewSerializer),
# 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing add permissions'),
}
),
destroy = extend_schema(
summary = 'Delete a user from this team',
description = '',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'team_id',
location = 'path',
type = int
),
],
responses = {
204: OpenApiResponse(description=''),
403: OpenApiResponse(description='User is missing delete permissions'),
}
),
list = extend_schema(
summary = 'Fetch all users from this team',
description='',
parameters = [
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'team_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamUserViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
retrieve = extend_schema(
summary = 'Fetch a single user from this team',
description='',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'team_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamUserViewSerializer),
403: OpenApiResponse(description='User is missing view permissions'),
}
),
update = extend_schema(exclude = True),
partial_update = extend_schema(
summary = 'Update a user within this team',
description = '',
parameters = [
OpenApiParameter(
name = 'id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'organization_id',
location = 'path',
type = int
),
OpenApiParameter(
name = 'team_id',
location = 'path',
type = int
),
],
responses = {
200: OpenApiResponse(description='', response=TeamUserViewSerializer),
# 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer),
# # 400: OpenApiResponse(description='Validation failed.'),
403: OpenApiResponse(description='User is missing change permissions'),
}
),
)
class ViewSet( ModelViewSet ):
filterset_fields = [
'manager',
'team__organization',
]
search_fields = []
model = TeamUsers
documentation: str = ''
view_description = 'Users belonging to a single team'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(
team_id = self.kwargs['team_id']
)
self.queryset = queryset
return self.queryset
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer']
return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer']

View File

@ -5,6 +5,20 @@ from rest_framework.authentication import BaseAuthentication, get_authorization_
from api.models.tokens import AuthToken
# scheme.py
from drf_spectacular.extensions import OpenApiAuthenticationExtension
class TokenScheme(OpenApiAuthenticationExtension):
target_class = "api.auth.TokenAuthentication"
name = "TokenAuthentication"
def get_security_definition(self, auto_schema):
return {
"type": "apiKey",
"in": "header",
"name": "Token Authorization",
"description": "Token-based authentication with required prefix 'Token'",
}
class TokenAuthentication(BaseAuthentication):

8
app/api/exceptions.py Normal file
View File

@ -0,0 +1,8 @@
from rest_framework.exceptions import APIException
from rest_framework import status
class UnknownTicketType(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Unable to determin the ticket type.'
default_code = 'unknown_ticket_type'

View File

@ -0,0 +1,41 @@
# Generated by Django 5.1.2 on 2024-10-13 15:27
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='authtoken',
name='expires',
field=models.DateTimeField(help_text='When this token expires', verbose_name='Expiry Date'),
),
migrations.AlterField(
model_name='authtoken',
name='id',
field=models.AutoField(help_text='ID of this token', primary_key=True, serialize=False, unique=True, verbose_name='ID'),
),
migrations.AlterField(
model_name='authtoken',
name='note',
field=models.CharField(blank=True, default=None, help_text='A note about this token', max_length=50, null=True, verbose_name='Note'),
),
migrations.AlterField(
model_name='authtoken',
name='token',
field=models.CharField(db_index=True, help_text='The authorization token', max_length=64, unique=True, verbose_name='Auth Token'),
),
migrations.AlterField(
model_name='authtoken',
name='user',
field=models.ForeignKey(help_text='User this token belongs to', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
]

View File

@ -48,37 +48,45 @@ class AuthToken(models.Model):
id = models.AutoField(
blank=False,
help_text = 'ID of this token',
primary_key=True,
unique=True,
blank=False
verbose_name = 'ID'
)
note = models.CharField(
blank = True,
max_length = 50,
default = None,
help_text = 'A note about this token',
max_length = 50,
null= True,
verbose_name = 'Note'
)
token = models.CharField(
verbose_name = 'Auth Token',
blank = False,
db_index=True,
help_text = 'The authorization token',
max_length = 64,
null = False,
blank = False,
unique = True,
verbose_name = 'Auth Token',
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
help_text = 'User this token belongs to',
on_delete=models.CASCADE,
verbose_name = 'Owner'
)
expires = models.DateTimeField(
verbose_name = 'Expiry Date',
blank = False,
help_text = 'When this token expires',
null = False,
blank = False
verbose_name = 'Expiry Date',
)

View File

@ -0,0 +1,356 @@
from django.utils.encoding import force_str
from rest_framework import serializers
from rest_framework_json_api.metadata import JSONAPIMetadata
from rest_framework.request import clone_request
from rest_framework.reverse import reverse
from rest_framework.utils.field_mapping import ClassLookupDict
from rest_framework_json_api.utils import get_related_resource_type
from app.serializers.user import User, UserBaseSerializer
from core import fields as centurion_field
from core.fields.badge import BadgeField
from core.fields.icon import IconField
class OverRideJSONAPIMetadata(JSONAPIMetadata):
type_lookup = ClassLookupDict(
{
serializers.Field: "GenericField",
serializers.RelatedField: "Relationship",
serializers.BooleanField: "Boolean",
serializers.CharField: "String",
serializers.URLField: "URL",
serializers.EmailField: "Email",
serializers.RegexField: "Regex",
serializers.SlugField: "Slug",
serializers.IntegerField: "Integer",
serializers.FloatField: "Float",
serializers.DecimalField: "Decimal",
serializers.DateField: "Date",
serializers.DateTimeField: "DateTime",
serializers.TimeField: "Time",
serializers.ChoiceField: "Choice",
serializers.MultipleChoiceField: "MultipleChoice",
serializers.FileField: "File",
serializers.ImageField: "Image",
serializers.ListField: "List",
serializers.DictField: "Dict",
serializers.Serializer: "Serializer",
serializers.JSONField: "JSON", # New. Does not exist in base class
BadgeField: 'Badge',
IconField: 'Icon',
User: 'Relationship',
UserBaseSerializer: 'Relationship',
centurion_field.CharField: 'String',
centurion_field.MarkdownField: 'Markdown'
}
)
class ReactUIMetadata(OverRideJSONAPIMetadata):
def determine_metadata(self, request, view):
metadata = {}
metadata["name"] = view.get_view_name()
metadata["description"] = view.get_view_description()
if 'pk' in view.kwargs:
if view.kwargs['pk']:
qs = view.get_queryset()[0]
if hasattr(qs, 'get_url'):
metadata['return_url'] = qs.get_url( request )
elif view.kwargs:
metadata['return_url'] = reverse('v2:' + view.basename + '-list', request = view.request, kwargs = view.kwargs )
else:
metadata['return_url'] = reverse('v2:' + view.basename + '-list', request = view.request )
metadata["renders"] = [
renderer.media_type for renderer in view.renderer_classes
]
metadata["parses"] = [parser.media_type for parser in view.parser_classes]
metadata["allowed_methods"] = view.allowed_methods
if hasattr(view, 'get_serializer'):
serializer = view.get_serializer()
metadata['fields'] = self.get_serializer_info(serializer)
if view.suffix == 'Instance':
metadata['layout'] = view.get_page_layout()
if hasattr(view, 'get_model_documentation'):
if view.get_model_documentation():
metadata['documentation'] = view.get_model_documentation()
elif view.suffix == 'List':
if hasattr(view, 'table_fields'):
metadata['table_fields'] = view.get_table_fields()
if view.documentation:
metadata['documentation'] = view.documentation
if hasattr(view, 'page_layout'):
metadata['layout'] = view.get_page_layout()
metadata['navigation'] = [
{
"display_name": "Access",
"name": "access",
"pages": [
{
"display_name": "Organization",
"name": "organization",
"link": "/access/organization"
}
]
},
{
"display_name": "Assistance",
"name": "assistance",
"pages": [
{
"display_name": "Requests",
"name": "request",
"icon": "ticket_request",
"link": "/assistance/ticket/request"
},
{
"display_name": "Knowledge Base",
"name": "knowledge_base",
"icon": "information",
"link": "/assistance/knowledge_base"
}
]
},
{
"display_name": "ITAM",
"name": "itam",
"pages": [
{
"display_name": "Devices",
"name": "device",
"icon": "device",
"link": "/itam/device"
},
{
"display_name": "Operating System",
"name": "operating_system",
"link": "/itam/operating_system"
},
{
"display_name": "Software",
"name": "software",
"link": "/itam/software"
}
]
},
{
"display_name": "ITIM",
"name": "itim",
"pages": [
{
"display_name": "Changes",
"name": "ticket_change",
"link": "/itim/ticket/change"
},
{
"display_name": "Clusters",
"name": "cluster",
"link": "/itim/cluster"
},
{
"display_name": "Incidents",
"name": "ticket_incident",
"link": "/itim/ticket/incident"
},
{
"display_name": "Problems",
"name": "ticket_problem",
"link": "/itim/ticket/problem"
},
{
"display_name": "Services",
"name": "service",
"link": "/itim/service"
},
]
},
{
"display_name": "Config Management",
"name": "config_management",
"icon": "ansible",
"pages": [
{
"display_name": "Groups",
"name": "group",
"icon": 'config_management',
"link": "/config_management/group"
}
]
},
{
"display_name": "Project Management",
"name": "project_management",
"icon": 'project',
"pages": [
{
"display_name": "Projects",
"name": "project",
"icon": 'kanban',
"link": "/project_management/project"
}
]
},
{
"display_name": "Settings",
"name": "settings",
"pages": [
{
"display_name": "System",
"name": "setting",
"icon": "system",
"link": "/settings"
},
{
"display_name": "Task Log",
"name": "celery_log",
# "icon": "settings",
"link": "/settings/celery_log"
}
]
}
]
return metadata
def get_field_info(self, field):
""" Custom from `rest_framewarok_json_api.metadata.py`
Require that read-only fields have their choices added to the
metadata.
Given an instance of a serializer field, return a dictionary
of metadata about it.
"""
field_info = {}
serializer = field.parent
if hasattr(field, 'textarea'):
if field.textarea:
field_info["multi_line"] = True
if isinstance(field, serializers.ManyRelatedField):
field_info["type"] = self.type_lookup[field.child_relation]
else:
field_info["type"] = self.type_lookup[field]
try:
serializer_model = serializer.Meta.model
field_info["relationship_type"] = self.relation_type_lookup[
getattr(serializer_model, field.field_name)
]
except KeyError:
pass
except AttributeError:
pass
else:
field_info["relationship_resource"] = get_related_resource_type(field)
if hasattr(field, 'autolink'):
if field.autolink:
field_info['autolink'] = field.autolink
field_info["required"] = getattr(field, "required", False)
if hasattr(field, 'style_class'):
field_info["style"]: dict = {
'class': field.style_class
}
attrs = [
"read_only",
"write_only",
"label",
"help_text",
"min_length",
"max_length",
"min_value",
"max_value",
"initial",
]
for attr in attrs:
value = getattr(field, attr, None)
if value is not None and value != "":
field_info[attr] = force_str(value, strings_only=True)
if getattr(field, "child", None):
field_info["child"] = self.get_field_info(field.child)
elif getattr(field, "fields", None):
field_info["children"] = self.get_serializer_info(field)
if (
# not field_info.get("read_only")
hasattr(field, "choices")
):
field_info["choices"] = [
{
"value": choice_value,
"display_name": force_str(choice_name, strings_only=True),
}
for choice_value, choice_name in field.choices.items()
]
if (
hasattr(serializer, "included_serializers")
and "relationship_resource" in field_info
):
field_info["allows_include"] = (
field.field_name in serializer.included_serializers
)
return field_info

View File

@ -25,7 +25,7 @@ class TeamSerializerBase(serializers.ModelSerializer):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_team", args=[obj.organization.id,obj.pk]))
return request.build_absolute_uri(reverse("v1:_api_team", args=[obj.organization.id,obj.pk]))
@ -48,7 +48,7 @@ class TeamSerializer(TeamSerializerBase):
team = Team.objects.get(pk=obj.id)
return request.build_absolute_uri(reverse('API:_api_team_permission', args=[team.organization_id,team.id]))
return request.build_absolute_uri(reverse('v1:_api_team_permission', args=[team.organization_id,team.id]))
def validate(self, data):
@ -67,7 +67,7 @@ class TeamSerializer(TeamSerializerBase):
request = self.context.get('request')
return request.build_absolute_uri(reverse('API:_api_team', args=[obj.organization_id,obj.id]))
return request.build_absolute_uri(reverse('v1:_api_team', args=[obj.organization_id,obj.id]))
class Meta:
@ -93,7 +93,7 @@ class TeamSerializer(TeamSerializerBase):
class OrganizationListSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_organization", format="html"
view_name="v1:_api_organization", format="html"
)
@ -110,7 +110,7 @@ class OrganizationListSerializer(serializers.ModelSerializer):
class OrganizationSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_organization", format="html"
view_name="v1:_api_organization", format="html"
)
team_url = serializers.SerializerMethodField('get_url')
@ -121,11 +121,11 @@ class OrganizationSerializer(serializers.ModelSerializer):
team = Team.objects.filter(pk=obj.id)
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
return request.build_absolute_uri(reverse('v1:_api_organization_teams', args=[obj.id]))
teams = TeamSerializer(source='team_set', many=True, read_only=False)
view_name="API:_api_organization"
view_name="v1:_api_organization"
class Meta:

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from core import fields as centurion_field
class CommonBaseSerializer(serializers.ModelSerializer):
pass
class CommonModelSerializer(CommonBaseSerializer):
model_notes = centurion_field.MarkdownField( required = False )

View File

@ -28,7 +28,7 @@ class ParentGroupSerializer(serializers.ModelSerializer):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
return request.build_absolute_uri(reverse("v1:_api_config_group", args=[obj.pk]))
@ -59,7 +59,7 @@ class ConfigGroupsSerializerBase(serializers.ModelSerializer):
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
return request.build_absolute_uri(reverse("v1:_api_config_group", args=[obj.pk]))
@ -74,6 +74,7 @@ class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
'parent',
'name',
'config',
'hosts',
'url',
]
read_only_fields = [

View File

@ -5,6 +5,7 @@ from rest_framework.fields import empty
from api.serializers.core.ticket_comment import TicketCommentSerializer
from core import exceptions as centurion_exception
from core.forms.validate_ticket import TicketValidation
from core.models.ticket.ticket import Ticket
@ -54,7 +55,7 @@ class TicketSerializer(
return request.build_absolute_uri(
reverse(
'API:' + view_name + '-detail',
'v1:' + view_name + '-detail',
kwargs = kwargs
)
)
@ -100,7 +101,7 @@ class TicketSerializer(
return request.build_absolute_uri(
reverse(
'API:' + view_name + '-list',
'v1:' + view_name + '-list',
kwargs = kwargs
)
)
@ -172,10 +173,10 @@ class TicketSerializer(
self._ticket_type = str(self.fields['ticket_type'].choices[self._context['view']._ticket_type_value]).lower().replace(' ', '_')
is_valid = self.validate_ticket()
self.validated_data['ticket_type'] = int(self._context['view']._ticket_type_value)
is_valid = self.validate_ticket()
if self.instance is None:
subscribed_users: list = []
@ -188,7 +189,7 @@ class TicketSerializer(
except Exception as unhandled_exception:
serializers.ParseError(
centurion_exception.ParseError(
detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}"
)

View File

@ -15,7 +15,7 @@ class TicketCategorySerializer(
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_ticket_category-detail", format="html"
view_name="v1:_api_ticket_category-detail", format="html"
)

View File

@ -38,7 +38,7 @@ class TicketCommentSerializer(serializers.ModelSerializer):
return request.build_absolute_uri(
reverse('API:' + view_name + '-detail',
reverse('v1:' + view_name + '-detail',
kwargs={
'ticket_id': item.ticket.id,
'pk': item.id

View File

@ -13,7 +13,7 @@ class TicketCommentCategorySerializer(
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_ticket_comment_category-detail", format="html"
view_name="v1:_api_ticket_comment_category-detail", format="html"
)

View File

@ -1,6 +1,9 @@
from django.core.exceptions import ValidationError
from django.utils.html import escape
from rest_framework.exceptions import ValidationError
class Inventory:
""" Inventory Object

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from api.serializers.config import ParentGroupSerializer
from config_management.models.groups import ConfigGroupHosts
from config_management.models.groups import ConfigGroups
from itam.models.device import Device
@ -12,15 +12,13 @@ from itam.models.device import Device
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='group.name', read_only=True)
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_config_group", format="html"
view_name="v1:_api_config_group", format="html"
)
class Meta:
model = ConfigGroupHosts
model = ConfigGroups
fields = [
'id',
@ -38,17 +36,17 @@ class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
class DeviceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:device-detail", format="html"
view_name="v1:device-detail", format="html"
)
config = serializers.SerializerMethodField('get_device_config')
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
groups = DeviceConfigGroupsSerializer(source='configgroups_set', many=True, read_only=True)
def get_device_config(self, device):
request = self.context.get('request')
return request.build_absolute_uri(reverse('API:_api_device_config', args=[device.slug]))
return request.build_absolute_uri(reverse('v1:_api_device_config', args=[device.slug]))
class Meta:

View File

@ -7,7 +7,7 @@ from itam.models.device import Software
class SoftwareSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="API:software-detail", format="html"
view_name="v1:software-detail", format="html"
)
class Meta:

View File

@ -19,7 +19,7 @@ class ProjectMilestoneSerializer(
request = self.context.get('request')
return request.build_absolute_uri(
reverse('API:_api_project_milestone-detail',
reverse('v1:_api_project_milestone-detail',
kwargs={
'project_id': item.project.id,
'pk': item.id

View File

@ -12,7 +12,7 @@ class ProjectStateSerializer(
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_project_state-detail", format="html"
view_name="v1:_api_project_state-detail", format="html"
)

View File

@ -12,7 +12,7 @@ class ProjectTypeSerializer(
):
url = serializers.HyperlinkedIdentityField(
view_name="API:_api_project_state-detail", format="html"
view_name="v1:_api_project_state-detail", format="html"
)

View File

@ -22,7 +22,7 @@ class ProjectSerializer(
request = self.context.get('request')
return request.build_absolute_uri(reverse("API:_api_projects-detail", args=[item.pk]))
return request.build_absolute_uri(reverse("v1:_api_projects-detail", args=[item.pk]))
project_tasks_url = serializers.SerializerMethodField('get_url_project_tasks')
@ -34,7 +34,7 @@ class ProjectSerializer(
return request.build_absolute_uri(
reverse(
'API:_api_project_tasks-list',
'v1:_api_project_tasks-list',
kwargs={
'project_id': item.id
}
@ -50,7 +50,7 @@ class ProjectSerializer(
return request.build_absolute_uri(
reverse(
'API:_api_project_milestone-list',
'v1:_api_project_milestone-list',
kwargs={
'project_id': item.id
}

View File

@ -9,7 +9,7 @@ from celery import states
from access.models import Organization
from api.serializers.inventory import Inventory
from itam.serializers.inventory import InventorySerializer
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
@ -32,8 +32,15 @@ def process_inventory(self, data, organization: int):
logger.info('Begin Processing Inventory')
data = json.loads(data)
data = Inventory(data)
if type(data) is str:
data = json.loads(data)
data = InventorySerializer(
data = data
)
data.is_valid()
organization = Organization.objects.get(id=organization)
@ -42,13 +49,13 @@ def process_inventory(self, data, organization: int):
device_serial_number = None
device_uuid = None
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
if data.validated_data['details']['serial_number'] and str(data.validated_data['details']['serial_number']).lower() != 'na':
device_serial_number = str(data.details.serial_number)
device_serial_number = str(data.validated_data['details']['serial_number'])
if data.details.uuid and str(data.details.uuid).lower() != 'na':
if data.validated_data['details']['uuid'] and str(data.validated_data['details']['uuid']).lower() != 'na':
device_uuid = str(data.details.uuid)
device_uuid = str(data.validated_data['details']['uuid'])
if device_serial_number: # Search for device by serial number.
@ -88,13 +95,13 @@ def process_inventory(self, data, organization: int):
if not device: # Search for device by Name.
device = Device.objects.filter(
name__iexact=str(data.details.name).lower()
name__iexact=str(data.validated_data['details']['name']).lower()
)
if device.exists():
device = Device.objects.get(
name__iexact=str(data.details.name).lower()
name__iexact=str(data.validated_data['details']['name']).lower()
)
else:
@ -107,7 +114,7 @@ def process_inventory(self, data, organization: int):
if not device: # Create the device
device = Device.objects.create(
name = data.details.name,
name = data.validated_data['details']['name'],
device_type = None,
serial_number = device_serial_number,
uuid = device_uuid,
@ -131,14 +138,14 @@ def process_inventory(self, data, organization: int):
if not device.serial_number and device_serial_number:
device.serial_number = data.details.serial_number
device.serial_number = data.validated_data['details']['serial_number']
device_edited = True
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
if str(device.name).lower() != str(data.validated_data['details']['name']).lower(): # Update device Name
device.name = data.details.name
device.name = data.validated_data['details']['name']
device_edited = True
@ -149,14 +156,14 @@ def process_inventory(self, data, organization: int):
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
name = data.validated_data['os']['name'],
is_global = True
)
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
name = data.validated_data['os']['name'],
is_global = True
)
@ -170,7 +177,7 @@ def process_inventory(self, data, organization: int):
if not operating_system:
operating_system = OperatingSystem.objects.filter(
name=data.operating_system.name,
name = data.validated_data['os']['name'],
organization = organization
)
@ -178,7 +185,7 @@ def process_inventory(self, data, organization: int):
if operating_system.exists():
operating_system = OperatingSystem.objects.get(
name=data.operating_system.name,
name = data.validated_data['os']['name'],
organization = organization
)
@ -190,22 +197,22 @@ def process_inventory(self, data, organization: int):
if not operating_system:
operating_system = OperatingSystem.objects.create(
name = data.operating_system.name,
name = data.validated_data['os']['name'],
organization = organization,
is_global = True
)
operating_system_version = OperatingSystemVersion.objects.filter(
name=data.operating_system.version_major,
operating_system=operating_system
name = data.validated_data['os']['version_major'],
operating_system = operating_system
)
if operating_system_version.exists():
operating_system_version = OperatingSystemVersion.objects.get(
name=data.operating_system.version_major,
operating_system=operating_system
name = data.validated_data['os']['version_major'],
operating_system = operating_system
)
else:
@ -218,7 +225,7 @@ def process_inventory(self, data, organization: int):
operating_system_version = OperatingSystemVersion.objects.create(
organization = organization,
is_global = True,
name = data.operating_system.version_major,
name = data.validated_data['os']['version_major'],
operating_system = operating_system,
)
@ -241,8 +248,8 @@ def process_inventory(self, data, organization: int):
device_operating_system = DeviceOperatingSystem.objects.create(
organization = organization,
device=device,
version = data.operating_system.version,
device = device,
version = data.validated_data['os']['version'],
operating_system_version = operating_system_version,
installdate = timezone.now()
)
@ -261,9 +268,9 @@ def process_inventory(self, data, organization: int):
device_operating_system.save()
if device_operating_system.version != data.operating_system.version:
if device_operating_system.version != data.validated_data['os']['version']:
device_operating_system.version = data.operating_system.version
device_operating_system.version = data.validated_data['os']['version']
device_operating_system.save()
@ -287,7 +294,7 @@ def process_inventory(self, data, organization: int):
inventoried_software: list = []
for inventory in list(data.software):
for inventory in list(data.validated_data['software']):
software = None
software_category = None
@ -295,13 +302,13 @@ def process_inventory(self, data, organization: int):
device_software = None
software_category = SoftwareCategory.objects.filter( name = inventory.category )
software_category = SoftwareCategory.objects.filter( name = inventory['category'] )
if software_category.exists():
software_category = SoftwareCategory.objects.get(
name = inventory.category
name = inventory['category']
)
else: # Create Software Category
@ -309,16 +316,16 @@ def process_inventory(self, data, organization: int):
software_category = SoftwareCategory.objects.create(
organization = software_category_organization,
is_global = True,
name = inventory.category,
name = inventory['category'],
)
if software_category.name == inventory.category:
if software_category.name == inventory['category']:
if Software.objects.filter( name = inventory.name ).exists():
if Software.objects.filter( name = inventory['name'] ).exists():
software = Software.objects.get(
name = inventory.name
name = inventory['name']
)
if not software.category:
@ -331,16 +338,16 @@ def process_inventory(self, data, organization: int):
software = Software.objects.create(
organization = software_organization,
is_global = True,
name = inventory.name,
name = inventory['name'],
category = software_category,
)
if software.name == inventory.name:
if software.name == inventory['name']:
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
semver = re.search(pattern, str(inventory.version), re.DOTALL)
semver = re.search(pattern, str(inventory['version']), re.DOTALL)
if semver:
@ -348,7 +355,7 @@ def process_inventory(self, data, organization: int):
semver = semver['semver']
else:
semver = inventory.version
semver = inventory['version']
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():

View File

@ -0,0 +1,249 @@
from rest_framework.relations import Hyperlink
class APICommonFields:
"""Test Cases for fields common to All API responses
Must contain:
- id
- display_name
- _urls
- _urls._self
"""
api_data: object
""" API Response data """
def test_api_field_exists_id(self):
""" Test for existance of API Field
id field must exist
"""
assert 'id' in self.api_data
def test_api_field_type_id(self):
""" Test for type for API Field
id field must be int
"""
assert type(self.api_data['id']) is int
def test_api_field_exists_display_name(self):
""" Test for existance of API Field
display_name field must exist
"""
assert 'display_name' in self.api_data
def test_api_field_type_display_name(self):
""" Test for type for API Field
display_name field must be str
"""
assert type(self.api_data['display_name']) is str
def test_api_field_exists_urls(self):
""" Test for existance of API Field
_urls field must exist
"""
assert '_urls' in self.api_data
def test_api_field_type_urls(self):
""" Test for type for API Field
_urls field must be str
"""
assert type(self.api_data['_urls']) is dict
def test_api_field_exists_urls_self(self):
""" Test for existance of API Field
_urls._self field must exist
"""
assert '_self' in self.api_data['_urls']
def test_api_field_type_urls(self):
""" Test for type for API Field
_urls._self field must be str
"""
assert type(self.api_data['_urls']['_self']) is str
class APIModelFields(
APICommonFields
):
"""Test Cases for fields common to All API Model responses
Must contain:
- id
- display_name
- _urls
- _urls._self
"""
api_data: object
""" API Response data """
def test_api_field_exists_model_notes(self):
""" Test for existance of API Field
model_notes field must exist
"""
assert 'model_notes' in self.api_data
def test_api_field_type_model_notes(self):
""" Test for type for API Field
model_notes field must be str
"""
assert type(self.api_data['model_notes']) is str
def test_api_field_exists_created(self):
""" Test for existance of API Field
created field must exist
"""
assert 'created' in self.api_data
def test_api_field_type_created(self):
""" Test for type for API Field
created field must be str
"""
assert type(self.api_data['created']) is str
def test_api_field_exists_modified(self):
""" Test for existance of API Field
modified field must exist
"""
assert 'modified' in self.api_data
def test_api_field_type_modified(self):
""" Test for type for API Field
modified field must be str
"""
assert type(self.api_data['modified']) is str
class APITenancyObject(
APIModelFields
):
api_data: object
""" API Response data """
def test_api_field_exists_organization(self):
""" Test for existance of API Field
organization field must exist
"""
assert 'organization' in self.api_data
def test_api_field_type_organization(self):
""" Test for type for API Field
organization field must be dict
"""
assert type(self.api_data['organization']) is dict
def test_api_field_exists_organization_id(self):
""" Test for existance of API Field
organization.id field must exist
"""
assert 'id' in self.api_data['organization']
def test_api_field_type_organization_id(self):
""" Test for type for API Field
organization.id field must be dict
"""
assert type(self.api_data['organization']['id']) is int
def test_api_field_exists_organization_display_name(self):
""" Test for existance of API Field
organization.display_name field must exist
"""
assert 'display_name' in self.api_data['organization']
def test_api_field_type_organization_display_name(self):
""" Test for type for API Field
organization.display_name field must be str
"""
assert type(self.api_data['organization']['display_name']) is str
def test_api_field_exists_organization_url(self):
""" Test for existance of API Field
organization.url field must exist
"""
assert 'url' in self.api_data['organization']
def test_api_field_type_organization_url(self):
""" Test for type for API Field
organization.url field must be str
"""
assert type(self.api_data['organization']['url']) is Hyperlink

View File

@ -0,0 +1,513 @@
import pytest
import unittest
from django.shortcuts import reverse
from django.test import TestCase, Client
class APIPermissionView:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_returned_results_only_user_orgs(self):
"""Returned results check
Ensure that a query to the viewset endpoint does not return
items that are not part of the users organizations.
"""
# Ensure the other org item exists, without test not able to function
print('Check that the different organization item has been defined')
assert hasattr(self, 'other_org_item')
# ensure that the variables for the two orgs are different orgs
print('checking that the different and user oganizations are different')
assert self.different_organization.id != self.organization.id
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.view_user)
response = client.get(url)
contains_different_org: bool = False
for item in response.data['results']:
if int(item['organization']['id']) != self.organization.id:
contains_different_org = True
assert not contains_different_org
class APIPermissionAdd:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_list: str
""" URL view name of the item list page """
url_kwargs: dict = None
""" URL view kwargs for the item list page """
add_data: dict = None
def test_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
response = client.put(url, data=self.add_data)
assert response.status_code == 401
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
client.force_login(self.different_organization_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.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_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.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
class APIPermissionChange:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
change_data: dict = None
def test_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 401
def test_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 200
class APIPermissionDelete:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
delete_data: dict = None
def test_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 401
def test_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 204
class APIPermissions(
APIPermissionAdd,
APIPermissionChange,
APIPermissionDelete,
APIPermissionView
):
""" Abstract class containing all API Permission test cases """
model: object
""" Item Model to test """

View File

@ -0,0 +1,163 @@
import pytest
import unittest
from django.shortcuts import reverse
from django.test import TestCase, Client
class SerializerView:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
def test_returned_serializer_user_view(self):
""" Check correct Serializer is returned
View action for view user must return `ViewSerializer`
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert str(response.renderer_context['view'].get_serializer().__class__.__name__).endswith('ViewSerializer')
class SerializerAdd:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_list: str
""" URL view name of the item list page """
url_kwargs: dict = None
""" URL view kwargs for the item list page """
add_data: dict = None
def test_returned_serializer_user_add(self):
""" Check correct Serializer is returned
Add action for add user must return `ModelSerializer`
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.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 str(response.renderer_context['view'].get_serializer().__class__.__name__).endswith('ModelSerializer')
class SerializerChange:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
change_data: dict = None
def test_returned_serializer_user_change(self):
""" Check correct Serializer is returned
Change action for change user must return `ModelSerializer`
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert str(response.renderer_context['view'].get_serializer().__class__.__name__).endswith('ModelSerializer')
class SerializerDelete:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
delete_data: dict = None
def test_returned_serializer_user_delete(self):
""" Check correct Serializer is returned
Delete action for delete user must return `ModelSerializer`
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs)
client.force_login(self.delete_user)
response = client.delete(url)
assert str(response.renderer_context['view'].get_serializer().__class__.__name__).endswith('ModelSerializer')
class SerializersTestCases(
SerializerAdd,
SerializerChange,
SerializerDelete,
SerializerView
):
""" Abstract class containing all ViewSet test cases """
model: object
""" Item Model to test """

View File

@ -0,0 +1,586 @@
from api.react_ui_metadata import ReactUIMetadata
from api.views.mixin import OrganizationPermissionAPI
class AllViewSet:
"""Tests specific to the Viewset
**Dont include these tests directly, see below for correct class**
Tests are for ALL viewsets.
"""
viewset = None
"""ViewSet to Test"""
def test_view_attr_allowed_methods_exists(self):
"""Attribute Test
Attribute `allowed_methods` must exist
"""
assert hasattr(self.viewset, 'allowed_methods')
def test_view_attr_allowed_methods_not_empty(self):
"""Attribute Test
Attribute `allowed_methods` must return a value
"""
view_set = self.viewset()
assert view_set.allowed_methods is not None
def test_view_attr_allowed_methods_type(self):
"""Attribute Test
Attribute `allowed_methods` must be of type list
"""
view_set = self.viewset()
assert type(view_set.allowed_methods) is list
def test_view_attr_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for index views
valid_values: list = [
'GET',
'HEAD',
'OPTIONS',
]
all_valid: bool = True
view_set = self.viewset()
for method in list(view_set.allowed_methods):
if method not in valid_values:
all_valid = False
assert all_valid
def test_view_attr_metadata_class_exists(self):
"""Attribute Test
Attribute `metadata_class` must exist
"""
assert hasattr(self.viewset, 'metadata_class')
def test_view_attr_metadata_class_not_empty(self):
"""Attribute Test
Attribute `metadata_class` must return a value
"""
view_set = self.viewset()
assert view_set.metadata_class is not None
def test_view_attr_metadata_class_type(self):
"""Attribute Test
Attribute `metadata_class` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.metadata_class is ReactUIMetadata
def test_view_attr_permission_classes_exists(self):
"""Attribute Test
Attribute `permission_classes` must exist
"""
assert hasattr(self.viewset, 'permission_classes')
def test_view_attr_permission_classes_not_empty(self):
"""Attribute Test
Attribute `permission_classes` must return a value
"""
view_set = self.viewset()
assert view_set.permission_classes is not None
def test_view_attr_permission_classes_type(self):
"""Attribute Test
Attribute `permission_classes` must be list
"""
view_set = self.viewset()
assert type(view_set.permission_classes) is list
def test_view_attr_permission_classes_value(self):
"""Attribute Test
Attribute `permission_classes` must be metadata class `ReactUIMetadata`
"""
view_set = self.viewset()
assert view_set.permission_classes[0] is OrganizationPermissionAPI
assert len(view_set.permission_classes) == 1
def test_view_attr_view_description_exists(self):
"""Attribute Test
Attribute `view_description` must exist
"""
assert hasattr(self.viewset, 'view_description')
def test_view_attr_view_description_not_empty(self):
"""Attribute Test
Attribute `view_description` must return a value
"""
assert self.viewset.view_description is not None
def test_view_attr_view_description_type(self):
"""Attribute Test
Attribute `view_description` must be of type str
"""
assert type(self.viewset.view_description) is str
def test_view_attr_view_name_exists(self):
"""Attribute Test
Attribute `view_name` must exist
"""
assert hasattr(self.viewset, 'view_name')
def test_view_attr_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
assert self.viewset.view_name is not None
def test_view_attr_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
view_set = self.viewset()
assert (
type(view_set.view_name) is str
)
class APIRenderViewSet:
"""Function ViewSet test
**Dont include these tests directly, see below for correct class**
These tests ensure that the data from the ViewSet is present for a
HTTP Request
"""
http_options_response_list: dict = None
"""The HTTP/Options Response for the ViewSet"""
def test_api_render_field_allowed_methods_exists(self):
"""Attribute Test
Attribute `allowed_methods` must exist
"""
assert 'allowed_methods' in self.http_options_response_list.data
def test_api_render_field_allowed_methods_not_empty(self):
"""Attribute Test
Attribute `allowed_methods` must return a value
"""
assert len(self.http_options_response_list.data['allowed_methods']) > 0
def test_api_render_field_allowed_methods_type(self):
"""Attribute Test
Attribute `allowed_methods` must be of type list
"""
assert type(self.http_options_response_list.data['allowed_methods']) is list
def test_api_render_field_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for index views
valid_values: list = [
'GET',
'HEAD',
'OPTIONS',
]
all_valid: bool = True
for method in list(self.http_options_response_list.data['allowed_methods']):
if method not in valid_values:
all_valid = False
assert all_valid
def test_api_render_field_view_description_exists(self):
"""Attribute Test
Attribute `description` must exist
"""
assert 'description' in self.http_options_response_list.data
def test_api_render_field_view_description_not_empty(self):
"""Attribute Test
Attribute `view_description` must return a value
"""
assert self.http_options_response_list.data['description'] is not None
def test_api_render_field_view_description_type(self):
"""Attribute Test
Attribute `view_description` must be of type str
"""
assert type(self.http_options_response_list.data['description']) is str
def test_api_render_field_view_name_exists(self):
"""Attribute Test
Attribute `view_name` must exist
"""
assert 'name' in self.http_options_response_list.data
def test_api_render_field_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
assert self.http_options_response_list.data['name'] is not None
def test_api_render_field_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
assert type(self.http_options_response_list.data['name']) is str
class ModelViewSet(AllViewSet):
"""Tests for Model Viewsets
**Dont include these tests directly, see below for correct class**
"""
viewset = None
"""ViewSet to Test"""
def test_view_attr_documentation_exists(self):
"""Attribute Test
Attribute `documentation` must exist
"""
assert hasattr(self.viewset, 'documentation')
def test_view_attr_documentation_type(self):
"""Attribute Test
Attribute `documentation` must be of type str or None.
this attribute is optional.
"""
view_set = self.viewset()
assert (
type(view_set.documentation) is str
or type(view_set.documentation) is None
)
def test_view_attr_filterset_fields_exists(self):
"""Attribute Test
Attribute `filterset_fields` must exist
"""
assert hasattr(self.viewset, 'filterset_fields')
def test_view_attr_filterset_fields_not_empty(self):
"""Attribute Test
Attribute `filterset_fields` must return a value
"""
assert self.viewset.filterset_fields is not None
def test_view_attr_filterset_fields_type(self):
"""Attribute Test
Attribute `filterset_fields` must be of type list
"""
view_set = self.viewset()
assert (
type(view_set.filterset_fields) is list
)
def test_view_attr_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for model views
valid_values: list = [
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
all_valid: bool = True
view_set = self.viewset()
for method in list(view_set.allowed_methods):
if method not in valid_values:
all_valid = False
assert all_valid
def test_view_attr_model_exists(self):
"""Attribute Test
Attribute `model` must exist
"""
assert hasattr(self.viewset, 'model')
def test_view_attr_model_not_empty(self):
"""Attribute Test
Attribute `model` must return a value
"""
view_set = self.viewset()
assert view_set.model is not None
def test_view_attr_search_fields_exists(self):
"""Attribute Test
Attribute `search_fields` must exist
"""
assert hasattr(self.viewset, 'search_fields')
def test_view_attr_search_fields_not_empty(self):
"""Attribute Test
Attribute `search_fields` must return a value
"""
assert self.viewset.search_fields is not None
def test_view_attr_search_fields_type(self):
"""Attribute Test
Attribute `search_fields` must be of type list
"""
view_set = self.viewset()
assert (
type(view_set.search_fields) is list
)
def test_view_attr_view_name_not_empty(self):
"""Attribute Test
Attribute `view_name` must return a value
"""
view_set = self.viewset()
assert (
view_set.view_name is not None
or view_set.get_view_name() is not None
)
def test_view_attr_view_name_type(self):
"""Attribute Test
Attribute `view_name` must be of type str
"""
view_set = self.viewset()
assert (
type(view_set.view_name) is str
or type(view_set.get_view_name()) is str
)
class APIRenderModelViewSet(APIRenderViewSet):
"""Tests for Model Viewsets
**Dont include these tests directly, see below for correct class**
"""
viewset = None
"""ViewSet to Test"""
def test_api_render_field_allowed_methods_values(self):
"""Attribute Test
Attribute `allowed_methods` only contains valid values
"""
# Values valid for model views
valid_values: list = [
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
all_valid: bool = True
for method in list(self.http_options_response_list.data['allowed_methods']):
if method not in valid_values:
all_valid = False
assert all_valid
class ViewSetCommon(
AllViewSet,
APIRenderViewSet
):
""" Tests for Non-Model Viewsets
**Include this class directly into Non-Model ViewSets**
Args:
AllViewSet (class): Tests for all Viewsets.
APIRenderViewSet (class): Tests to check API Rendering to ensure data present.
"""
pass
class ViewSetModel(
ModelViewSet,
APIRenderModelViewSet
):
"""Tests for model ViewSets
**Include this class directly into Model ViewSets**
Args:
ModelViewSet (class): Tests for Model Viewsets, includes `AllViewSet` tests.
APIRenderModelViewSet (class): Tests to check API rendering to ensure data is present, includes `APIRenderViewSet` tests.
"""
pass

View File

@ -160,7 +160,7 @@ class InventoryAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -182,7 +182,7 @@ class InventoryAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -201,7 +201,7 @@ class InventoryAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -220,7 +220,7 @@ class InventoryAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -239,7 +239,7 @@ class InventoryAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -395,7 +395,7 @@ class InventoryAPI(TestCase):
""" Successful inventory upload returns 200 for existing device"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)
response = client.post(url, data=self.inventory, content_type='application/json')
@ -409,7 +409,7 @@ class InventoryAPI(TestCase):
""" Incorrectly formated inventory upload returns 400 """
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
mod_inventory = self.inventory.copy()

View File

@ -201,7 +201,7 @@ class InventoryPermissionsAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
response = client.put(url, data=self.inventory, content_type='application/json')
@ -218,7 +218,7 @@ class InventoryPermissionsAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.no_permissions_user)
@ -236,7 +236,7 @@ class InventoryPermissionsAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.different_organization_user)
@ -254,7 +254,7 @@ class InventoryPermissionsAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.view_user)
@ -272,7 +272,7 @@ class InventoryPermissionsAPI(TestCase):
"""
client = Client()
url = reverse('API:_api_device_inventory')
url = reverse('v1:_api_device_inventory')
client.force_login(self.add_user)

View File

@ -0,0 +1,42 @@
from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models import Organization
from api.tests.abstract.viewsets import ViewSetCommon
from api.viewsets.index import Index
class HomeViewset(
TestCase,
ViewSetCommon
):
viewset = Index
route_name = 'API:_api_v2_home'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user
3. create super user
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True)
client = Client()
url = reverse(self.route_name + '-list')
client.force_login(self.view_user)
self.http_options_response_list = client.options(url)

View File

@ -64,15 +64,10 @@ router.register('settings/ticket_comment_categories', ticket_comment_categories.
router.register('software', software.SoftwareViewSet, basename='software')
urlpatterns = [
path("assistance", assistance.index.Index.as_view(), name="_api_assistance"),
#
# Sof Old Paths to be refactored
#
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),

196
app/api/urls_v2.py Normal file
View File

@ -0,0 +1,196 @@
from django.urls import path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.routers import DefaultRouter
from api.viewsets import (
index as v2
)
from app.viewsets.base import (
index as base_index_v2,
content_type as content_type_v2,
permisson as permission_v2,
user as user_v2
)
from access.viewsets import (
index as access_v2,
organization as organization_v2,
team as team_v2,
team_user as team_user_v2
)
from assistance.viewsets import (
index as assistance_index_v2,
knowledge_base as knowledge_base_v2,
knowledge_base_category as knowledge_base_category_v2,
request as request_ticket_v2,
)
from config_management.viewsets import (
index as config_management_v2,
config_group as config_group_v2,
config_group_software as config_group_software_v2
)
from core.viewsets import (
celery_log as celery_log_v2,
history as history_v2,
manufacturer as manufacturer_v2,
notes as notes_v2,
ticket_category,
ticket_comment,
ticket_comment_category,
ticket_linked_item,
related_ticket,
)
from itam.viewsets import (
index as itam_index_v2,
device as device_v2,
device_model as device_model_v2,
device_type as device_type_v2,
device_software as device_software_v2,
device_operating_system,
inventory,
operating_system as operating_system_v2,
operating_system_version as operating_system_version_v2,
software as software_v2,
software_category as software_category_v2,
software_version as software_version_v2,
)
from itim.viewsets import (
index as itim_v2,
change,
cluster as cluster_v2,
cluster_type as cluster_type_v2,
incident,
port as port_v2,
problem,
service as service_v2,
service_device as service_device_v2
)
from project_management.viewsets import (
index as project_management_v2,
project as project_v2,
project_milestone as project_milestone_v2,
project_state as project_state_v2,
project_task,
project_type as project_type_v2,
)
from settings.viewsets import (
app_settings as app_settings_v2,
external_link as external_link_v2,
index as settings_index_v2,
user_settings as user_settings_v2
)
app_name = "API"
router = DefaultRouter(trailing_slash=False)
router.register('', v2.Index, basename='_api_v2_home')
router.register('access', access_v2.Index, basename='_api_v2_access_home')
router.register('access/organization', organization_v2.ViewSet, basename='_api_v2_organization')
router.register('access/organization/(?P<organization_id>[0-9]+)/team', team_v2.ViewSet, basename='_api_v2_organization_team')
router.register('access/organization/(?P<organization_id>[0-9]+)/team/(?P<team_id>[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user')
router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home')
router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base')
router.register('assistance/ticket/request', request_ticket_v2.ViewSet, basename='_api_v2_ticket_request')
router.register('base', base_index_v2.Index, basename='_api_v2_base_home')
router.register('base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type')
router.register('base/permission', permission_v2.ViewSet, basename='_api_v2_permission')
router.register('base/user', user_v2.ViewSet, basename='_api_v2_user')
router.register('config_management', config_management_v2.Index, basename='_api_v2_config_management_home')
router.register('config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group')
router.register('config_management/group/(?P<parent_group>[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child')
router.register('config_management/group/(?P<config_group_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_config_group_notes')
router.register('config_management/group/(?P<config_group_id>[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software')
router.register('core/(?P<model_class>.+)/(?P<model_id>[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history')
router.register('core/ticket/(?P<ticket_id>[0-9]+)/comments', ticket_comment.ViewSet, basename='_api_v2_ticket_comment')
router.register('core/ticket/(?P<ticket_id>[0-9]+)/comments/(?P<parent_id>[0-9]+)/threads', ticket_comment.ViewSet, basename='_api_v2_ticket_comment_threads')
router.register('core/ticket/(?P<ticket_id>[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item')
router.register('core/ticket/(?P<ticket_id>[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related')
router.register('core/(?P<item_class>[a-z_]+)/(?P<item_id>[0-9]+)/item_ticket', ticket_linked_item.ViewSet, basename='_api_v2_item_tickets')
router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home')
router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device')
router.register('itam/device/(?P<device_id>[0-9]+)/operating_system', device_operating_system.ViewSet, basename='_api_v2_device_operating_system')
router.register('itam/device/(?P<device_id>[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software')
router.register('itam/device/(?P<device_id>[0-9]+)/service', service_device_v2.ViewSet, basename='_api_v2_service_device')
router.register('itam/device/(?P<device_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes')
router.register('itam/inventory', inventory.ViewSet, basename='_api_v2_inventory')
router.register('itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system')
router.register('itam/operating_system/(?P<operating_system_id>[0-9]+)/installs', device_operating_system.ViewSet, basename='_api_v2_operating_system_installs')
router.register('itam/operating_system/(?P<operating_system_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes')
router.register('itam/operating_system/(?P<operating_system_id>[0-9]+)/version', operating_system_version_v2.ViewSet, basename='_api_v2_operating_system_version')
router.register('itam/software', software_v2.ViewSet, basename='_api_v2_software')
router.register('itam/software/(?P<software_id>[0-9]+)/installs', device_software_v2.ViewSet, basename='_api_v2_software_installs')
router.register('itam/software/(?P<software_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes')
router.register('itam/software/(?P<software_id>[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version')
router.register('itim', itim_v2.Index, basename='_api_v2_itim_home')
router.register('itim/ticket/change', change.ViewSet, basename='_api_v2_ticket_change')
router.register('itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster')
router.register('itim/cluster/(?P<cluster_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes')
router.register('itim/ticket/incident', incident.ViewSet, basename='_api_v2_ticket_incident')
router.register('itim/ticket/problem', problem.ViewSet, basename='_api_v2_ticket_problem')
router.register('itim/service', service_v2.ViewSet, basename='_api_v2_service')
router.register('itim/service/(?P<service_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes')
router.register('project_management', project_management_v2.Index, basename='_api_v2_project_management_home')
router.register('project_management/project', project_v2.ViewSet, basename='_api_v2_project')
router.register('project_management/project/(?P<project_id>[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone')
router.register('project_management/project/(?P<project_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes')
router.register('project_management/project/(?P<project_id>[0-9]+)/project_task', project_task.ViewSet, basename='_api_v2_ticket_project_task')
router.register('settings', settings_index_v2.Index, basename='_api_v2_settings_home')
router.register('settings/app_settings', app_settings_v2.ViewSet, basename='_api_v2_app_settings')
router.register('settings/celery_log', celery_log_v2.ViewSet, basename='_api_v2_celery_log')
router.register('settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type')
router.register('settings/cluster_type/(?P<cluster_type_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_type_notes')
router.register('settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model')
router.register('settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type')
router.register('settings/external_link', external_link_v2.ViewSet, basename='_api_v2_external_link')
router.register('settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category')
router.register('settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer')
router.register('settings/manufacturer/(?P<manufacturer_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes')
router.register('settings/port', port_v2.ViewSet, basename='_api_v2_port')
router.register('settings/port/(?P<port_id>[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_port_notes')
router.register('settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state')
router.register('settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type')
router.register('settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_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/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings')
urlpatterns = [
path('schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',),
path('docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'),
]
urlpatterns += router.urls

View File

@ -0,0 +1,47 @@
from django.test import TestCase
class ViewSetAttributesUnit:
""" Unit Tests For View Set attributes.
These tests ensure that View sets contian the required attributesthat are
used by the API .
"""
def test_attribute_exists_page_layout(self):
"""Attrribute Test, Exists
Ensure attribute `page_layout` exists
"""
pass
def test_attribute_type_page_layout(self):
"""Attrribute Test, Type
Ensure attribute `page_layout` is of type `list`
"""
pass
def test_attribute_not_callable_page_layout(self):
"""Attrribute Test, Not Callable
Attribute must be a property
Ensure attribute `page_layout` is not callable.
"""
pass
# other tests required
# - filterset_fields
# - metadata_class
# - search_fields
# - documentation
# - model_documentation or is in `model.documentation`

View File

@ -12,7 +12,7 @@ from access.models import Organization, Team
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated=True)
@extend_schema_view(
get=extend_schema(
summary = "Fetch Organizations",
@ -34,7 +34,7 @@ class OrganizationList(generics.ListAPIView):
return "Organizations"
@extend_schema(deprecated=True)
@extend_schema_view(
get=extend_schema(
summary = "Get An Organization",
@ -61,7 +61,7 @@ class OrganizationDetail(generics.RetrieveUpdateAPIView):
return "Organization"
@extend_schema(deprecated=True)
@extend_schema_view(
post=extend_schema(
summary = "Create a Team",
@ -97,7 +97,7 @@ class TeamList(generics.ListCreateAPIView):
return "Organization Teams"
@extend_schema(deprecated=True)
@extend_schema_view(
get=extend_schema(
summary = "Fetch a Team",
@ -149,7 +149,7 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'group_ptr_id'
@extend_schema(deprecated=True)
@extend_schema_view(
get=extend_schema(
summary = "Fetch a teams permissions",

View File

@ -1,5 +1,7 @@
from django.utils.safestring import mark_safe
from drf_spectacular.utils import extend_schema
from rest_framework import generics, permissions, routers, views
# from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
@ -7,7 +9,7 @@ from rest_framework.response import Response
from rest_framework.reverse import reverse
@extend_schema(deprecated=True)
class Index(views.APIView):
permission_classes = [
@ -29,7 +31,7 @@ class Index(views.APIView):
def get(self, request, *args, **kwargs):
body: dict = {
'requests': reverse('API:_api_assistance_request-list', request=request)
'requests': reverse('v1:_api_assistance_request-list', request=request)
}
return Response(body)

View File

@ -4,6 +4,7 @@ from api.serializers.assistance.request import RequestTicketSerializer
from api.views.core.tickets import View
@extend_schema(deprecated=True)
class View(View):
_ticket_type:str = 'request'

View File

@ -8,7 +8,7 @@ from api.views.mixin import OrganizationPermissionAPI
from config_management.models.groups import ConfigGroups
@extend_schema( deprecated = True )
@extend_schema_view(
get=extend_schema(
summary = "Fetch Config groups",
@ -31,6 +31,7 @@ class ConfigGroupsList(generics.ListAPIView):
@extend_schema( deprecated = True )
@extend_schema_view(
get=extend_schema(
summary = "Get A Config Group",

View File

@ -10,7 +10,7 @@ from api.serializers.core.ticket_category import TicketCategory, TicketCategoryS
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated=True)
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -10,7 +10,7 @@ from api.serializers.core.ticket_comment_category import TicketCommentCategory,
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated=True)
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -12,7 +12,7 @@ from api.views.mixin import OrganizationPermissionAPI
from core.models.ticket.ticket_comment import TicketComment
@extend_schema(deprecated=True)
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -1,3 +1,5 @@
from django.conf import settings as django_settings
from django.utils.safestring import mark_safe
from rest_framework import generics, permissions, routers, viewsets
@ -16,10 +18,10 @@ class Index(viewsets.ViewSet):
def get_view_name(self):
return "API Index"
return "API"
def get_view_description(self, html=False) -> str:
text = "My REST API"
text = "Centurion ERP Rest API"
if html:
return mark_safe(f"<p>{text}</p>")
else:
@ -27,16 +29,18 @@ class Index(viewsets.ViewSet):
def list(self, request, pk=None):
return Response(
{
API: dict = {
# "teams": reverse("_api_teams", request=request),
'assistance': reverse("API:_api_assistance", request=request),
"devices": reverse("API:device-list", request=request),
"config_groups": reverse("API:_api_config_groups", request=request),
'itim': reverse("API:_api_itim", request=request),
"organizations": reverse("API:_api_orgs", request=request),
'project_management': reverse("API:_api_project_management", request=request),
"settings": reverse('API:_settings', request=request),
"software": reverse("API:software-list", request=request),
'assistance': reverse("v1:_api_assistance", request=request),
"devices": reverse("v1:device-list", request=request),
"config_groups": reverse("v1:_api_config_groups", request=request),
'itim': reverse("v1:_api_itim", request=request),
"organizations": reverse("v1:_api_orgs", request=request),
'project_management': reverse("v1:_api_project_management", request=request),
"settings": reverse('v1:_settings', request=request),
"software": reverse("v1:software-list", request=request),
'v2': reverse("v2:_api_v2_home-list", request=request)
}
)
return Response( API )

View File

@ -1,11 +1,14 @@
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from drf_spectacular.utils import extend_schema
from itam.models.device import Device
from rest_framework import views
from rest_framework.response import Response
@extend_schema( deprecated = True )
class View(views.APIView):
def get(self, request, slug):

View File

@ -14,7 +14,7 @@ from api.views.mixin import OrganizationPermissionAPI
from itam.models.device import Device
@extend_schema( deprecated = True )
class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -1,11 +1,10 @@
import json
import re
from django.core.exceptions import ValidationError, PermissionDenied
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import generics, views
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response
from api.views.mixin import OrganizationPermissionAPI
@ -34,6 +33,7 @@ class InventoryPermissions(OrganizationPermissionAPI):
@extend_schema( deprecated = True )
class Collect(OrganizationPermissionAPI, views.APIView):
queryset = Device.objects.all()
@ -91,12 +91,13 @@ this setting populated, no device will be created and the endpoint will return H
if not self.permission_check(request=request, view=self, obj=device):
raise Http404
raise PermissionDenied()
task = process_inventory.delay(request.body, self.default_organization.id)
response_data: dict = {"task_id": f"{task.id}"}
except PermissionDenied as e:
status = Http.Status.FORBIDDEN
@ -105,7 +106,7 @@ this setting populated, no device will be created and the endpoint will return H
except ValidationError as e:
status = Http.Status.BAD_REQUEST
response_data = e.message
response_data = e.detail
except Exception as e:

View File

@ -1,6 +1,8 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework import generics, viewsets
from access.mixin import OrganizationMixin
@ -11,7 +13,7 @@ from api.views.mixin import OrganizationPermissionAPI
from itam.models.software import Software
@extend_schema(deprecated = True)
class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -5,7 +5,7 @@ from api.serializers.itim.change import ChangeTicketSerializer
from api.views.core.tickets import View
@extend_schema(deprecated=True)
class View(View):
_ticket_type:str = 'change'

View File

@ -5,6 +5,7 @@ from api.views.core.tickets import View
@extend_schema(deprecated=True)
class View(View):
_ticket_type:str = 'incident'

View File

@ -1,5 +1,7 @@
from django.utils.safestring import mark_safe
from drf_spectacular.utils import extend_schema
from rest_framework import views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@ -7,6 +9,7 @@ from rest_framework.reverse import reverse
@extend_schema(deprecated=True)
class Index(views.APIView):
permission_classes = [
@ -28,9 +31,9 @@ class Index(views.APIView):
def get(self, request, *args, **kwargs):
body: dict = {
'changes': reverse('API:_api_itim_change-list', request=request),
'incidents': reverse('API:_api_itim_incident-list', request=request),
'problems': reverse('API:_api_itim_problem-list', request=request),
'changes': reverse('v1:_api_itim_change-list', request=request),
'incidents': reverse('v1:_api_itim_incident-list', request=request),
'problems': reverse('v1:_api_itim_problem-list', request=request),
}
return Response(body)

View File

@ -5,6 +5,7 @@ from api.views.core.tickets import View
@extend_schema(deprecated=True)
class View(View):
_ticket_type:str = 'problem'

View File

@ -6,6 +6,8 @@ from rest_framework.permissions import DjangoObjectPermissions
from access.mixin import OrganizationMixin
from core import exceptions as centurion_exceptions
class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
@ -27,104 +29,111 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
return False
self.request = request
try:
method = self.request._request.method.lower()
self.request = request
if method.upper() not in view.allowed_methods:
method = self.request._request.method.lower()
view.http_method_not_allowed(request._request)
if method.upper() not in view.allowed_methods:
if hasattr(view, 'get_queryset'):
view.http_method_not_allowed(request._request)
queryset = view.get_queryset()
if request.user.is_authenticated and method == 'options':
self.obj = queryset.model
return True
elif hasattr(view, 'queryset'):
if hasattr(view, 'get_queryset'):
if view.queryset.model._meta:
self.obj = view.queryset.model
queryset = view.get_queryset()
object_organization = None
self.obj = queryset.model
if method == 'get':
elif hasattr(view, 'queryset'):
action = 'view'
elif method == 'post':
if view.queryset.model._meta:
action = 'add'
self.obj = view.queryset.model
if 'organization' in request.data:
object_organization = None
if not request.data['organization']:
raise ValidationError('you must provide an organization')
if method == 'get':
object_organization = int(request.data['organization'])
elif method == 'patch':
action = 'view'
elif method == 'post':
action = 'change'
action = 'add'
elif method == 'put':
if 'organization' in request.data:
action = 'change'
if not request.data['organization']:
raise centurion_exceptions.ValidationError('you must provide an organization')
elif method == 'delete':
object_organization = int(request.data['organization'])
elif method == 'patch':
action = 'delete'
action = 'change'
else:
elif method == 'put':
action = 'view'
action = 'change'
permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name
elif method == 'delete':
self.permission_required = [ permission ]
action = 'delete'
if hasattr(view, 'get_dynamic_permissions'):
else:
self.permission_required = view.get_dynamic_permissions()
action = 'view'
if hasattr(self, 'obj'):
permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name
self.permission_required = [ permission ]
if hasattr(view, 'get_dynamic_permissions'):
self.permission_required = view.get_dynamic_permissions()
if view:
if 'organization_id' in view.kwargs:
if view.kwargs['organization_id']:
object_organization = view.kwargs['organization_id']
if object_organization is None and 'pk' in view.kwargs:
try:
self.obj = view.queryset.get(pk=view.kwargs['pk']) # Here
except ObjectDoesNotExist:
return False
if view:
if 'organization_id' in view.kwargs:
if obj:
if view.kwargs['organization_id']:
if obj.get_organization():
object_organization = view.kwargs['organization_id']
object_organization = obj.get_organization().id
if object_organization is None and 'pk' in view.kwargs:
if hasattr(self.obj, 'is_global'):
if obj.is_global:
try:
self.obj = view.queryset.get(pk=view.kwargs['pk']) # Here
except ObjectDoesNotExist:
return False
object_organization = 0
if obj:
if 'pk' in view.kwargs:
if obj.get_organization():
if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']:
object_organization = obj.get_organization().id
if hasattr(self.obj, 'is_global'):
if obj.is_global:
object_organization = 0
if 'pk' in view.kwargs:
if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']:
object_organization = view.kwargs['pk']
object_organization = view.kwargs['pk']
if object_organization is None:
@ -137,35 +146,43 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
return False
if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs:
if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs:
if self.obj.get_organization():
if self.obj.get_organization():
object_organization = self.obj.get_organization().id
object_organization = self.obj.get_organization().id
if hasattr(self.obj, 'is_global'):
if hasattr(self.obj, 'is_global'):
if self.obj.is_global:
if self.obj.is_global:
object_organization = 0
object_organization = 0
# ToDo: implement proper checking of listview as this if allows ALL.
if 'pk' not in view.kwargs and method == 'get' and object_organization is None:
# ToDo: implement proper checking of listview as this if allows ALL.
if 'pk' not in view.kwargs and method == 'get' and object_organization is None:
return True
return True
if hasattr(self, 'default_organization'):
object_organization = self.default_organization
if hasattr(self, 'default_organization'):
object_organization = self.default_organization
if method == 'post' and hasattr(self, 'default_organization'):
if method == 'post' and hasattr(self, 'default_organization'):
if self.default_organization:
if self.default_organization:
object_organization = self.default_organization.id
object_organization = self.default_organization.id
if not self.has_organization_permission(object_organization) and not request.user.is_superuser:
if not self.has_organization_permission(object_organization) and not request.user.is_superuser:
raise PermissionDenied('You are not part of this organization')
raise PermissionDenied('You are not part of this organization')
except centurion_exceptions.MethodNotAllowed as e:
raise centurion_exceptions.MethodNotAllowed( str(method).upper() )
except Exception as e:
return False
return True

View File

@ -1,12 +1,14 @@
from django.utils.safestring import mark_safe
from drf_spectacular.utils import extend_schema
from rest_framework import generics, permissions, routers, views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
@extend_schema(deprecated=True)
class Index(views.APIView):
permission_classes = [
@ -28,7 +30,7 @@ class Index(views.APIView):
def get(self, request, *args, **kwargs):
body: dict = {
'projects': reverse('API:_api_projects-list', request=request)
'projects': reverse('v1:_api_projects-list', request=request)
}
return Response(body)

View File

@ -9,7 +9,7 @@ from api.serializers.project_management.project_milestone import ProjectMileston
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated = True )
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -9,7 +9,7 @@ from api.views.core.tickets import View
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated = True )
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -6,6 +6,7 @@ from api.views.core.tickets import View
@extend_schema(deprecated=True)
class View(View):
_ticket_type:str = 'project_task'

View File

@ -8,7 +8,7 @@ from api.serializers.project_management.project_type import ProjectType, Project
from api.views.mixin import OrganizationPermissionAPI
@extend_schema(deprecated = True )
class View(OrganizationMixin, viewsets.ModelViewSet):
permission_classes = [

View File

@ -16,7 +16,7 @@ from project_management.models.projects import Project
from settings.models.user_settings import UserSettings
@extend_schema(deprecated = True )
class View(OrganizationMixin, viewsets.ModelViewSet):
filterset_fields = [
@ -39,12 +39,18 @@ class View(OrganizationMixin, viewsets.ModelViewSet):
def get_serializer_class(self):
if self.has_organization_permission(
organization = UserSettings.objects.get(user = self.request.user).default_organization,
permissions_required = ['project_management.import_project']
) or self.request.user.is_superuser:
user_default_organization = UserSettings.objects.get(user = self.request.user).default_organization
return ProjectImportSerializer
if user_default_organization:
if hasattr(user_default_organization, 'default_organization'):
if self.has_organization_permission(
organization = user_default_organization.default_organization.id,
permissions_required = ['project_management.import_project']
) or self.request.user.is_superuser:
return ProjectImportSerializer
return ProjectSerializer

View File

@ -10,7 +10,7 @@ from rest_framework.reverse import reverse
from core.http.common import Http
@extend_schema( deprecated = True )
class View(views.APIView):
permission_classes = [
@ -37,11 +37,11 @@ class View(views.APIView):
status = Http.Status.OK
response_data: dict = {
"permissions": reverse('API:_settings_permissions', request=request),
"project_state": reverse('API:_api_project_state-list', request=request),
"project_type": reverse('API:_api_project_type-list', request=request),
"ticket_categories": reverse('API:_api_ticket_category-list', request=request),
"ticket_comment_categories": reverse('API:_api_ticket_comment_category-list', request=request)
"permissions": reverse('v1:_settings_permissions', request=request),
"project_state": reverse('v1:_api_project_state-list', request=request),
"project_type": reverse('v1:_api_project_type-list', request=request),
"ticket_categories": reverse('v1:_api_ticket_category-list', request=request),
"ticket_comment_categories": reverse('v1:_api_ticket_comment_category-list', request=request)
}
return Response(data=response_data,status=status)

View File

@ -9,7 +9,7 @@ from access.functions import permissions
from core.http.common import Http
@extend_schema(deprecated=True)
class View(views.APIView):
permission_classes = [

602
app/api/viewsets/common.py Normal file
View File

@ -0,0 +1,602 @@
from django.utils.safestring import mark_safe
from rest_framework import viewsets
from rest_framework.exceptions import APIException
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from access.mixin import OrganizationMixin
from api.auth import TokenScheme
from api.react_ui_metadata import ReactUIMetadata
from api.views.mixin import OrganizationPermissionAPI
class CommonViewSet(
OrganizationMixin,
viewsets.ViewSet
):
"""Common ViewSet class
This class is to be inherited by ALL viewsets.
Args:
OrganizationMixin (class): Contains the Authorization checks.
viewsets (class): Django Rest Framework base class.
"""
@property
def allowed_methods(self):
"""Allowed HTTP Methods
_Optional_, HTTP Methods allowed for the `viewSet`.
Returns:
list: Allowed HTTP Methods
"""
return super().allowed_methods
documentation: str = None
""" Viewset Documentation URL
_Optional_, if specified will be add to list view metadata
"""
metadata_class = ReactUIMetadata
""" Metadata Class
_Mandatory_, required so that the HTTP/Options method is populated with the data
required to generate the UI.
"""
model_documentation: str = None
"""Model Documentation URL
_Optional_, if specified will be add to detail view metadata"""
page_layout: list = []
""" Page layout class
_Optional_, used by metadata to add the page layout to the HTTP/Options method
for detail view, Enables the UI can setup the page layout.
"""
permission_classes = [ OrganizationPermissionAPI ]
"""Permission Class
_Mandatory_, Permission check class
"""
table_fields: list = []
""" Table layout list
_Optional_, used by metadata for the table fields and added to the HTTP/Options
method for detail view, Enables the UI can setup the table.
"""
view_description: str = None
view_name: str = None
def get_model_documentation(self):
if not self.model_documentation:
if hasattr(self.model, 'documentataion'):
self.model_documentation = self.model.documentation
else:
self.model_documentation = ''
return self.model_documentation
def get_page_layout(self):
if len(self.page_layout) < 1:
if hasattr(self, 'model'):
if hasattr(self.model, 'page_layout'):
self.page_layout = self.model.page_layout
else:
self.page_layout = []
return self.page_layout
def get_table_fields(self):
if len(self.table_fields) < 1:
if hasattr(self, 'model'):
if hasattr(self.model, 'table_fields'):
self.table_fields = self.model.table_fields
else:
self.table_fields = []
return self.table_fields
def get_view_description(self, html=False) -> str:
if not self.view_description:
self.view_description = ""
if html:
return mark_safe(f"<p>{self.view_description}</p>")
else:
return self.view_description
def get_view_name(self):
if hasattr(self, 'model'):
if self.detail:
return self.model._meta.verbose_name
return self.model._meta.verbose_name_plural
if not self.view_name:
return 'Error'
return self.view_name
class ModelViewSetBase(
CommonViewSet
):
filterset_fields: list = []
"""Fields to use for filtering the query
_Optional_, if specified, these fields can be used to filter the API response
"""
model: object = None
"""Django Model
_Mandatory_, Django model used for this view.
"""
queryset: object = None
"""View Queryset
_Optional_, View model Query
"""
search_fields:list = []
""" Search Fields
_Optional_, Used by API text search as the fields to search.
"""
def get_queryset(self):
if not self.queryset:
queryset = self.model.objects.all()
if 'pk' in self.kwargs:
if self.kwargs['pk']:
queryset = queryset.filter( pk = int( self.kwargs['pk'] ) )
self.queryset = queryset
return self.queryset
def get_serializer_class(self):
if (
self.action == 'list'
or self.action == 'retrieve'
):
return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']
class ModelViewSet(
ModelViewSetBase,
viewsets.ModelViewSet,
):
def retrieve(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().retrieve(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
class ModelCreateViewSet(
ModelViewSetBase,
viewsets.mixins.CreateModelMixin,
):
def create(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().create(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
class ModelListRetrieveDeleteViewSet(
viewsets.mixins.ListModelMixin,
viewsets.mixins.RetrieveModelMixin,
viewsets.mixins.DestroyModelMixin,
viewsets.GenericViewSet,
ModelViewSetBase
):
""" Use for models that you wish to delete and view ONLY!"""
def list(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().list(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
def retrieve(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().retrieve(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
def destroy(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().destroy(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
class ModelRetrieveUpdateViewSet(
viewsets.mixins.RetrieveModelMixin,
viewsets.mixins.UpdateModelMixin,
viewsets.GenericViewSet,
ModelViewSetBase
):
""" Use for models that you wish to update and view ONLY!"""
def partial_update(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().partial_update(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
def update(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().update(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
class ReadOnlyModelViewSet(
viewsets.ReadOnlyModelViewSet,
ModelViewSetBase
):
permission_classes = [
IsAuthenticated,
]
def retrieve(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().retrieve(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response
def list(self, request, *args, **kwargs):
"""Sainty override
This function overrides the function of the same name
in the parent class for the purpose of ensuring a
non-api exception will not have the API return a HTTP
500 error.
This function is a sanity check that if it triggers,
(an exception occured), the user will be presented with
a stack trace that they will hopefully report as a bug.
HTTP status set to HTTP/501 so it's distinguishable from
a HTTP/500 which is generally a random error that has not
been planned for. i.e. uncaught exception
"""
response = None
try:
response = super().list(request = request, *args, **kwargs)
except Exception as e:
if not isinstance(e, APIException):
response = Response(
data = {
'server_error': str(e)
},
status = 501
)
return response

38
app/api/viewsets/index.py Normal file
View File

@ -0,0 +1,38 @@
from drf_spectacular.utils import extend_schema
from rest_framework.response import Response
from rest_framework.reverse import reverse
from api.viewsets.common import CommonViewSet
@extend_schema(exclude = True)
class Index(CommonViewSet):
allowed_methods: list = [
'GET',
'HEAD',
'OPTIONS'
]
view_description = 'Centurion ERP API V2.'
view_name = "v2"
def list(self, request, *args, **kwargs):
return Response(
{
"access": reverse('v2:_api_v2_access_home-list', request=request),
"assistance": reverse('v2:_api_v2_assistance_home-list', request=request),
"docs": reverse('v2:_api_v2_docs', request=request),
"base": reverse('v2:_api_v2_base_home-list', request=request),
"itam": reverse('v2:_api_v2_itam_home-list', request=request),
"itim": reverse('v2:_api_v2_itim_home-list', request=request),
"config_management": reverse('v2:_api_v2_config_management_home-list', request=request),
"project_management": reverse('v2:_api_v2_project_management_home-list', request=request),
"settings": reverse('v2:_api_v2_settings_home-list', request=request)
}
)

View File

@ -0,0 +1,38 @@
import zoneinfo
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
def _activate_tz(tz):
timezone.activate(zoneinfo.ZoneInfo(tz))
# tzname = request.session.get("django_timezone", None)
# if tzname:
# _activate_tz(tzname)
# else:
user = request.user
if hasattr(user, 'user_settings'):
tzname = user.user_settings.all()[0].timezone
# set the cookie
# request.session['django_timezone'] = tzname
_activate_tz(tzname)
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -0,0 +1,69 @@
from django.contrib.auth.models import ContentType
from rest_framework import serializers
from rest_framework.reverse import reverse
class ContentTypeBaseSerializer(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_content_type-detail", format="html"
)
class Meta:
model = ContentType
fields = '__all__'
fields = [
'id',
'display_name',
'url'
]
read_only_fields = [
'id',
'display_name',
'url'
]
class ContentTypeViewSerializer(ContentTypeBaseSerializer):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': reverse("v2:_api_v2_content_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
}
class Meta:
model = ContentType
fields = [
'id',
'app_label',
'model',
'_urls',
]
read_only_fields = [
'id',
'app_label',
'model',
'_urls',
]

View File

@ -0,0 +1,77 @@
from django.contrib.auth.models import Permission
from rest_framework import serializers
from rest_framework.reverse import reverse
from app.serializers.content_type import ContentTypeBaseSerializer
class PermissionBaseSerializer(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_permission-detail", format="html"
)
class Meta:
model = Permission
fields = '__all__'
fields = [
'id',
'display_name',
'url'
]
read_only_fields = [
'id',
'display_name',
'url'
]
class PermissionViewSerializer(PermissionBaseSerializer):
content_type = ContentTypeBaseSerializer()
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': reverse("v2:_api_v2_permission-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
}
class Meta:
model = Permission
fields = [
'id',
'name',
'display_name',
'codename',
'content_type',
'_urls',
]
read_only_fields = [
'id',
'name',
'display_name',
'codename',
'content_type',
'_urls',
]

View File

@ -0,0 +1,44 @@
from django.contrib.auth.models import User
from rest_framework import serializers
class UserBaseSerializer(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_user-detail", format="html"
)
class Meta:
model = User
fields = '__all__'
fields = [
'id',
'display_name',
'first_name',
'last_name',
'username',
'is_active',
'url'
]
read_only_fields = [
'id',
'display_name',
'first_name',
'last_name',
'username',
'is_active',
'url'
]

Some files were not shown because too many files have changed in this diff Show More