Compare commits

..

68 Commits

Author SHA1 Message Date
bbf6d27839 Merge branch 'development' 2025-07-03 22:55:52 +00:00
Jon
810ae2354b Merge pull request #853 from nofusscomputing/development 2025-07-04 08:25:35 +09:30
b689016e07 Merge branch 'development' 2025-06-02 00:05:21 +00:00
Jon
89597bacd0 Merge pull request #784 from nofusscomputing/development 2025-06-02 09:35:05 +09:30
d38a44bc99 Merge branch 'development' 2025-05-16 10:31:56 +00:00
Jon
cebb5a52c8 Merge pull request #763 from nofusscomputing/development 2025-05-16 20:01:42 +09:30
4b5bca7322 Merge branch 'development' 2025-05-04 10:50:46 +00:00
Jon
478518ff6f Merge pull request #739 from nofusscomputing/development 2025-05-04 20:20:30 +09:30
4b7cbf4973 Merge branch 'development' 2025-04-10 05:12:46 +00:00
Jon
1273fbdef0 Merge pull request #717 from nofusscomputing/development 2025-04-10 14:42:29 +09:30
4ca56f00c8 Merge branch 'development' 2025-04-10 03:21:55 +00:00
Jon
b3e086a537 Merge pull request #716 from nofusscomputing/development 2025-04-10 12:51:37 +09:30
697322faef Merge branch 'development' 2025-03-29 05:22:03 +00:00
Jon
06db98deb1 Merge pull request #701 from nofusscomputing/development 2025-03-29 14:51:45 +09:30
6af5996601 Merge branch 'development' 2025-03-17 04:16:29 +00:00
Jon
2b0a5309dd Merge pull request #689 from nofusscomputing/development 2025-03-17 13:46:13 +09:30
69ee41deea Merge branch 'development' 2025-03-16 02:34:26 +00:00
Jon
3908a1f919 Merge pull request #680 from nofusscomputing/development 2025-03-16 12:04:09 +09:30
e04c819b74 Merge branch 'development' 2025-03-01 10:43:29 +00:00
Jon
3a496011da Merge pull request #658 from nofusscomputing/development 2025-03-01 20:13:14 +09:30
48e27deb9c Merge branch 'development' 2025-02-21 11:36:37 +00:00
Jon
9cba4e234c Merge pull request #644 from nofusscomputing/development 2025-02-21 21:06:19 +09:30
c8201afb7b Merge branch 'development' 2025-02-14 12:24:22 +00:00
Jon
8765888733 Merge pull request #604 from nofusscomputing/development 2025-02-14 21:54:02 +09:30
2c97098b66 Merge branch 'development' 2025-02-10 09:27:29 +00:00
Jon
239bfa196a Merge pull request #559 from nofusscomputing/development 2025-02-10 18:57:09 +09:30
0215eda6d4 Merge branch 'development' 2025-02-06 12:29:12 +00:00
Jon
2eda95f729 Merge pull request #521 from nofusscomputing/development 2025-02-06 21:58:50 +09:30
bdcf452901 Merge branch 'development' 2025-01-23 08:27:44 +00:00
Jon
8a63728254 Merge pull request #482 from nofusscomputing/development 2025-01-23 17:57:28 +09:30
8f37e2e0b8 Merge branch 'development' 2025-01-04 10:53:45 +00:00
Jon
5c3ea53f99 Merge pull request #464 from nofusscomputing/development 2025-01-04 20:23:26 +09:30
037a5c80d3 Merge branch 'development' 2024-12-23 07:50:40 +00:00
Jon
bc16498533 Merge pull request #446 from nofusscomputing/development 2024-12-23 17:20:25 +09:30
e982d65974 Merge branch 'development' 2024-12-09 15:10:30 +00:00
Jon
b76168b149 Merge pull request #420 from nofusscomputing/development 2024-12-10 00:40:05 +09:30
ef12eb9a10 Merge branch 'development' 2024-11-30 06:12:30 +00:00
Jon
1a22b7fc90 Merge pull request #413 from nofusscomputing/development 2024-11-30 15:42:09 +09:30
49df68fdc4 Merge branch 'development' 2024-11-28 15:54:18 +00:00
Jon
746edacd55 Merge pull request #402 from nofusscomputing/development
chore: release v1.4
2024-11-29 01:24:01 +09:30
dfafe10089 Merge branch 'development' 2024-11-27 16:32:02 +00:00
Jon
b61b969994 Merge pull request #400 from nofusscomputing/development 2024-11-28 02:01:45 +09:30
bc731c1c37 Merge branch 'development' 2024-10-31 06:44:58 +00:00
Jon
4a79563def Merge pull request #372 from nofusscomputing/development 2024-10-31 16:14:36 +09:30
4e23d4a9c0 Merge branch 'development' 2024-10-29 13:00:56 +00:00
Jon
f0c01b9ede Merge pull request #370 from nofusscomputing/development 2024-10-29 22:30:37 +09:30
45183f7667 Merge branch 'development' 2024-10-22 09:16:32 +00:00
Jon
c3a4685200 Merge pull request #358 from nofusscomputing/development 2024-10-22 18:46:17 +09:30
c24943ca86 Merge branch 'development' 2024-10-11 14:59:17 +00:00
Jon
c59e64cbd1 Merge pull request #342 from nofusscomputing/development 2024-10-12 00:29:00 +09:30
c653197ef7 Merge branch 'development' 2024-08-23 08:26:38 +00:00
Jon
37c31a2626 Merge pull request #255 from nofusscomputing/development 2024-08-23 17:56:17 +09:30
b11966548e Merge branch 'development' 2024-08-23 08:03:32 +00:00
Jon
ecdc30674b Merge pull request #209 from nofusscomputing/development 2024-08-23 17:33:12 +09:30
b47dfbf28c Merge branch 'development' 2024-07-14 06:25:41 +00:00
Jon
9120aa0fd1 Merge branch 'development' into 'master'
chore: release 0.7.0

See merge request nofusscomputing/projects/centurion_erp!35
2024-07-14 06:11:30 +00:00
91663394cf Merge branch 'development' 2024-06-30 07:13:43 +00:00
Jon
4ab4ac2984 Merge branch 'development' into 'master'
chore: release 0.6.0

See merge request nofusscomputing/projects/django_template!29
2024-06-30 07:02:29 +00:00
74a021d207 Merge branch 'development' 2024-06-17 15:14:28 +00:00
Jon
5e893a25aa Merge branch 'development' into 'master'
chore: release 0.5.0

See merge request nofusscomputing/projects/django_template!21
2024-06-17 15:03:03 +00:00
551d884cba Merge branch 'development' 2024-06-05 21:36:02 +00:00
Jon
a79adb0bdd Merge branch 'development' into 'master'
chore: release 0.4.0

See merge request nofusscomputing/projects/django_template!19
2024-06-05 21:24:09 +00:00
1513216301 Merge branch 'development' 2024-05-29 01:32:39 +00:00
Jon
9fd1eac7f2 Merge branch 'development' into 'master'
chore: release 0.3.0

See merge request nofusscomputing/projects/django_template!14
2024-05-29 01:23:28 +00:00
a51b426bd3 Merge branch 'development' 2024-05-18 18:26:44 +00:00
Jon
6ca1488b54 Merge branch 'development' into 'master'
chore: release

See merge request nofusscomputing/projects/django_template!7
2024-05-18 18:25:36 +00:00
b3a9c3a879 Merge branch 'development' 2024-05-17 03:08:20 +00:00
Jon
a6d6702317 Merge branch 'development' into 'master'
chore: release 0.1.0

See merge request nofusscomputing/projects/django_template!4
2024-05-17 03:07:11 +00:00
1748 changed files with 78107 additions and 48604 deletions

View File

@ -39,16 +39,35 @@ Describe in detail the following:
- [ ] 🔗 URL Route Added
- [ ] 🏷️ [Model tag]().
- [ ] 🏷️ Model tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()` function
- [ ] 📘 Tag updated in the [docs](https://nofusscomputing.com/projects/centurion_erp/user/core/markdown/#model-reference)
- [ ] tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()`
- [ ] ⚒️ Migration _Ticket Linked Item item_type choices update_
- [ ] tag added to class
>[!note]
> Ensure that when creating the tag the following is adhered to:
> - Two words are not to contain a space char, `\s`. It is to be replaced with an underscore `_`
> - As much as practical, keep the tag as close to the model name as possible
- [ ] 📝 New [History model](https://nofusscomputing.com/projects/centurion_erp/development/core/model_history/) created
- Sub-Models **_ONLY_**
- [ ] Model class variable [`history_app_label`](https://nofusscomputing.com/projects/centurion_erp/development/models/#history) set to correct application label
- [ ] Model class variable [`history_model_name`](https://nofusscomputing.com/projects/centurion_erp/development/models/#history) set to correct model label
- [ ] 📓 New [Notes model](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/) created
- [ ] 🆕 Model Created
- [ ] 🛠️ Migrations added
- [ ] Add `app_label` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().model_apps`
- [ ] _(Notes not used/required) -_ Add `model_name` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().excluded_models`
- [ ] 🧪 [Unit tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
- [ ] 🧪 [Functional tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
- [ ] Admin Documentation added/updated _if applicable_
- [ ] Developer Documentation added/updated _if applicable_
- [ ] User Documentation added/updated
---
@ -60,13 +79,14 @@ Describe in detail the following:
### 🧪 Tests
- Unit Tests
- [ ] API Render (fields)
- [ ] [Model](https://nofusscomputing.com/projects/centurion_erp/development/models/#tests)
- [ ] ViewSet
- [ ] Serializer
- Function Test
- [ ] History API Render (fields)
- [ ] History Entries
- [ ] API Metadata
- [ ] API Permissions
- [ ] API Render (fields)
- [ ] Model
- [ ] Serializer
- [ ] ViewSet

View File

@ -8,6 +8,5 @@
"qwtel.sqlite-viewer",
"jebbs.markdown-extended",
"william-voyek.vscode-nginx",
"detachhead.basedpyright",
]
}

25
.vscode/launch.json vendored
View File

@ -29,7 +29,7 @@
"3",
"--bind",
"0.0.0.0:8002",
"centurion.wsgi:application",
"app.wsgi:application",
],
"django": true,
"autoStartBrowser": false,
@ -50,29 +50,6 @@
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Centurion Model (Management Command)",
"type": "debugpy",
"request": "launch",
"args": [
"models",
// "0.0.0.0:8002"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Make Migrations",
"type": "debugpy",
"request": "launch",
"args": [
"makemigrations"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Migrate",
"type": "debugpy",

11
.vscode/settings.json vendored
View File

@ -6,7 +6,6 @@
],
"python.testing.pytestArgs": [
"--override-ini", "addopts=",
"--no-migrations",
"app",
],
"python.testing.unittestEnabled": false,
@ -25,14 +24,4 @@
"green": 90
},
"telemetry.feedback.enabled": false,
"python.languageServer": "None",
"debug.javascript.enableNetworkView": false,
"typescript.experimental.expandableHover": false,
"ipynb.experimental.serialization": false,
"notebook.experimental.generate": false,
"extensions.experimental.issueQuickAccess": false,
"workbench.commandPalette.experimental.enableNaturalLanguageSearch": false,
"multiDiffEditor.experimental.enabled": false,
"diffEditor.experimental.showEmptyDecorations": false,
"editor.experimental.asyncTokenization": false,
}

View File

@ -1,28 +1,3 @@
## Version 1.18.0
- Added new model for History
!!! info
Migration of the old history tables to the new history tables occurs as part of post migration. As such the time it will take to migrate the history is dependent upon how many history entries per model. This should be planned for when upgrading to this version. if for some reason the migration is interrupted, you can safely restart it again by running the migrate command.
!!! note
Permission migration from the old history models to the new Audit History models are not migrated. As such users whom used to be able to access history models will need to be granted the required permission to view the new Audit History models
- Added new model for notes
!!! info
Migration of the old notes tables to the new note tables occurs as part of post migration. As such the time it will take to migrate the history is dependent upon how many history entries per model. This should be planned for when upgrading to this version. if for some reason the migration is interrupted, you can safely restart it again by running the migrate command.
!!! note
Permission migration from the old history models to the new Centurion Notes models are not migrated. As such users whom used to be able to access notes models will need to be granted the required permission to view the new Centurion Notes models
- Removed Django UI
[UI](https://github.com/nofusscomputing/centurion_erp) must be deployed seperatly.
- Removed API v1
## Version 1.17.0
- Added setting for log files.

View File

@ -1,7 +1,6 @@
from django.db import models
from django.utils.timezone import now
from django.template.defaultfilters import slugify
class AutoCreatedField(models.DateTimeField):
"""
@ -51,7 +50,7 @@ class AutoLastModifiedField(AutoCreatedField):
def pre_save(self, model_instance, add):
value = now().replace(microsecond=0)
value = now()
setattr(model_instance, self.attname, value)

View File

@ -0,0 +1,38 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models.tenant import Tenant as Organization
from core.forms.common import CommonModelForm
class OrganizationForm(CommonModelForm):
class Meta:
model = Organization
fields = [
'name',
'manager',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)

69
app/access/forms/team.py Normal file
View File

@ -0,0 +1,69 @@
from django import forms
from django.db.models import Q
from django.forms import inlineformset_factory
from .team_users import TeamUsersForm, TeamUsers
from access.models.team import Team
from access.functions import permissions
from app import settings
from core.forms.common import CommonModelForm
TeamUserFormSet = inlineformset_factory(
model=TeamUsers,
parent_model= Team,
extra = 1,
fields=[
'user',
'manager'
]
)
class TeamFormAdd(CommonModelForm):
class Meta:
model = Team
fields = [
'team_name',
'model_notes',
]
class TeamForm(CommonModelForm):
class Meta:
model = Team
fields = [
'team_name',
'permissions',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
self.fields['permissions'].queryset = permissions.permission_queryset()

View File

@ -0,0 +1,16 @@
from django.db.models import Q
from app import settings
from access.models.team_user import TeamUsers
from core.forms.common import CommonModelForm
class TeamUsersForm(CommonModelForm):
class Meta:
model = TeamUsers
fields = [
'user',
'manager',
]

View File

@ -1,10 +1,4 @@
from django.apps import apps
from django.contrib.auth.models import (
ContentType,
Permission
)
from django.conf import settings
from django.contrib.auth.models import Permission
def permission_queryset():
"""Filter Permissions to those used within the application
@ -13,7 +7,7 @@ def permission_queryset():
list: Filtered queryset that only contains the used permissions
"""
centurion_apps = [
apps = [
'access',
'accounting',
'assistance',
@ -58,50 +52,10 @@ def permission_queryset():
'view_history',
]
if not settings.RUNNING_TESTS:
models = apps.get_models()
for model in models:
if(
not str(model._meta.object_name).endswith('AuditHistory')
and not str(model._meta.model_name).lower().endswith('history')
):
# check `endswith('history')` can be removed when the old history models are removed
continue
content_type = ContentType.objects.get(
app_label = model._meta.app_label,
model = model._meta.model_name
)
permissions = Permission.objects.filter(
content_type = content_type,
)
for permission in permissions:
if(
not permission.codename == 'view_' + str(model._meta.model_name)
and str(model._meta.object_name).endswith('AuditHistory')
):
exclude_permissions += [ permission.codename ]
elif(
not str(model._meta.object_name).endswith('AuditHistory')
and str(model._meta.model_name).lower().endswith('history')
):
# This `elif` can be removed when the old history models are removed
exclude_permissions += [ permission.codename ]
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in = centurion_apps,
content_type__app_label__in=apps,
).exclude(
content_type__model__in = exclude_models
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -1,10 +1,15 @@
import django
from django.contrib.auth.middleware import (
AuthenticationMiddleware,
SimpleLazyObject,
partial,
)
from django.contrib.auth.models import Group
from django.utils.deprecation import MiddlewareMixin
from access.models.tenant import Tenant
from access.models.tenant import Tenant as Organization
from access.models.team import Team
@ -23,9 +28,9 @@ class RequestTenancy(MiddlewareMixin):
def process_request(self, request):
request.app_settings = AppSettings.objects.select_related('global_organization').filter(
request.app_settings = AppSettings.objects.select_related('global_organization').get(
owner_organization = None
)[0]
)
request.tenancy = Tenancy(user = request.user, app_settings = request.app_settings)
@ -40,8 +45,8 @@ class Tenancy:
_app_settings: AppSettings = None
_user_organizations: list([Tenant]) = None
"""Cached User Tenants"""
_user_organizations: list([Organization]) = None
"""Cached User Organizations"""
_user_teams: list([Team]) = None
"""Cached User Teams"""
@ -90,7 +95,7 @@ class Tenancy:
def is_member(self, organization: Tenant) -> bool:
def is_member(self, organization: Organization) -> bool:
"""Returns true if the current user is a member of the organization
iterates over the user_organizations list and returns true if the user is a member
@ -113,11 +118,11 @@ class Tenancy:
def has_organization_permission(self, organization: Tenant, permissions_required: str) -> bool:
def has_organization_permission(self, organization: Organization, permissions_required: str) -> bool:
""" Check if user has permission within organization.
Args:
organization (int): Tenant to check.
organization (int): Organization to check.
permissions_required (list): if doing object level permissions, pass in required permission.
Returns:
@ -126,9 +131,9 @@ class Tenancy:
has_permission: bool = False
if type(organization) is not Tenant:
if type(organization) is not Organization:
raise TypeError('Tenant must be of type Tenant')
raise TypeError('Organization must be of type Organization')
if type(permissions_required) is not str:

View File

@ -1,110 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-06 01:41
import access.models.tenancy_abstract
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0010_company_alter_entity_entity_type_alter_person_dob_and_more"),
("core", "0028_delete_history"),
]
operations = [
migrations.RemoveField(
model_name="team",
name="is_global",
),
migrations.AlterField(
model_name="team",
name="model_notes",
field=models.TextField(
blank=True,
help_text="Tid bits of information",
null=True,
verbose_name="Notes",
),
),
migrations.AlterField(
model_name="team",
name="organization",
field=models.ForeignKey(
help_text="Tenant this belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.tenant",
validators=[
access.models.tenancy_abstract.TenancyAbstractModel.validatate_organization_exists
],
verbose_name="Tenant",
),
),
migrations.CreateModel(
name="TeamAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_history",
to="access.team",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Team History",
"verbose_name_plural": "Team Histories",
"db_table": "access_team_audithistory",
"managed": True,
},
bases=("core.centurionaudit", models.Model),
),
migrations.CreateModel(
name="TeamCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.team",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Team Note",
"verbose_name_plural": "Team Notes",
"db_table": "access_team_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote", models.Model),
)
]

View File

@ -1,102 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-06 01:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0011_remove_team_is_global_model_notes_and_more"),
("core", "0028_delete_history"),
]
operations = [
migrations.AddField(
model_name="teamusers",
name="model_notes",
field=models.TextField(
blank=True,
help_text="Tid bits of information",
null=True,
verbose_name="Notes",
),
),
migrations.AlterField(
model_name="teamusers",
name="id",
field=models.AutoField(
help_text="ID of the item",
primary_key=True,
serialize=False,
unique=True,
verbose_name="ID",
),
),
migrations.CreateModel(
name="TeamUsersAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_history",
to="access.teamusers",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Team User History",
"verbose_name_plural": "Team User Histories",
"db_table": "access_teamusers_audithistory",
"managed": True,
},
bases=("core.centurionaudit", models.Model),
),
migrations.CreateModel(
name="TeamUsersCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.teamusers",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Team User Note",
"verbose_name_plural": "Team User Notes",
"db_table": "access_teamusers_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote", models.Model),
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-06 05:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("access", "0012_teamusers_model_notes_alter_teamusers_id_and_more"),
]
operations = [
migrations.DeleteModel(
name="TeamUsersAuditHistory",
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-07 09:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("access", "0013_delete_teamusersaudithistory"),
]
operations = [
migrations.DeleteModel(
name="TeamUsersCenturionModelNote",
),
]

View File

@ -1,75 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-07 10:10
import access.models.team
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0014_delete_teamuserscenturionmodelnote"),
]
operations = [
migrations.RemoveField(
model_name="teamcenturionmodelnote",
name="centurionmodelnote_ptr",
),
migrations.RemoveField(
model_name="teamcenturionmodelnote",
name="model",
),
migrations.RemoveField(
model_name="teamusers",
name="model_notes",
),
migrations.AddField(
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="Tenant this belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="access.tenant",
validators=[access.models.team.Team.validatate_organization_exists],
verbose_name="Tenant",
),
),
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.DeleteModel(
name="TeamAuditHistory",
),
migrations.DeleteModel(
name="TeamCenturionModelNote",
),
]

View File

@ -1,112 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-08 04:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"access",
"0015_remove_teamcenturionmodelnote_centurionmodelnote_ptr_and_more",
),
("core", "0031_remove_ticketcategory_is_global_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name="tenant",
name="slug",
),
migrations.AlterField(
model_name="tenant",
name="manager",
field=models.ForeignKey(
blank=True,
help_text="Manager for this Tenancy",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
verbose_name="Manager",
),
),
migrations.AlterField(
model_name="tenant",
name="model_notes",
field=models.TextField(
blank=True,
help_text="Tid bits of information",
null=True,
verbose_name="Notes",
),
),
migrations.CreateModel(
name="TenantAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_history",
to="access.tenant",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Tenant History",
"verbose_name_plural": "Tenant Histories",
"db_table": "access_tenant_audithistory",
"managed": True,
},
bases=("core.centurionaudit", models.Model),
),
migrations.CreateModel(
name="TenantCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.tenant",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Tenant Note",
"verbose_name_plural": "Tenant Notes",
"db_table": "access_tenant_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote", models.Model),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-17 07:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("access", "0016_remove_tenant_slug_alter_tenant_manager_and_more"),
("core", "0033_alter_ticketcommentcategory_parent_and_more"),
]
operations = [
migrations.DeleteModel(
name="EntityHistory",
),
migrations.DeleteModel(
name="EntityNotes",
),
]

View File

@ -1,253 +0,0 @@
# Generated by Django 5.1.9 on 2025-06-17 07:32
import access.models.tenancy_abstract
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0017_remove_entitynotes_model_and_more"),
("core", "0033_alter_ticketcommentcategory_parent_and_more"),
]
operations = [
migrations.RemoveField(
model_name="entity",
name="is_global",
),
migrations.AlterField(
model_name="entity",
name="id",
field=models.AutoField(
help_text="ID of the item",
primary_key=True,
serialize=False,
unique=True,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="entity",
name="model_notes",
field=models.TextField(
blank=True,
help_text="Tid bits of information",
null=True,
verbose_name="Notes",
),
),
migrations.AlterField(
model_name="entity",
name="organization",
field=models.ForeignKey(
help_text="Tenant this belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.tenant",
validators=[
access.models.tenancy_abstract.TenancyAbstractModel.validatate_organization_exists
],
verbose_name="Tenant",
),
),
migrations.CreateModel(
name="ContactAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.contact",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Contact History",
"verbose_name_plural": "Contact Histories",
"db_table": "access_contact_audithistory",
"managed": True,
},
bases=("core.centurionaudit",),
),
migrations.CreateModel(
name="ContactCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.contact",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Contact Note",
"verbose_name_plural": "Contact Notes",
"db_table": "access_contact_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote",),
),
migrations.CreateModel(
name="EntityAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_history",
to="access.entity",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Entity History",
"verbose_name_plural": "Entity Histories",
"db_table": "access_entity_audithistory",
"managed": True,
},
bases=("core.centurionaudit",),
),
migrations.CreateModel(
name="EntityCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.entity",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Entity Note",
"verbose_name_plural": "Entity Notes",
"db_table": "access_entity_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote",),
),
migrations.CreateModel(
name="PersonAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.person",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Person History",
"verbose_name_plural": "Person Histories",
"db_table": "access_person_audithistory",
"managed": True,
},
bases=("core.centurionaudit",),
),
migrations.CreateModel(
name="PersonCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.person",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Person Note",
"verbose_name_plural": "Person Notes",
"db_table": "access_person_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote",),
),
]

View File

@ -1,81 +0,0 @@
# Generated by Django 5.1.10 on 2025-07-06 10:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0018_remove_entity_is_global_alter_entity_id_and_more"),
("core", "0033_alter_ticketcommentcategory_parent_and_more"),
]
operations = [
migrations.CreateModel(
name="CompanyAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.company",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Company History",
"verbose_name_plural": "Company Histories",
"db_table": "access_company_audithistory",
"managed": True,
},
bases=("core.centurionaudit",),
),
migrations.CreateModel(
name="CompanyCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.company",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Company Note",
"verbose_name_plural": "Company Notes",
"db_table": "access_company_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote",),
),
]

View File

@ -1,56 +0,0 @@
# Generated by Django 5.1.10 on 2025-07-12 07:20
import access.models.tenancy_abstract
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0019_companyaudithistory_companycenturionmodelnote"),
]
operations = [
migrations.AlterField(
model_name="role",
name="id",
field=models.AutoField(
help_text="ID of the item",
primary_key=True,
serialize=False,
unique=True,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="role",
name="model_notes",
field=models.TextField(
blank=True,
help_text="Tid bits of information",
null=True,
verbose_name="Notes",
),
),
migrations.AlterField(
model_name="role",
name="organization",
field=models.ForeignKey(
help_text="Tenant this belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.tenant",
validators=[
access.models.tenancy_abstract.TenancyAbstractModel.validatate_organization_exists
],
verbose_name="Tenant",
),
),
migrations.DeleteModel(
name="RoleHistory",
),
migrations.DeleteModel(
name="RoleNotes",
),
]

View File

@ -1,81 +0,0 @@
# Generated by Django 5.1.10 on 2025-07-12 08:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("access", "0020_remove_rolenotes_model_and_more"),
("core", "0033_alter_ticketcommentcategory_parent_and_more"),
]
operations = [
migrations.CreateModel(
name="RoleAuditHistory",
fields=[
(
"centurionaudit_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionaudit",
),
),
(
"model",
models.ForeignKey(
help_text="Model this history belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_history",
to="access.role",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Role History",
"verbose_name_plural": "Role Histories",
"db_table": "access_role_audithistory",
"managed": True,
},
bases=("core.centurionaudit",),
),
migrations.CreateModel(
name="RoleCenturionModelNote",
fields=[
(
"centurionmodelnote_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.centurionmodelnote",
),
),
(
"model",
models.ForeignKey(
help_text="Model this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="access.role",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Role Note",
"verbose_name_plural": "Role Notes",
"db_table": "access_role_centurionmodelnote",
"managed": True,
},
bases=("core.centurionmodelnote",),
),
]

438
app/access/mixin.py Normal file
View File

@ -0,0 +1,438 @@
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.functional import cached_property
from access.models.tenant import Tenant as Organization
from access.models.team import Team
class OrganizationMixin():
"""Base Organization class"""
parent_model: str = None
""" Parent Model
This attribute defines the parent model for the model in question. The parent model when defined
will be used as the object to obtain the permissions from.
"""
parent_model_pk_kwarg: str = 'pk'
"""Parent Model kwarg
This value is used to define the kwarg that is used as the parent objects primary key (pk).
"""
request = None
user_groups = []
def get_parent_obj(self):
""" Get the Parent Model Object
Use in views where the the model has no organization and the organization should be fetched from the parent model.
Requires attribute `parent_model` within the view with the value of the parent's model class
Returns:
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
def object_organization(self) -> int:
id = None
if hasattr(self, '_object_organization'):
return int(self._object_organization)
try:
if hasattr(self, 'get_queryset'):
self.get_queryset()
if self.parent_model:
obj = self.get_parent_obj()
id = obj.get_organization().id
if obj.is_global:
id = 0
if hasattr(self, 'get_object') and id is None:
obj = self.get_object()
id = obj.get_organization().id
if hasattr(obj, 'is_global'):
if obj.is_global:
id = 0
if hasattr(self, 'instance') and id is None: # Form Instance
id = self.instance.get_organization()
except AttributeError:
if self.request.method == 'POST':
if self.request.POST.get("organization", ""):
id = int(self.request.POST.get("organization", ""))
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
a_field = str(field).split('-')
if len(a_field) == 2:
if a_field[1] == 'organization':
id = int(self.request.POST.get(field))
except:
pass
if id is not None:
self._object_organization = id
return id
def is_member(self, organization: int) -> bool:
"""Returns true if the current user is a member of the organization
iterates over the user_organizations list and returns true if the user is a member
Returns:
bool: _description_
"""
is_member = False
if organization is None:
return False
if int(organization) in self.user_organizations():
is_member = True
return is_member
def get_permission_required(self):
"""
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 "
f"permission_required attribute. Define "
f"{self.__class__.__name__}.permission_required, or override "
f"{self.__class__.__name__}.get_permission_required()."
)
if isinstance(self.permission_required, str):
perms = (self.permission_required,)
else:
perms = self.permission_required
return perms
@cached_property
def is_manager(self) -> bool:
""" Returns true if the current user is a member of the organization"""
is_manager = False
return is_manager
def user_organizations(self) -> list():
"""Current Users organizations
Fetches the Organizations the user is apart of.
Get All groups the user is part of, fetch the associated team,
iterate over the results adding the organization ID to a list to be returned.
Returns:
_type_: User Organizations.
"""
user_organizations = []
if hasattr(self, '_user_organizations'):
return self._user_organizations
teams = Team.objects
for group in self.request.user.groups.all():
team = teams.get(pk=group.id)
self.user_groups = self.user_groups + [group.id]
user_organizations = user_organizations + [team.organization.id]
if len(user_organizations) > 0:
self._user_organizations = user_organizations
return user_organizations
# ToDo: Ensure that the group has access to item
def has_organization_permission(self, organization: int = None, permissions_required: list = None) -> bool:
""" Check if user has permission within organization.
Args:
organization (int, optional): Organization to check. Defaults to None.
permissions_required (list, optional): if doing object level permissions, pass in required permission. Defaults to None.
Returns:
bool: True for yes.
"""
has_permission = False
if permissions_required is None:
permissions_required = self.get_permission_required()
if not organization:
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)
for group in groups:
team = Team.objects.filter(pk=group.id)
team = team.values('organization_id').get()
for permission in group.permissions.values('content_type__app_label', 'codename').all():
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
if assembled_permission in permissions_required and (team['organization_id'] == organization or organization == 0):
return True
return has_permission
def permission_check(self, request, permissions_required: list = None) -> bool:
self.request = request
if permissions_required:
self.permission_required = permissions_required
organization_manager_models = [
'access.tenant',
'access.team',
'access.teamusers',
]
is_organization_manager = False
queryset = None
if hasattr(self, 'get_queryset'):
queryset = self.get_queryset()
obj = None
if hasattr(self, 'get_object'):
try:
obj = self.get_object()
except:
pass
if hasattr(self, 'model'):
if self.model._meta.label_lower in organization_manager_models:
organization = Organization.objects.get(pk=self.object_organization())
if organization.manager == request.user:
is_organization_manager = True
return True
if request.user.is_superuser:
return True
if permissions_required:
perms = permissions_required
else:
perms = self.get_permission_required()
if self.has_organization_permission(permissions_required = perms):
return True
if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get':
if len(self.kwargs) == 0 or (len(self.kwargs) == 1 and 'ticket_type' in self.kwargs):
return True
for required_permission in self.permission_required:
if required_permission.replace(
'view_', ''
) == 'access.tenant' and len(self.kwargs) == 0:
return True
return False
class OrganizationPermission(AccessMixin, OrganizationMixin):
"""## Permission Checking
The base django permissions have not been modified with this app providing Multi-Tenancy. This is done by a mixin, that checks if the item is apart of an organization, if it is; confirmation is made that the user is part of the same organization and as long as they have the correct permission within the organization, access is granted.
### How it works
The overall permissions system of django has not been modified with it remaining fully functional. The multi-tenancy has been setup based off of an organization with teams. A team to the underlying django system is an extension of the django auth group and for every team created a django auth group is created. THe group name is set using the following format: `<organization>_<team name>` and contains underscores `_` instead of spaces.
A User who is added to an team as a "Manager" can modify the team members or if they have permission `access.change_team` which also allows the changing of team permissions. Modification of an organization can be done by the django administrator (super user) or any user with permission `access._change_organization`.
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
Permissions that can be modified for a team have been limited to application permissions only unless adjust the permissions from the django admin site.
### Multi-Tenancy workflow
The workflow is conducted as part of the view and has the following flow:
1. Checks if user is member of organization the object the action is being performed on. Will also return true if the object has field `is_global` set to `true`.
1. Fetches all teams the user is part of.
1. obtains all permissions that are linked to the team.
1. checks if user has the required permission for the action.
1. confirms that the team the permission came from is part of the same organization as the object the action is being conducted on.
1. ONLY on success of the above items, grants access.
"""
permission_required: list = []
""" Permission required for the view
Not specifying this property adjusts the permission check logic so that you can
use the `permission_check()` function directly.
An example of a get request....
``` py
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
```
this example details manual usage of the `permission_check()` function for a get request.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if len(self.permission_required) == 0:
if hasattr(self, 'get_dynamic_permissions'):
self.permission_required = self.get_dynamic_permissions()
if len(self.permission_required) > 0:
non_organization_models = [
'TaskResult'
]
if hasattr(self, 'model'):
if hasattr(self.model, '__name__'):
if self.model.__name__ in non_organization_models:
if hasattr(self, 'get_object'):
self.get_object()
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return self.handle_no_permission()
return super().dispatch(self.request, *args, **kwargs)
if not self.permission_check(request):
raise PermissionDenied('You are not part of this organization')
return super().dispatch(self.request, *args, **kwargs)

View File

@ -1,8 +1,10 @@
import django
from django.contrib.auth.models import Group
from django.db import models
from access.models.tenant import Tenant as Organization
from access.models.team import Team
User = django.contrib.auth.get_user_model()

View File

@ -1,11 +1,13 @@
import traceback
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import exceptions
from rest_framework.permissions import DjangoObjectPermissions
from access.models.tenancy import Tenant
from access.models.tenancy import Tenant, TenancyObject
from core import exceptions as centurion_exceptions
from core.mixins.centurion import Centurion
@ -58,11 +60,11 @@ class OrganizationPermissionMixin(
if hasattr(view, 'model'):
self._is_tenancy_model = issubclass(view.model, Centurion)
self._is_tenancy_model = issubclass(view.model, TenancyObject)
if view.get_parent_model():
self._is_tenancy_model = issubclass(view.get_parent_model(), Centurion)
self._is_tenancy_model = issubclass(view.get_parent_model(), TenancyObject)
return self._is_tenancy_model
@ -111,12 +113,6 @@ class OrganizationPermissionMixin(
raise centurion_exceptions.NotAuthenticated()
if request.method not in view.allowed_methods:
raise centurion_exceptions.MethodNotAllowed(method = request.method)
try:
if (
@ -160,7 +156,12 @@ class OrganizationPermissionMixin(
has_permission_required: bool = permission_required in user_permissions
if not has_permission_required and not request.user.is_superuser:
if request.method not in view.allowed_methods:
raise centurion_exceptions.MethodNotAllowed(method = request.method)
elif not has_permission_required and not request.user.is_superuser:
raise centurion_exceptions.PermissionDenied()
@ -290,11 +291,6 @@ class OrganizationPermissionMixin(
view.model.__name__ == 'AuthToken'
and request._user.id == int(view.kwargs.get('model_id', 0))
)
or ( # org=None is the application wide settings.
view.model.__name__ == 'AppSettings'
and request.user.is_superuser
and obj.organization is None
)
):
return True

View File

@ -1,3 +1,4 @@
from .organization_history import OrganizationHistory # pylint: disable=W0611:unused-import
from .organization_notes import OrganizationNotes # pylint: disable=W0611:unused-import
from . import contact
from . import company_base
from . import person
from . import role

View File

@ -12,10 +12,6 @@ class Company(
# references in code to `organization` witch clashes with the intended name of
# this model.
_is_submodel = True
documentation = ''
class Meta:
@ -44,6 +40,10 @@ class Company(
return self.name
documentation = ''
history_model_name = 'company'
page_layout: dict = [
{
"name": "Details",
@ -95,3 +95,8 @@ class Company(
'organization',
'created',
]
def clean(self):
super().clean()

View File

@ -8,10 +8,6 @@ class Contact(
Person
):
documentation = ''
_is_submodel = True
class Meta:
@ -46,6 +42,10 @@ class Contact(
return self.f_name + ' ' + self.l_name
documentation = ''
history_model_name = 'contact'
page_layout: list = [
{
"name": "Details",

View File

@ -1,23 +1,18 @@
from django.db import models
from access.fields import AutoLastModifiedField
from rest_framework.reverse import reverse
from core.models.centurion import CenturionModel
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
from core.lib.feature_not_used import FeatureNotUsed
class Entity(
CenturionModel
TenancyObject
):
model_tag = 'entity'
documentation = ''
kb_model_name = 'entity'
url_model_name = 'entity'
class Meta:
@ -34,6 +29,15 @@ class Entity(
verbose_name_plural = 'Entities'
id = models.AutoField(
blank=False,
help_text = 'Primary key of the entry',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
entity_type = models.CharField(
blank = False,
help_text = 'Type this entity is',
@ -42,12 +46,14 @@ class Entity(
verbose_name = 'Entity Type'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self) -> str:
related_model = self.get_related_model()
if related_model is None:
@ -58,8 +64,22 @@ class Entity(
return str( related_model )
# app_namespace = 'access'
history_app_label = 'access'
history_model_name = 'entity'
kb_model_name = 'entity'
note_basename = '_api_v2_entity_note'
documentation = ''
page_layout: dict = []
table_fields: list = [
'organization',
'entity_type',
@ -69,23 +89,6 @@ class Entity(
]
def clean_fields(self, exclude = None ):
related_model = self.get_related_model()
if related_model is None:
related_model = self
if self.entity_type != str(related_model._meta.verbose_name).lower().replace(' ', '_'):
self.entity_type = str(related_model._meta.verbose_name).lower().replace(' ', '_')
super().clean_fields( exclude = exclude )
def get_related_field_name(self) -> str:
meta = getattr(self, '_meta')
@ -98,12 +101,13 @@ class Entity(
if getattr(self, related_object.name, None):
if(
if(
not str(related_object.name).endswith('history')
and not str(related_object.name).endswith('notes')
):
return related_object.name
break
return ''
@ -141,3 +145,105 @@ class Entity(
return related_model
def get_url_kwargs(self) -> dict:
model = self.get_related_model()
if len(self._meta.parents) == 0 and model is None:
return {
'pk': self.id
}
if model is None:
model = self
kwargs = {
'entity_model': str(model._meta.verbose_name).lower().replace(' ', '_'),
}
if model.pk:
kwargs.update({
'pk': model.id
})
return kwargs
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 = None
if getattr(self, 'get_related_model', None):
model = self.get_related_model()
if model is None:
model = self
sub_entity = ''
if model._meta.model_name != 'entity':
sub_entity = '_sub'
kwargs = self.get_url_kwargs()
view = 'list'
if 'pk' in kwargs:
view = 'detail'
if request:
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, request=request, kwargs = kwargs )
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, kwargs = kwargs )
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
related_model = self.get_related_model()
if related_model is None:
related_model = self
if self.entity_type != str(related_model._meta.verbose_name).lower().replace(' ', '_'):
self.entity_type = str(related_model._meta.verbose_name).lower().replace(' ', '_')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def save_history(self, before: dict, after: dict) -> bool:
from access.models.entity_history import EntityHistory
history = super().save_history(
before = before,
after = after,
history_model = EntityHistory
)
return history

View File

@ -0,0 +1,55 @@
from django.db import models
from access.models.entity import Entity
from core.models.model_history import ModelHistory
from devops.models.feature_flag import FeatureFlag
class EntityHistory(
ModelHistory
):
class Meta:
db_table = 'access_entity_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Entity History'
verbose_name_plural = 'Entity History'
model = models.ForeignKey(
Entity,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'history',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_object(self):
return self
def get_serialized_model(self, serializer_context):
model = None
from access.serializers.entity import BaseSerializer
model = BaseSerializer(self.model, context = serializer_context)
return model

View File

@ -0,0 +1,45 @@
from django.db import models
from access.models.entity import Entity
from core.models.model_notes import ModelNotes
class EntityNotes(
ModelNotes
):
class Meta:
db_table = 'access_entity_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Entity Note'
verbose_name_plural = 'Entity Notes'
model = models.ForeignKey(
Entity,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'notes',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_url_kwargs(self) -> dict:
return {
'model_id': self.model.pk,
'pk': self.pk
}

View File

@ -1 +1 @@
from .tenant import Tenant as Organization # pylint: disable=W0611:unused-import
from .tenant import Tenant as Organization

View File

@ -10,10 +10,6 @@ class Person(
Entity
):
_is_submodel = True
documentation = ''
class Meta:
@ -67,6 +63,10 @@ class Person(
return self.f_name + ' ' + self.l_name + f' (DOB: {self.dob})'
documentation = ''
history_model_name = 'person'
page_layout: dict = []
table_fields: list = [
@ -102,7 +102,7 @@ class Person(
for entry in duplicate_entry:
if(
entry.f_name == self.f_name
and entry.m_name == self.m_name
@ -112,8 +112,8 @@ class Person(
raise ValidationError(
detail = {
'dob': f'Person {self.f_name} {self.l_name}' \
f'already exists with this birthday {entry.dob}'
'dob': f'Person {self.f_name} {self.l_name} already exists with this birthday {entry.dob}'
},
code = 'duplicate_person_on_dob'
)

View File

@ -1,20 +1,15 @@
from django.contrib.auth.models import Permission
from django.db import models
from access.fields import AutoLastModifiedField
from core.models.centurion import CenturionModel
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
class Role(
CenturionModel
TenancyObject
):
documentation = ''
model_tag = 'role'
class Meta:
@ -33,6 +28,14 @@ class Role(
verbose_name_plural = 'Roles'
id = models.AutoField(
blank=False,
help_text = 'Primary key of the entry',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
name = models.CharField(
blank = False,
help_text = 'Name of this role',
@ -50,15 +53,21 @@ class Role(
verbose_name = 'Permissions'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
is_global = None
def __str__(self) -> str:
return str( self.organization ) + ' / ' + self.name
documentation = ''
page_layout: dict = [
{
"name": "Details",
@ -121,29 +130,14 @@ class Role(
]
_permissions: list[ Permission ] = None
def save_history(self, before: dict, after: dict) -> bool:
_permissions_int: list[ int ] = None
from access.models.role_history import RoleHistory
def get_permissions(self, as_int_list = False ):
history = super().save_history(
before = before,
after = after,
history_model = RoleHistory
)
if self._permissions is None:
permissions = []
permissions_int = []
for permission in self.permissions: # pylint: disable=E1133:not-an-iterable
if permission in _permissions:
continue
permissions += [ permission ]
permissions_int += [ permission.id ]
self._permissions = permissions
self._permissions_int = permissions_int
if as_int_list:
return self._permissions_int
return self._permissions_int
return history

View File

@ -0,0 +1,53 @@
from django.db import models
from core.models.model_history import ModelHistory
from access.models.role import Role
class RoleHistory(
ModelHistory
):
class Meta:
db_table = 'access_role_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Role History'
verbose_name_plural = 'Role History'
model = models.ForeignKey(
Role,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'history',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_object(self):
return self
def get_serialized_model(self, serializer_context):
model = None
from access.serializers.role import BaseSerializer
model = BaseSerializer(self.model, context = serializer_context)
return model

View File

@ -0,0 +1,45 @@
from django.db import models
from access.models.role import Role
from core.models.model_notes import ModelNotes
class RoleNotes(
ModelNotes
):
class Meta:
db_table = 'access_role_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Role Note'
verbose_name_plural = 'Role Notes'
model = models.ForeignKey(
Role,
blank = False,
help_text = 'Model this note belongs to',
null = False,
on_delete = models.CASCADE,
related_name = 'notes',
verbose_name = 'Model',
)
table_fields: list = []
page_layout: dict = []
def get_url_kwargs(self) -> dict:
return {
'model_id': self.model.pk,
'pk': self.pk
}

View File

@ -179,12 +179,12 @@ class Team(Group, TenancyObject):
def save_history(self, before: dict, after: dict) -> bool:
from access.models.team_history import TeamAuditHistory
from access.models.team_history import TeamHistory
history = super().save_history(
before = before,
after = after,
history_model = TeamAuditHistory
history_model = TeamHistory
)

View File

@ -15,7 +15,7 @@ from access.models.tenant import Tenant
from access.models.team import Team
from core.lib.feature_not_used import FeatureNotUsed
from core.mixins.history_save import SaveHistory
from core.mixin.history_save import SaveHistory
User = django.contrib.auth.get_user_model()

View File

@ -1,3 +1,4 @@
import django
import logging
from django.db import models
@ -8,7 +9,7 @@ from access.models.tenant import Tenant
from core import exceptions as centurion_exceptions
from core.middleware.get_request import get_request
from core.mixins.history_save import SaveHistory
from core.mixin.history_save import SaveHistory
@ -51,10 +52,6 @@ class TenancyManager(models.Manager):
user_organizations: list(str()) = []
has_tenant_field = False
if getattr(self.model, 'organization', None) is not None:
has_tenant_field = True
if request:
@ -73,33 +70,29 @@ class TenancyManager(models.Manager):
if team.organization.id not in user_organizations:
# if not user_organizations:
if not user_organizations:
# self.user_organizations = []
self.user_organizations = []
user_organizations += [ team.organization.id ]
# if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
if len(user_organizations) > 0 and not user.is_superuser:
if has_tenant_field:
if getattr(self.model, 'is_global', False) is True:
return super().get_queryset().select_related('organization').filter(
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
else:
# return super().get_queryset().filter(
# models.Q(organization__in=user_organizations)
# )
return super().get_queryset().filter()
if has_tenant_field:
return super().get_queryset().select_related('organization')
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
)
return super().get_queryset()
@ -236,7 +229,7 @@ class TenancyObject(SaveHistory):
if self.app_namespace:
app_namespace = self.app_namespace
app_namespace = self.app_namespace + ':'
return str(app_namespace)
@ -255,17 +248,12 @@ class TenancyObject(SaveHistory):
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
namespace = f'v2'
if self.get_app_namespace():
namespace = namespace + ':' + self.get_app_namespace()
if request:
return reverse(f"{namespace}:_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"v2:" + self.get_app_namespace() + f"_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"{namespace}:_api_v2_{model_name}-detail", kwargs = self.get_url_kwargs() )
return reverse(f"v2:" + self.get_app_namespace() + f"_api_v2_{model_name}-detail", kwargs = self.get_url_kwargs() )
def get_url_kwargs(self) -> dict:

View File

@ -1,126 +0,0 @@
from django.core.exceptions import (
ValidationError,
)
from django.db import models
from access.models.tenancy import (
TenancyManager as TenancyManagerDepreciated
)
from access.models.tenant import Tenant
class TenancyManager(
models.Manager
):
"""Multi-Tennant Object Manager
This manager specifically caters for the multi-tenancy features of Centurion ERP.
"""
def get_queryset(self):
""" Fetch the data
When the model contains the user data, the query is filtered to their
and the globally defined Tenancy only.
Returns:
(queryset): **super user**: return unfiltered data.
(queryset): **not super user**: return data from the stored unique organizations.
"""
user = None # When CenturionUser in use
if hasattr(self.model, 'context'):
user = self.model.context['user']
has_tenant_field = False
if getattr(self.model, 'organization', None) is not None:
has_tenant_field = True
if user:
tenancies = user.get_tenancies(int_list = True)
if len(tenancies) > 0 and not request.user.is_superuser:
if has_tenant_field:
return super().get_queryset().select_related('organization').filter(
models.Q(organization__in = tenancies)
)
return super().get_queryset().filter()
if has_tenant_field:
return super().get_queryset().select_related('organization')
return super().get_queryset()
class TenancyAbstractModel(
models.Model,
):
""" Tenancy Model Abstract class.
This class is for inclusion within **every** model within Centurion ERP.
Provides the required fields, functions and methods for multi tennant objects.
Unless otherwise stated, **no** object within this class may be overridden.
Raises:
ValidationError: User failed to supply organization
"""
objects = TenancyManagerDepreciated()
""" ~~Multi-Tenant Manager~~
**Note:** ~~This manager relies upon the model class having `context['user']`
set. without a user the manager can not perform multi-tenant queries.~~
"""
class Meta:
abstract = True
def validatate_organization_exists(self):
"""Ensure that the user did provide an organization
Raises:
ValidationError: User failed to supply organization.
"""
if not self:
raise ValidationError(
code = 'required',
message = 'You must provide an organization'
)
organization = models.ForeignKey(
Tenant,
blank = False,
help_text = 'Tenant this belongs to',
null = False,
on_delete = models.CASCADE,
related_name = '+',
validators = [
validatate_organization_exists
],
verbose_name = 'Tenant'
)
def get_tenant(self) -> Tenant:
""" Return the models Tenancy
This model can be safely over-ridden as long as it returns the models
tenancy
"""
return self.organization

View File

@ -3,41 +3,37 @@ import django
from django.conf import settings
from django.db import models
from rest_framework.reverse import reverse
from access.fields import (
AutoCreatedField,
AutoLastModifiedField,
AutoSlugField
)
from core.mixins.centurion import Centurion
from core.mixin.history_save import SaveHistory
User = django.contrib.auth.get_user_model()
class Tenant(
Centurion,
):
@property
def organization(self):
return self
model_tag = 'tenant'
class Tenant(SaveHistory):
class Meta:
verbose_name = "Tenant"
verbose_name_plural = "Tenants"
ordering = ['name']
ordering = [
'name'
]
def save(self, *args, **kwargs):
if self.slug == '_':
self.slug = self.name.lower().replace(' ', '_')
super().save(*args, **kwargs)
id = models.AutoField(
blank = False,
blank=False,
help_text = 'ID of this item',
primary_key = True,
unique = True,
primary_key=True,
unique=True,
verbose_name = 'ID'
)
@ -51,39 +47,38 @@ class Tenant(
manager = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank = True,
blank = False,
help_text = 'Manager for this Tenancy',
null = True,
on_delete = models.PROTECT,
on_delete=models.SET_NULL,
verbose_name = 'Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
help_text = 'Tid bits of information',
null = True,
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
created = AutoCreatedField()
modified = AutoLastModifiedField()
def get_organization(self):
return self
def __int__(self):
return self.id
def __str__(self):
return self.name
def get_organization(self):
return self
table_fields: list = [
'nbsp',
'name',
@ -139,5 +134,28 @@ class Tenant(
]
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})
def save_history(self, before: dict, after: dict) -> bool:
from access.models.organization_history import OrganizationHistory
history = super().save_history(
before = before,
after = after,
history_model = OrganizationHistory
)
return history
Organization = Tenant

View File

@ -1,197 +0,0 @@
from django.contrib.auth.models import Permission, User
from django.core.exceptions import PermissionDenied
from access.models.tenant import Tenant
class CenturionUser(
User,
):
"""Centurion User
A Multi-Tenant User wirh permission Checking.
ToDo:
- Add to Roles user field `related_name = roles`
- Add to Roles group field `related_name = roles`
# - have group lookup prefetch related roles__permissions
- have user lookup prefetch related roles__permissions and groups__roles__permissions
Args:
User (Model): Django Base User
"""
_tenancies: list[Tenant] = None
_tenancies_int: list[int] = None
_permissions: list[Permission] = None
_permissions_by_tenancy: dict[ str, list[ Permission ] ] = None
"""Permissions by Tenancy
`{ 'tenancy_{id}': [ Permission ] }`
"""
# EMAIL_FIELD = 'email' # Update contact email field name so it's different to the user model.
# REQUIRED_FIELDS = [
# EMAIL_FIELD,
# 'f_name',
# 'l_name',
# ]
class Meta:
abstract = False
proxy = True # User will be linked to Employee/Customer entity via related_name from the entity.
# ToDo: refactory Employee/Customer to inherit from a new model. entity_user
verbose_name = 'Centurion User'
verbose_name_plural = 'Centurion Users'
def get_full_name(self) -> str:
return f'{self.entity_user.f_name} {self.entity_user.l_name}'
def get_group_permissions(self, tenancy: bool = True) -> dict[ str, list[ Permission ] ] | list[ Permission ]:
""" Get the Users Permissions
Args:
tenancy (bool, optional): Return permission in list. Defaults to True.
Returns:
dict[ str, list[ Permission ] ]: Permissions listed by tenancy
list[ Permission ]: All Permissions
"""
for group in self.groups: # pylint: disable=E1133:not-an-iterable
for role in group.roles:
pass
# role.get_permissions()
def get_permissions(self, tenancy: bool = True) -> dict[ str, list[ Permission ] ] | list[ Permission ]:
""" Get the Users Permissions
Args:
tenancy (bool, optional): Return permission in list. Defaults to True.
Returns:
dict[ str, list[ Permission ] ]: Permissions listed by tenancy
list[ Permission ]: All Permissions
"""
# also get group permissions. self.get_group_permissions()
for role in self.roles:
pass
# role.get_permissions()
# also populate `self._tenancies` and `self._tenancies_int`
return []
def get_short_name() -> str:
return self.entity_user.f_name
def get_tenancies(self, int_list = False) -> list[ Tenant ] | list[ int ]:
"""Get the Tenancies the user is in.
Args:
int_list (bool, optional): Return Tenancy list as int values. Defaults to False.
Returns:
list[ Tenant ] | list[ int ]: All Tenancies the user is in.
"""
if self._tenancies is None:
if self._permissions is None:
self.get_permissions
tenancies: list = []
tenancies_int: list = []
for role in self.roles:
if role.organization in tenancies:
continue
tenancies += [ role.organization ]
tenancies_int += [ role.organization.id ]
self._tenancies = tenancies
self._tenancies_int = tenancies_int
if as_int_list:
return self._tenancies_int
return self._tenancies
def has_module_perms(self, app_label): # is this needed?
# if has app_label in perms
raise PermissionDenied
def has_perm(self, permission: Permission, obj = None, tenancy: Tenant = None) -> bool:
if(
obj is None
and tenancy is None
):
raise ValueError('Both obj and tenancy cant be None')
if tenancy is None:
tenancy = obj.organization
# if self.has_tenancy_permission(perm, tenancy):
# for tenancy, permissions in self.get_permissions().items()
if tenancy is None:
raise ValueError('tenancy cant be None')
permissions = self.get_permissions()
if f'tenancy_{tenancy.id}' not in permissions:
raise PermissionDenied
for tenancy, permissions in self.get_permissions().items():
if(
tenancy == f'tenancy_{tenancy.id}'
and perm in permissions
):
return True
raise PermissionDenied
def has_perms(self, permission_list: list[ Permission ], obj = None, tenancy: Tenant = None):
for perm in perm_list:
self.has_perm( perm, obj )
return True

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import CompanyAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'CompanyAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = CompanyAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'CompanyAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import ContactAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'ContactAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = ContactAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'ContactAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import EntityAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'EntityAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = EntityAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'EntityAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import PersonAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'PersonAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = PersonAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'PersonAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import RoleAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'RoleAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = RoleAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'RoleAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,56 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from api.serializers import common
from centurion.models.meta import TenantAuditHistory # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionaudit import (
BaseSerializer,
ViewSerializer as AuditHistoryViewSerializer
)
@extend_schema_serializer(component_name = 'TenantAuditHistoryModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Git Group Audit History Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = TenantAuditHistory
fields = [
'id',
'organization',
'display_name',
'content_type',
'model',
'before',
'after',
'action',
'user',
'created',
'_urls',
]
read_only_fields = fields
@extend_schema_serializer(component_name = 'TenantAuditHistoryViewSerializer')
class ViewSerializer(
ModelSerializer,
AuditHistoryViewSerializer,
):
"""Git Group Audit History Base View Model"""
pass

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import CompanyCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'CompanyModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = CompanyCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'CompanyModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import ContactCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'ContactModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = ContactCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'ContactModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import EntityCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'EntityModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = EntityCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'EntityModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import PersonCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'PersonModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = PersonCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'PersonModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import RoleCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'RoleModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = RoleCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'RoleModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -1,87 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.serializers.organization import (TenantBaseSerializer)
from centurion.models.meta import TenantCenturionModelNote # pylint: disable=E0401:import-error disable=E0611:no-name-in-module
from core.serializers.centurionmodelnote import ( # pylint: disable=W0611:unused-import
BaseSerializer,
ModelSerializer as BaseModelModelSerializer,
ViewSerializer as BaseModelViewSerializer
)
@extend_schema_serializer(component_name = 'TeamModelNoteModelSerializer')
class ModelSerializer(
BaseModelModelSerializer,
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
}
class Meta:
model = TenantCenturionModelNote
fields = [
'id',
'organization',
'display_name',
'body',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'organization',
'created_by',
'modified_by',
'content_type',
'model',
'created',
'modified',
'_urls',
]
def validate(self, attrs):
is_valid = False
note_model = self.Meta.model.model.field.related_model
attrs['model'] = note_model.objects.get(
id = int( self.context['view'].kwargs['model_id'] )
)
is_valid = super().validate(attrs)
return is_valid
@extend_schema_serializer(component_name = 'TeamModelNoteViewSerializer')
class ViewSerializer(
ModelSerializer,
BaseModelViewSerializer,
):
organization = TenantBaseSerializer( many = False, read_only = True )

View File

@ -66,6 +66,7 @@ class ModelSerializer(
'entity_type',
'display_name',
'model_notes',
'is_global',
'created',
'modified',
'_urls',

View File

@ -41,6 +41,7 @@ class ModelSerializer(
'display_name',
'name',
'model_notes',
'is_global',
'created',
'modified',
'_urls',

View File

@ -46,6 +46,7 @@ class ModelSerializer(
'email',
'directory',
'model_notes',
'is_global',
'created',
'modified',
'_urls',

View File

@ -0,0 +1,41 @@
from core.serializers.model_notes import (
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
from access.models.entity_notes import EntityNotes
class EntityNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class EntityNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = EntityNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class EntityNoteViewSerializer(
ModelNoteViewSerializer,
EntityNoteModelSerializer,
):
pass

View File

@ -44,6 +44,7 @@ class ModelSerializer(
'l_name',
'dob',
'model_notes',
'is_global',
'created',
'modified',
'_urls',

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from access.models.tenant import Tenant
from centurion.serializers.user import UserBaseSerializer
from app.serializers.user import UserBaseSerializer
from core import fields as centurion_field
@ -20,7 +20,7 @@ class TenantBaseSerializer(serializers.ModelSerializer):
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_tenant-detail", format="html"
view_name="v2:_api_v2_organization-detail", format="html"
)
class Meta:
@ -61,13 +61,13 @@ class TenantModelSerializer(
'model_pk': item.pk
}
),
# 'notes': reverse(
# "v2:_api_v2_organization_note-list",
# request=self._context['view'].request,
# kwargs={
# 'model_id': item.pk
# }
# ),
'notes': reverse(
"v2:_api_v2_organization_note-list",
request=self._context['view'].request,
kwargs={
'model_id': item.pk
}
),
'teams': reverse("v2:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}),
}

View File

@ -0,0 +1,48 @@
from rest_framework import serializers
from access.models.organization_notes import OrganizationNotes
from api.serializers import common
from app.serializers.user import UserBaseSerializer
from core.serializers.model_notes import (
ModelNotes,
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
class OrganizationNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class OrganizationNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = OrganizationNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class OrganizationNoteViewSerializer(
ModelNoteViewSerializer,
OrganizationNoteModelSerializer,
):
pass

View File

@ -9,7 +9,7 @@ from access.serializers.organization import TenantBaseSerializer
from api.serializers import common
from centurion.serializers.permission import PermissionBaseSerializer
from app.serializers.permission import PermissionBaseSerializer

View File

@ -0,0 +1,48 @@
from rest_framework import serializers
from access.models.role_notes import RoleNotes
from api.serializers import common
from app.serializers.user import UserBaseSerializer
from core.serializers.model_notes import (
ModelNotes,
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
class RoleNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class RoleNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = RoleNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class RoleNoteViewSerializer(
ModelNoteViewSerializer,
RoleNoteModelSerializer,
):
pass

View File

@ -0,0 +1,48 @@
from rest_framework import serializers
from access.models.team_notes import TeamNotes
from api.serializers import common
from app.serializers.user import UserBaseSerializer
from core.serializers.model_notes import (
ModelNotes,
ModelNoteBaseSerializer,
ModelNoteModelSerializer,
ModelNoteViewSerializer
)
class TeamNoteBaseSerializer(ModelNoteBaseSerializer):
pass
class TeamNoteModelSerializer(
ModelNoteModelSerializer
):
class Meta:
model = TeamNotes
fields = ModelNoteModelSerializer.Meta.fields + [
'model',
]
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
'model',
'content_type',
]
class TeamNoteViewSerializer(
ModelNoteViewSerializer,
TeamNoteModelSerializer,
):
pass

View File

@ -1,10 +1,12 @@
from rest_framework.reverse import reverse
from rest_framework import serializers
from access.models.team_user import TeamUsers
from api.serializers import common
from centurion.serializers.user import UserBaseSerializer
from app.serializers.user import UserBaseSerializer
@ -53,7 +55,7 @@ class TeamUserModelSerializer(
get_url = super().get_url( item = item )
# del get_url['history']
del get_url['history']
del get_url['knowledge_base']

View File

@ -9,7 +9,7 @@ from api.serializers import common
from access.functions.permissions import permission_queryset
from access.serializers.organization import TenantBaseSerializer
from centurion.serializers.permission import PermissionBaseSerializer
from app.serializers.permission import Permission, PermissionBaseSerializer
from core import fields as centurion_field
@ -94,6 +94,7 @@ class TeamModelSerializer(
'model_notes',
'permissions',
'organization',
'is_global',
'created',
'modified',
'_urls',

View File

@ -0,0 +1,22 @@
{% extends 'base.html.j2' %}
{% block content_header_icon %}{% endblock %}
{% block content %}
<table class="data">
<tr>
<th>Name</th>
<th>Created</th>
<th>Modified</th>
</tr>
{% for org in organization_list %}
<tr>
<td><a href="/organization/{{ org.id }}/">{{ org.name }}</a></td>
<td>{{ org.created }}</td>
<td>{{ org.modified }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,106 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}Organization - {{ organization.name }}{% endblock %}
{% block content %}
<style>
form div .helptext {
background-color: rgb(0, 140, 255);
display: block;
}
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.manager.label }}</label>
<span>{{ organization.manager }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.created.label }}</label>
<span>{{ form.created.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.modified.label }}</label>
<span>{{ form.modified.value }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.model_notes.value | markdown | safe }}</div>
</div>
</div>
<div style="display: block;">
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
</div>
<hr />
<table>
<thead>
<tr>
<th>Team Name</th>
<th>Created</th>
<th>Modified</th>
</tr>
</thead>
{% for field in teams %}
<tr>
<td><a href="{% url 'Access:_team_view' organization_id=organization.id pk=field.id %}">{{ field.team_name }}</a></td>
<td>{{ field.created }}</td>
<td>{{ field.modified }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends 'base.html.j2' %}
{% block title %}Team - {{ team.team_name }}{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_div }}
<input style="display:unset;" type="submit" value="Submit">
</form>
<hr />
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
<input type="button" value="Delete Team"
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
<input type="button" value="Assign User"
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
{{ formset.management_form }}
<table id="formset" class="form">
<thead>
<tr>
<th>User</th>
<th>Manager</th>
<th>Created</th>
<th>Modified</th>
<th>&nbsp;</th>
</tr>
</thead>
{% for field in teamusers %}
<tr>
<td>{{ field.user }}</td>
<td><input type="checkbox" {% if field.manager %}checked{% endif %} disabled></td>
<td>{{ field.created }}</td>
<td>{{ field.modified }}</td>
<td><a
href="{% url 'Access:_team_user_delete' organization_id=organization.id team_id=field.team_id pk=field.id %}">Delete</a></a>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,32 @@
class TenancyObject:
""" Tests for checking TenancyObject """
model = None
""" Model to be tested """
should_model_history_be_saved: bool = True
""" Should model history be saved.
By default this should always be 'True', however in special
circumstances, this may not be desired.
"""
# def test_history_save(self):
# """Confirm the desired intent for saving model history."""
# assert self.model.save_model_history == self.should_model_history_be_saved
# @pytest.mark.skip(reason="to be written")
# def test_edit_no_organization_fails(self):
# """ Devices must be assigned an organization
# Must not be able to edit an item without an organization
# """
# pass

View File

@ -1,204 +0,0 @@
import pytest
import random
from django.test import Client
class AdditionalTestCases:
def test_permission_add(self, model_instance, api_request_permissions,
model_kwargs, kwargs_api_create
):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
client.force_login( api_request_permissions['user']['add'] )
the_model = model_instance( kwargs_create = self.kwargs_create_item )
url = the_model.get_url( many = True )
the_model.delete()
kwargs = kwargs_api_create.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
response = client.post(
path = url,
data = kwargs,
content_type = 'application/json'
)
assert response.status_code == 201, response.content
def test_returned_results_only_user_orgs(self, model_instance, model_kwargs, api_request_permissions):
"""Returned results check
Ensure that a query to the viewset endpoint does not return
items that are not part of the users organizations.
"""
if model_kwargs.get('organization', None) is None:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
client = Client()
viewable_organizations = [
api_request_permissions['tenancy']['user'].id,
]
if getattr(self, 'global_organization', None):
# Cater for above test that also has global org
viewable_organizations += [ api_request_permissions['tenancy']['global'] ]
client.force_login( api_request_permissions['user']['view'] )
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['different']
})
model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['global']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance( kwargs_create = kwargs )
response = client.get(
path = the_model.get_url( many = True )
)
# if response.status_code == 405:
# pytest.xfail( reason = 'ViewSet does not have this request method.' )
# elif IsAuthenticatedOrReadOnly in response.renderer_context['view'].permission_classes:
# pytest.xfail( reason = 'ViewSet is public viewable, test is N/A' )
assert response.status_code == 200
contains_different_org: bool = False
for item in response.data['results']:
if 'organization' not in item:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
if(
int(item['organization']['id']) not in viewable_organizations
and
int(item['organization']['id']) != api_request_permissions['tenancy']['global'].id
):
contains_different_org = True
print(f'Failed returned row was: {item}')
assert not contains_different_org
def test_returned_data_from_user_and_global_organizations_only(
self, model_instance, model_kwargs, api_request_permissions
):
"""Check items returned
Items returned from the query Must be from the users organization and
global ONLY!
"""
if model_kwargs.get('organization', None) is None:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
client = Client()
only_from_user_org: bool = True
viewable_organizations = [
api_request_permissions['tenancy']['user'].id,
api_request_permissions['tenancy']['global'].id
]
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['different']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['global']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
model_instance(
kwargs_create = kwargs
)
client.force_login( api_request_permissions['user']['view'] )
kwargs = self.kwargs_create_item.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance( kwargs_create = kwargs )
response = client.get(
path = the_model.get_url( many = True )
)
assert len(response.data['results']) >= 2 # fail if only one item extist.
for row in response.data['results']:
if 'organization' not in row:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
if row['organization']['id'] not in viewable_organizations:
only_from_user_org = False
print(f"Users org: {api_request_permissions['tenancy']['user'].id}")
print(f"global org: {api_request_permissions['tenancy']['global'].id}")
print(f'Failed returned row was: {row}')
assert only_from_user_org

View File

@ -1,204 +0,0 @@
import pytest
import random
from django.test import Client
class AdditionalTestCases:
def test_permission_add(self, model_instance, api_request_permissions,
model_kwargs, kwargs_api_create
):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
client.force_login( api_request_permissions['user']['add'] )
the_model = model_instance( kwargs_create = self.kwargs_create_item )
url = the_model.get_url( many = True )
the_model.delete()
kwargs = kwargs_api_create.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
response = client.post(
path = url,
data = kwargs,
content_type = 'application/json'
)
assert response.status_code == 201, response.content
def test_returned_results_only_user_orgs(self, model_instance, model_kwargs, api_request_permissions):
"""Returned results check
Ensure that a query to the viewset endpoint does not return
items that are not part of the users organizations.
"""
if model_kwargs.get('organization', None) is None:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
client = Client()
viewable_organizations = [
api_request_permissions['tenancy']['user'].id,
]
if getattr(self, 'global_organization', None):
# Cater for above test that also has global org
viewable_organizations += [ api_request_permissions['tenancy']['global'] ]
client.force_login( api_request_permissions['user']['view'] )
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['different']
})
model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['global']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance( kwargs_create = kwargs )
response = client.get(
path = the_model.get_url( many = True )
)
# if response.status_code == 405:
# pytest.xfail( reason = 'ViewSet does not have this request method.' )
# elif IsAuthenticatedOrReadOnly in response.renderer_context['view'].permission_classes:
# pytest.xfail( reason = 'ViewSet is public viewable, test is N/A' )
assert response.status_code == 200
contains_different_org: bool = False
for item in response.data['results']:
if 'organization' not in item:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
if(
int(item['organization']['id']) not in viewable_organizations
and
int(item['organization']['id']) != api_request_permissions['tenancy']['global'].id
):
contains_different_org = True
print(f'Failed returned row was: {item}')
assert not contains_different_org
def test_returned_data_from_user_and_global_organizations_only(
self, model_instance, model_kwargs, api_request_permissions
):
"""Check items returned
Items returned from the query Must be from the users organization and
global ONLY!
"""
if model_kwargs.get('organization', None) is None:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
client = Client()
only_from_user_org: bool = True
viewable_organizations = [
api_request_permissions['tenancy']['user'].id,
api_request_permissions['tenancy']['global'].id
]
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['different']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance(
kwargs_create = kwargs
)
kwargs = self.kwargs_create_item.copy()
kwargs.update({
'organization': api_request_permissions['tenancy']['global']
})
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
model_instance(
kwargs_create = kwargs
)
client.force_login( api_request_permissions['user']['view'] )
kwargs = self.kwargs_create_item.copy()
kwargs['dob'] = str(random.randint(1972, 2037)) + '-' + str(
random.randint(1, 12)) + '-' + str(random.randint(1, 28))
the_model = model_instance( kwargs_create = kwargs )
response = client.get(
path = the_model.get_url( many = True )
)
assert len(response.data['results']) >= 2 # fail if only one item extist.
for row in response.data['results']:
if 'organization' not in row:
pytest.xfail( reason = 'Model lacks organization field. test is n/a' )
if row['organization']['id'] not in viewable_organizations:
only_from_user_org = False
print(f"Users org: {api_request_permissions['tenancy']['user'].id}")
print(f"global org: {api_request_permissions['tenancy']['global'].id}")
print(f'Failed returned row was: {row}')
assert only_from_user_org

View File

@ -1,44 +0,0 @@
import pytest
from django.test import Client
class AdditionalTestCases:
def test_permission_add(self, model_instance, api_request_permissions,
model_kwargs, kwargs_api_create
):
""" Check correct permission for add
Attempt to add as user with permission
"""
client = Client()
client.force_login( api_request_permissions['user']['add'] )
the_model = model_instance( kwargs_create = model_kwargs )
url = the_model.get_url( many = True )
response = client.post(
path = url,
data = kwargs_api_create,
content_type = 'application/json'
)
assert response.status_code == 201, response.content
def test_returned_data_from_user_and_global_organizations_only(
self
):
"""Check items returned
Items returned from the query Must be from the users organization and
global ONLY!
"""
pytest.mark.xfail( reason = 'model is not for global use' )

View File

@ -22,14 +22,3 @@ def create_serializer():
yield ModelSerializer
@pytest.fixture( scope = 'class')
def model_kwargs(request, kwargs_company):
request.cls.kwargs_create_item = kwargs_company.copy()
yield kwargs_company.copy()
if hasattr(request.cls, 'kwargs_create_item'):
del request.cls.kwargs_create_item

View File

@ -1,36 +0,0 @@
import pytest
from access.tests.functional.entity.test_functional_entity_api_fields import (
EntityAPIInheritedCases
)
@pytest.mark.model_company
class CompanyAPITestCases(
EntityAPIInheritedCases,
):
@property
def parameterized_api_fields(self):
return {
'name': {
'expected': str
}
}
class CompanyAPIInheritedCases(
CompanyAPITestCases,
):
pass
@pytest.mark.module_access
class CompanyAPIPyTest(
CompanyAPITestCases,
):
pass

View File

@ -53,11 +53,11 @@ class CompanyMetadataInheritedCases(
}
# self.url_kwargs = {
# 'model_name': self.model._meta.sub_model_type
# 'entity_model': self.model._meta.sub_model_type
# }
# self.url_view_kwargs = {
# 'model_name': self.model._meta.sub_model_type
# 'entity_model': self.model._meta.sub_model_type
# }
super().setUpTestData()

View File

@ -0,0 +1,43 @@
import pytest
from access.tests.functional.entity.test_functional_entity_permission import (
EntityPermissionsAPIInheritedCases
)
class CompanyPermissionsAPITestCases(
EntityPermissionsAPIInheritedCases,
):
add_data: dict = {
'name': 'Ian1',
}
kwargs_create_item: dict = {
'name': 'Ian2',
}
kwargs_create_item_diff_org: dict = {
'name': 'Ian3',
}
class CompanyPermissionsAPIInheritedCases(
CompanyPermissionsAPITestCases,
):
add_data: dict = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
class CompanyPermissionsAPIPyTest(
CompanyPermissionsAPITestCases,
):
pass

View File

@ -0,0 +1,46 @@
import pytest
from rest_framework.exceptions import ValidationError
from access.tests.functional.entity.test_functional_entity_serializer import (
MockView,
EntitySerializerInheritedCases
)
class CompanySerializerTestCases(
EntitySerializerInheritedCases
):
parameterized_test_data: dict = {
"name": {
'will_create': False,
'exception_key': 'required'
},
}
valid_data: dict = {
'name': 'Ian',
}
"""Valid data used by serializer to create object"""
class CompanySerializerInheritedCases(
CompanySerializerTestCases,
):
parameterized_test_data: dict = None
valid_data: dict = None
"""Valid data used by serializer to create object"""
class CompanySerializerPyTest(
CompanySerializerTestCases,
):
parameterized_test_data: dict = None

View File

@ -0,0 +1,58 @@
from django.test import TestCase
from access.models.company_base import Company
from access.tests.functional.entity.test_functional_entity_viewset import (
EntityViewSetInheritedCases
)
class ViewSetTestCases(
EntityViewSetInheritedCases,
):
add_data: dict = {
'name': 'Ian',
}
kwargs_create_item: dict = {
'name': 'Ian2',
}
kwargs_create_item_diff_org: dict = {
'name': 'Ian3',
}
model = Company
class CompanyViewSetInheritedCases(
ViewSetTestCases,
):
model = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item = {
**super().kwargs_create_item,
**self.kwargs_create_item
}
self.kwargs_create_item_diff_org = {
**super().kwargs_create_item_diff_org,
**self.kwargs_create_item_diff_org
}
super().setUpTestData()
class CompanyViewSetTest(
ViewSetTestCases,
TestCase,
):
pass

View File

@ -22,13 +22,3 @@ def create_serializer():
yield ModelSerializer
@pytest.fixture( scope = 'class')
def model_kwargs(request, kwargs_contact):
request.cls.kwargs_create_item = kwargs_contact.copy()
yield kwargs_contact.copy()
if hasattr(request.cls, 'kwargs_create_item'):
del request.cls.kwargs_create_item

View File

@ -1,41 +0,0 @@
import pytest
from access.tests.functional.person.test_functional_person_api_fields import (
PersonAPIInheritedCases
)
@pytest.mark.model_contact
class ContactAPITestCases(
PersonAPIInheritedCases,
):
property
def parameterized_api_fields(self):
return {
'email': {
'expected': str
},
'directory': {
'expected': bool
}
}
class ContactAPIInheritedCases(
ContactAPITestCases,
):
pass
@pytest.mark.module_access
class ContactAPIPyTest(
ContactAPITestCases,
):
pass

View File

@ -0,0 +1,60 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.functional.person.test_functional_person_history import (
PersonHistoryInheritedCases
)
class ContactTestCases(
PersonHistoryInheritedCases,
):
field_name = 'model_notes'
kwargs_create_obj: dict = {
'email': 'ipfunny@unit.test',
}
kwargs_delete_obj: dict = {
'email': 'ipweird@unit.test',
}
model = Contact
class ContactHistoryInheritedCases(
ContactTestCases,
):
model = None
"""Entity model to test"""
kwargs_create_obj: dict = None
kwargs_delete_obj: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_obj.update(
super().kwargs_create_obj
)
self.kwargs_delete_obj.update(
super().kwargs_delete_obj
)
super().setUpTestData()
class ContactHistoryTest(
ContactTestCases,
TestCase,
):
pass

View File

@ -0,0 +1,70 @@
import pytest
from access.tests.functional.person.test_functional_person_permission import (
PersonPermissionsAPIInheritedCases
)
class ContactPermissionsAPITestCases(
PersonPermissionsAPIInheritedCases,
):
add_data: dict = {
'email': 'ipfunny@unit.test',
}
kwargs_create_item: dict = {
'email': 'ipweird@unit.test',
}
kwargs_create_item_diff_org: dict = {
'email': 'ipstrange@unit.test',
}
class ContactPermissionsAPIInheritedCases(
ContactPermissionsAPITestCases,
):
add_data: dict = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
# url_name = '_api_v2_entity_sub'
# @pytest.fixture(scope='class')
# def inherited_var_setup(self, request):
# request.cls.url_kwargs.update({
# 'entity_model': self.model._meta.sub_model_type
# })
# request.cls.url_view_kwargs.update({
# 'entity_model': self.model._meta.sub_model_type
# })
# @pytest.fixture(scope='class', autouse = True)
# def class_setup(self, request, django_db_blocker,
# model,
# var_setup,
# prepare,
# inherited_var_setup,
# diff_org_model,
# create_model,
# ):
# pass
class ContactPermissionsAPIPyTest(
ContactPermissionsAPITestCases,
):
pass

View File

@ -0,0 +1,46 @@
import pytest
from rest_framework.exceptions import ValidationError
from access.tests.functional.person.test_functional_person_serializer import (
MockView,
PersonSerializerInheritedCases
)
class ContactSerializerTestCases(
PersonSerializerInheritedCases
):
parameterized_test_data: dict = {
"email": {
'will_create': False,
'exception_key': 'required'
}
}
valid_data: dict = {
'email': 'contactentityduplicatetwo@unit.test',
}
"""Valid data used by serializer to create object"""
class ContactSerializerInheritedCases(
ContactSerializerTestCases,
):
parameterized_test_data: dict = None
valid_data: dict = None
"""Valid data used by serializer to create object"""
class ContactSerializerPyTest(
ContactSerializerTestCases,
):
parameterized_test_data: dict = None

View File

@ -0,0 +1,58 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.functional.person.test_functional_person_viewset import (
PersonViewSetInheritedCases
)
class ViewSetTestCases(
PersonViewSetInheritedCases,
):
add_data: dict = {
'email': 'ipfunny@unit.test',
}
kwargs_create_item: dict = {
'email': 'ipweird@unit.test',
}
kwargs_create_item_diff_org: dict = {
'email': 'ipstrange@unit.test',
}
model = Contact
class ContactViewSetInheritedCases(
ViewSetTestCases,
):
model = None
@classmethod
def setUpTestData(self):
self.kwargs_create_item = {
**super().kwargs_create_item,
**self.kwargs_create_item
}
self.kwargs_create_item_diff_org = {
**super().kwargs_create_item_diff_org,
**self.kwargs_create_item_diff_org
}
super().setUpTestData()
class ContactViewSetTest(
ViewSetTestCases,
TestCase,
):
pass

View File

@ -22,13 +22,3 @@ def create_serializer():
yield ModelSerializer
@pytest.fixture( scope = 'class')
def model_kwargs(request, kwargs_entity):
request.cls.kwargs_create_item = kwargs_entity.copy()
yield kwargs_entity.copy()
if hasattr(request.cls, 'kwargs_create_item'):
del request.cls.kwargs_create_item

View File

@ -1,48 +0,0 @@
import pytest
from access.models.entity import Entity
from api.tests.functional.test_functional_api_fields import (
APIFieldsInheritedCases,
)
@pytest.mark.model_entity
class EntityAPITestCases(
APIFieldsInheritedCases,
):
base_model = Entity
@property
def parameterized_api_fields(self):
return {
'entity_type': {
'expected': str
},
'_urls.history': {
'expected': str
},
'_urls.knowledge_base': {
'expected': str
}
}
class EntityAPIInheritedCases(
EntityAPITestCases,
):
pass
@pytest.mark.module_access
class EntityAPIPyTest(
EntityAPITestCases,
):
pass

View File

@ -0,0 +1,78 @@
from django.test import TestCase
from access.models.entity_history import Entity, EntityHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class HistoryTestCases(
HistoryEntriesCommon,
):
field_name = 'model_notes'
history_model = EntityHistory
kwargs_create_obj: dict = {}
kwargs_delete_obj: dict = {}
model = Entity
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.obj = self.model.objects.create(
organization = self.organization,
model_notes = self.field_value_original,
**self.kwargs_create_obj,
)
self.obj_delete = self.model.objects.create(
organization = self.organization,
model_notes = 'another note',
**self.kwargs_delete_obj,
)
self.call_the_banners()
class EntityHistoryInheritedCases(
HistoryTestCases,
):
model = None
"""Entity model to test"""
kwargs_create_obj: dict = None
kwargs_delete_obj: dict = None
@classmethod
def setUpTestData(self):
self.kwargs_create_obj.update(
super().kwargs_create_obj
)
self.kwargs_delete_obj.update(
super().kwargs_delete_obj
)
super().setUpTestData()
class EntityHistoryTest(
HistoryTestCases,
TestCase,
):
pass

View File

@ -223,7 +223,7 @@ class EntityMetadataInheritedCases(
kwargs_create_item_diff_org: dict = {}
url_name = '_api_entity_sub'
url_name = '_api_v2_entity_sub'
@classmethod
@ -240,11 +240,11 @@ class EntityMetadataInheritedCases(
}
self.url_kwargs = {
'model_name': self.model._meta.sub_model_type
'entity_model': self.model._meta.sub_model_type
}
self.url_view_kwargs = {
'model_name': self.model._meta.sub_model_type
'entity_model': self.model._meta.sub_model_type
}
super().setUpTestData()
@ -257,4 +257,4 @@ class EntityMetadataTest(
):
url_name = '_api_entity'
url_name = '_api_v2_entity'

View File

@ -0,0 +1,89 @@
import pytest
from api.tests.functional.test_functional_api_permissions import (
APIPermissionsInheritedCases,
)
class EntityPermissionsAPITestCases(
APIPermissionsInheritedCases,
):
add_data: dict = {}
app_namespace = 'v2'
change_data = {}
delete_data = {}
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
url_kwargs: dict = {}
url_name = '_api_v2_entity'
url_view_kwargs: dict = {}
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a tenancy model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
class EntityPermissionsAPIInheritedCases(
EntityPermissionsAPITestCases,
):
add_data: dict = None
kwargs_create_item: dict = None
kwargs_create_item_diff_org: dict = None
url_name = '_api_v2_entity_sub'
@pytest.fixture(scope='class')
def inherited_var_setup(self, request):
request.cls.url_kwargs.update({
'entity_model': self.model._meta.sub_model_type
})
request.cls.url_view_kwargs.update({
'entity_model': self.model._meta.sub_model_type
})
@pytest.fixture(scope='class', autouse = True)
def class_setup(self, request, django_db_blocker,
model,
var_setup,
prepare,
inherited_var_setup,
diff_org_model,
create_model,
):
pass
class EntityPermissionsAPIPyTest(
EntityPermissionsAPITestCases,
):
pass

View File

@ -28,7 +28,7 @@ class MockView:
"""
@pytest.mark.skip(reason = 'see #874, tests being refactored')
class EntitySerializerTestCases:

View File

@ -0,0 +1,266 @@
import django
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.entity import Entity
from access.models.tenant import Tenant as Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
User = django.contrib.auth.get_user_model()
class ViewSetBase:
add_data: dict = {
'model_notes': 'added model note'
}
app_namespace = 'v2'
base_model = Entity
"""Base model for this sub model
don't change or override this value
"""
change_data = None
delete_data = {}
kwargs_create_item: dict = {
'model_notes': 'added model note'
}
kwargs_create_item_diff_org: dict = {
'model_notes': 'added model note'
}
model = None
url_kwargs: dict = {}
url_view_kwargs: dict = {}
url_name = None
@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.different_organization = Organization.objects.create(name='test_different_organization')
self.view_user = User.objects.create_user(username="test_user_view", password="password")
self.item = self.model.objects.create(
organization = organization,
**self.kwargs_create_item
)
self.other_org_item = self.model.objects.create(
organization = self.different_organization,
**self.kwargs_create_item_diff_org
)
self.url_view_kwargs.update({ 'pk': self.item.id })
if self.add_data is not None:
self.add_data.update({
'organization': self.organization.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])
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")
TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
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 = self.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
)
def test_sanity_is_asset_sub_model(self):
"""Sanity Test
This test ensures that the model being tested `self.model` is a
sub-model of `self.base_model`.
This test is required as the same viewset is used for all sub-models
of `Entity`
"""
assert issubclass(self.model, self.base_model)
class ViewSetTestCases(
ViewSetBase,
SerializersTestCases,
):
model = Entity
class EntityViewSetInheritedCases(
ViewSetTestCases,
):
model = None
url_name = '_api_v2_entity_sub'
@classmethod
def setUpTestData(self):
self.kwargs_create_item = {
**super().kwargs_create_item,
**self.kwargs_create_item
}
self.kwargs_create_item_diff_org = {
**super().kwargs_create_item_diff_org,
**self.kwargs_create_item_diff_org
}
self.url_kwargs = {
'entity_model': self.model._meta.sub_model_type
}
self.url_view_kwargs = {
'entity_model': self.model._meta.sub_model_type
}
super().setUpTestData()
class EntityViewSetTest(
ViewSetTestCases,
TestCase,
):
url_name = '_api_v2_entity'

View File

@ -0,0 +1,81 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.models.entity_notes import Entity, EntityNotes
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
class NotesAPITestCases(
ModelNotesNotesAPIFields,
):
entity_model = None
model = EntityNotes
kwargs_model_create: dict = None
# url_view_kwargs: dict = None
view_name: str = '_api_v2_entity_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Call parent setup
2. Create a model note
3. add url kwargs
4. make the API request
"""
super().setUpTestData()
self.item = self.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.entity_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_model_create
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.pk
}
self.make_request()
class EntityNotesAPIInheritedCases(
NotesAPITestCases,
):
entity_model = None
kwargs_model_create = None
class EntityNotesAPITest(
NotesAPITestCases,
TestCase,
):
entity_model = Entity
kwargs_model_create = {}

View File

@ -0,0 +1,162 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.viewsets.entity_notes import ViewSet
from core.tests.abstract.test_functional_notes_viewset import (
ModelNotesViewSetBase,
ModelNotesMetadata,
ModelNotesPermissionsAPI,
ModelNotesSerializer
)
class ViewSetBase(
ModelNotesViewSetBase
):
viewset = ViewSet
kwargs_create_model_item: dict = {}
kwargs_create_model_item_other_org: dict = {}
url_name = '_api_v2_entity_note'
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.item = self.viewset.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.viewset.model.model.field.related_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_create_model_item
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.other_org_item = self.viewset.model.objects.create(
organization = self.different_organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.viewset.model.model.field.related_model.objects.create(
organization = self.organization,
model_notes = 'text',
**self.kwargs_create_model_item_other_org
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_kwargs = {
'model_id': self.item.model.pk,
}
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.id
}
class NotesPermissionsAPITestCases(
ViewSetBase,
ModelNotesPermissionsAPI,
):
viewset = None
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a global model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
class EntityNotesPermissionsAPIInheritedCases(
NotesPermissionsAPITestCases,
):
viewset = None
class EntityNotesPermissionsAPITest(
NotesPermissionsAPITestCases,
TestCase,
):
viewset = ViewSet
class NotesSerializerTestCases(
ViewSetBase,
ModelNotesSerializer,
):
viewset = None
class EntityNotesSerializerInheritedCases(
NotesSerializerTestCases,
):
viewset = None
class EntityNotesSerializerTest(
NotesSerializerTestCases,
TestCase,
):
viewset = ViewSet
class NotesMetadataTestCases(
ViewSetBase,
ModelNotesMetadata,
):
viewset = None
class EntityNotesMetadataInheritedCases(
NotesMetadataTestCases,
):
viewset = None
class EntityNotesMetadataTest(
NotesMetadataTestCases,
TestCase,
):
viewset = ViewSet

View File

@ -0,0 +1,32 @@
from django.test import TestCase
from access.models.organization_history import Tenant as Organization, OrganizationHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class History(
HistoryEntriesCommon,
TestCase,
):
model = Organization
history_model = OrganizationHistory
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.obj = self.model.objects.create(
name = self.field_value_original,
)
self.obj_delete = self.model.objects.create(
name = self.field_value_delete,
)
self.call_the_banners()

View File

@ -1,14 +1,20 @@
import django
import pytest
import unittest
import requests
from django.contrib.auth.models import Permission
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.shortcuts import reverse
from django.test import Client, TestCase
from access.models.tenant import Tenant as Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
from api.tests.abstract.api_permissions_viewset import APIPermissions
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional, MetaDataNavigationEntriesFunctional
@ -22,7 +28,7 @@ class ViewSetBase:
app_namespace = 'v2'
url_name = '_api_tenant'
url_name = '_api_v2_organization'
change_data = {'name': 'device'}
@ -191,6 +197,108 @@ class ViewSetBase:
class OrganizationPermissionsAPI(
ViewSetBase,
APIPermissions,
TestCase
):
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a tenancy model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with permission
"""
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
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
def test_add_different_organization_denied(self):
""" Check correct permission for add
This test is a duplicate of a test case with the same name.
Organizations are not tenancy models so this test does nothing of value
attempt to add as user from different organization
"""
pass
class OrganizationViewSet(
ViewSetBase,
SerializersTestCases,

View File

@ -0,0 +1,116 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from access.viewsets.organization_notes import ViewSet
from core.tests.abstract.test_functional_notes_viewset import (
ModelNotesViewSetBase,
ModelNotesMetadata,
ModelNotesPermissionsAPI,
ModelNotesSerializer
)
class ViewSetBase(
ModelNotesViewSetBase
):
viewset = ViewSet
url_name = '_api_v2_organization_note'
@classmethod
def setUpTestData(self):
super().setUpTestData()
self.item = self.viewset.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.organization,
created_by = self.view_user,
modified_by = self.view_user,
)
self.other_org_item = self.viewset.model.objects.create(
organization = self.different_organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.different_organization,
created_by = self.view_user,
modified_by = self.view_user,
)
self.global_org_item = self.viewset.model.objects.create(
organization = self.global_organization,
content = 'a random comment global_organization',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = self.global_organization,
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_kwargs = {
'model_id': self.item.model.pk,
}
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.id
}
class OrganizationModelNotesPermissionsAPI(
ViewSetBase,
ModelNotesPermissionsAPI,
TestCase,
):
def test_returned_data_from_user_and_global_organizations_only(self):
"""Check items returned
This test case is a over-ride of a test case with the same name.
This model is not a global model making this test not-applicable.
Items returned from the query Must be from the users organization and
global ONLY!
"""
pass
class OrganizationModelNotesSerializer(
ViewSetBase,
ModelNotesSerializer,
TestCase,
):
pass
class OrganizationModelNotesMetadata(
ViewSetBase,
ModelNotesMetadata,
TestCase,
):
pass

View File

@ -0,0 +1,53 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
from access.models.tenant import Tenant as Organization
from access.models.organization_notes import OrganizationNotes
class OrganizationNotesAPI(
ModelNotesNotesAPIFields,
TestCase,
):
model = OrganizationNotes
view_name: str = '_api_v2_organization_note'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Call parent setup
2. Create a model note
3. add url kwargs
4. make the API request
"""
super().setUpTestData()
self.item = self.model.objects.create(
organization = self.organization,
content = 'a random comment',
content_type = ContentType.objects.get(
app_label = str(self.model._meta.app_label).lower(),
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
),
model = Organization.objects.create(
name = 'dev'
),
created_by = self.view_user,
modified_by = self.view_user,
)
self.url_view_kwargs = {
'model_id': self.item.model.pk,
'pk': self.item.pk
}
self.make_request()

View File

@ -22,13 +22,3 @@ def create_serializer():
yield ModelSerializer
@pytest.fixture( scope = 'class')
def model_kwargs(request, kwargs_person):
request.cls.kwargs_create_item = kwargs_person.copy()
yield kwargs_person.copy()
if hasattr(request.cls, 'kwargs_create_item'):
del request.cls.kwargs_create_item

View File

@ -1,47 +0,0 @@
import pytest
from access.tests.functional.entity.test_functional_entity_api_fields import (
EntityAPIInheritedCases
)
@pytest.mark.model_person
class PersonAPITestCases(
EntityAPIInheritedCases,
):
property
def parameterized_api_fields(self):
return {
'f_name': {
'expected': str
},
'm_name': {
'expected': str
},
'l_name': {
'expected': str
},
'dob': {
'expected': str
}
}
class PersonAPIInheritedCases(
PersonAPITestCases,
):
pass
@pytest.mark.module_access
class PersonAPIPyTest(
PersonAPITestCases,
):
pass

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