Compare commits

..

34 Commits

Author SHA1 Message Date
Jon
8d071c68df chore: add Merge/Pull request template
#226
2024-08-14 01:52:22 +09:30
Jon
3b1691ff62 docs(roadmap): update completed features
#226
2024-08-14 01:46:21 +09:30
Jon
a77c43d213 docs(base): detail view template
. #24 #226 closes #22
2024-08-14 01:33:48 +09:30
Jon
086959b431 refactor(itim): services now use details template
. #22 #226
2024-08-14 01:23:17 +09:30
Jon
3f117f9d83 feat(base): create detail view templates
purpose is to aid in the development of a detail form

#22 #24 #226
2024-08-14 00:02:25 +09:30
Jon
6a23845a4f docs: initial adding of template page
#22
2024-08-13 15:03:10 +09:30
Jon
b9c6d04e04 chore: add services navigation icon
!43 #69
2024-08-13 13:23:45 +09:30
Jon
32c0027ecf fix(itim): ensure that the service template config is also rendered as part of device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
dae52e8646 docs: fluff the port and services
!43 closes #69
2024-08-13 13:23:45 +09:30
Jon
890a5651a0 fix(itim): dont render link if no device
!43 #69
2024-08-13 13:23:45 +09:30
Jon
4cb37f8347 feat(itam): Render Service Config with device config
!43 #69
2024-08-13 13:23:45 +09:30
Jon
a2010b9517 feat(itam): Display deployed services for devices
!43 #69
2024-08-13 13:23:45 +09:30
Jon
c95736ce14 feat(itim): Prevent circular service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
b46c61954c feat(itim): Port number validation to check for valid port numbers
!43 #69
2024-08-13 13:23:45 +09:30
Jon
afe4266600 feat(itim): Prevent Service template from being assigned as dependent service
!43 #69
2024-08-13 13:23:45 +09:30
Jon
0c8d1c8da1 feat(itim): Add service template support
!43 #69
2024-08-13 13:23:45 +09:30
Jon
eac998b5cc fix(itim): Dont show self within service dependencies
!43 #69
2024-08-13 13:23:45 +09:30
Jon
5914782252 feat(itim): Ports for service management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
73d875c4ac feat(itim): Service Management
!43 #69
2024-08-13 13:23:45 +09:30
Jon
8f439f0675 fix(assistance): Only return distinct values when limiting KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
0f102c6aaf docs(assistance): document kb categories for user
!43 closes #10
2024-08-13 13:23:45 +09:30
Jon
4852c6caeb feat(assistance): Filter KB articles to target user
only intended to filter for users whom dont have change perm.

!43 #10
2024-08-13 13:23:45 +09:30
Jon
3fffba2eba feat(assistance): Add date picker to date fields for KB articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a1293984ea feat(assistance): Dont display expired articles for "view" users
!43 #10
2024-08-13 13:23:45 +09:30
Jon
4876db50c1 docs(assistance): document kb for user
!43 #10
2024-08-13 13:23:45 +09:30
Jon
425cc066af feat(base): add code highlighting to markdown
!43 #10
2024-08-13 13:23:45 +09:30
Jon
1086f517fa feat(assistance): Categorised Knowledge base articles
!43 #10
2024-08-13 13:23:45 +09:30
Jon
2fdbf87ddd docs(assistance): added pages for knowledgebase
!43 #10
2024-08-13 13:23:45 +09:30
Jon
86228836c7 chore(base): rename information -> assistance
!43 #10
2024-08-13 13:23:45 +09:30
Jon
a6e6c948a5 feat(itim): Add menu entry
!43 #69 #71
2024-08-13 13:23:45 +09:30
Jon
dcdfa8feb7 feat(itam): Ability to add device configuration
!43 fixes #44
2024-08-13 13:23:45 +09:30
Jon
8388d2e695 test(external_link): add tests
!43 fixes #6
2024-08-13 13:23:45 +09:30
Jon
29f269050f feat(settings): New model to allow adding templated links to devices and software
!43 #6
2024-08-13 13:23:45 +09:30
Jon
93c4fc2009 docs: move settings pages into sub-directory
!43 #6
2024-08-13 13:23:45 +09:30
1648 changed files with 9533 additions and 171203 deletions

View File

@ -17,5 +17,5 @@ commitizen:
prerelease_offset: 1
tag_format: $version
update_changelog_on_bump: false
version: 1.17.0
version: 1.0.0-b14
version_scheme: semver

View File

@ -1,121 +0,0 @@
---
name: New Database Model
about: Use when creating a new database model.
title: "New Model - <model table name>"
type: Task
labels: task::feature, triage, type::task
---
<!-- Add an intro -->
<!-- describe a use case if not covered in intro -->
## 📝 Details
<!--
Describe in detail the following:
- New model field
- if foreign key field, what it's name will be or if it's not to be linked ensure specified and coded with `related_name = '+' to disable the link`.
- How the UI will work, be layed out, new ui features etc
- custom permissions if required
-->
## 🚧 Tasks
<!-- Don't remove tasks strike them out. use `~~` before and after the item. i.e. `- ~~[ ] Model Created~~` note: don't include the list dash-->
- [ ] 🆕 [Model Created](https://nofusscomputing.com/projects/centurion_erp/development/models/)
- [ ] 🛠️ Migrations added
- [ ] ♻️ Serializer Created
- [ ] 🔄 [ViewSet Created](https://nofusscomputing.com/projects/centurion_erp/development/views/)
- [ ] 🔗 URL Route Added
- [ ] 🏷️ 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_
>[!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
---
<!-- Add additional tasks here and as a check box list -->
### 🧪 Tests
- Unit Tests
- [ ] API Render (fields)
- [ ] [Model](https://nofusscomputing.com/projects/centurion_erp/development/models/#tests)
- [ ] ViewSet
- Function Test
- [ ] History API Render (fields)
- [ ] History Entries
- [ ] API Metadata
- [ ] API Permissions
- [ ] Model
- [ ] Serializer
- [ ] ViewSet
## ✅ Requirements
A Requirement is a must have. In addition will also be tested.
- [ ] Must have a [model_tag](https://nofusscomputing.com/projects/centurion_erp/user/core/markdown/#model-reference)
<!--
When detailing requirements the following must be taken into account:
- what the user should be able to do
- what the user should not be able to do
- what should occur when a user performs an action
-->
- Functional Requirements
- Non-Functional Requirements
---
<!-- Add additional requirement here and as a check box list -->

View File

@ -20,9 +20,6 @@
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] **Feature Release ONLY** :red_square: [Squash migration files](https://docs.djangoproject.com/en/5.2/topics/migrations/#squashing-migrations) :red_square:
_Multiple migration files created as part of this release are to be sqauashed into a few files as possible so as to limit the number of migrations_
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
@ -35,8 +32,6 @@
- [ ] :checkered_flag: Milestone assigned
- [ ] :gear: :test_tube: [Functional Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
- [ ] :test_tube: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
_ensure test coverage delta is not less than zero_

View File

@ -16,17 +16,6 @@ env:
jobs:
mkdocs:
name: 'MKDocs'
permissions:
pull-requests: write
contents: write
statuses: write
checks: write
actions: write
uses: nofusscomputing/action_mkdocs/.github/workflows/reusable_mkdocs.yaml@development
docker:
name: 'Docker'
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development

14
.gitignore vendored
View File

@ -1,23 +1,11 @@
venv/**
*/static/**
__pycache__
**.sqlite*
**.sqlite3
**.sqlite
**.coverage
.coverage*
artifacts/
**.tmp.*
volumes/
build/
pages/
node_modules/
.markdownlint-cli2.jsonc
.markdownlint.json
package-lock.json
package.json
**.junit.xml
**.JUnit.xml
feature_flags.json
coverage_*.json
*-coverage.xml
log/

View File

@ -7,6 +7,5 @@
"streetsidesoftware.code-spell-checker",
"qwtel.sqlite-viewer",
"jebbs.markdown-extended",
"william-voyek.vscode-nginx",
]
}

79
.vscode/launch.json vendored
View File

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Centurion",
"name": "Debug: Django",
"type": "debugpy",
"request": "launch",
"args": [
@ -16,54 +16,9 @@
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Gunicorn",
"type": "debugpy",
"request": "launch",
"module": "gunicorn",
"args": [
"--config=../includes/etc/gunicorn.conf.py",
"--access-logfile",
"-",
"--workers",
"3",
"--bind",
"0.0.0.0:8002",
"app.wsgi:application",
],
"django": true,
"autoStartBrowser": false,
"cwd": "${workspaceFolder}/app",
"env": {
"PROMETHEUS_MULTIPROC_DIR": ""
}
},
{
"name": "Centurion Feature Flag (Management Command)",
"type": "debugpy",
"request": "launch",
"args": [
"feature_flag",
// "0.0.0.0:8002"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Migrate",
"type": "debugpy",
"request": "launch",
"args": [
"migrate"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/app/manage.py"
},
{
"name": "Debug: Celery",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",
@ -77,34 +32,6 @@
"debug-itsm@%h"
],
"cwd": "${workspaceFolder}/app"
},
{
"name": "Debug pytest (collect)",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": [
"--override-ini", "addopts=",
"--collect-only",
"app",
],
"console": "integratedTerminal",
"justMyCode": false
},
{
"name": "Python Debugger: Local Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
},
}
]
}

17
.vscode/settings.json vendored
View File

@ -5,23 +5,16 @@
"!python"
],
"python.testing.pytestArgs": [
"--override-ini", "addopts=",
"app",
// "-v",
// "--cov",
// "--cov-report xml",
"app"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testing.coverageToolbarEnabled": true,
"cSpell.words": [
"ITSM"
],
"cSpell.language": "en-AU",
"jest.enable": false,
"pylint.enabled": true,
"testing.showCoverageInExplorer": true,
"testing.coverageToolbarEnabled": true,
"testing.coverageBarThresholds": {
"red": 0,
"yellow": 60,
"green": 90
},
"telemetry.feedback.enabled": false,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,131 +1,6 @@
# Contribution Guide
Development of this project has been setup to be done from VSCodium. The following additional requirements need to be met:
- npm has been installed. _required for `markdown` linting_
`sudo apt install -y --no-install-recommends npm`
- setup of other requirements can be done with `make prepare`
- **ALL** Linting must pass for Merge to be conducted.
_`make lint`_
## TL;DR
from the root of the project to start a test server use:
``` bash
# activate python venv
source /tmp/centurion_erp/bin/activate
# enter app dir
cd app
# Start dev server can be viewed at http://127.0.0.1:8002
python manage.py runserver 8002
# Run any migrations, if required
python manage.py migrate
# Create a super suer if required
python manage.py createsuperuser
```
## Makefile
!!! tip "TL;DR"
Common make commands are `make prepare` then `make docs` and `make lint`
Included within the root of the repository is a makefile that can be used during development to check/run different items as is required during development. The following make targets are available:
- `prepare`
_prepare the repository. init's all git submodules and sets up a python virtual env and other make targets_
- `docs`
_builds the docs and places them within a directory called build, which can be viewed within a web browser_
- `lint`
_conducts all required linting_
- `docs-lint`
_lints the markdown documents within the docs directory for formatting errors that MKDocs may/will have an issue with._
- `clean`
_cleans up build artifacts and removes the python virtual environment_
> this doc is yet to receive a re-write
## Docker Container
within the `deploy/` directory there is a docker compose file. running `docker compose up` from this directory will launch a full stack deployment locally containing Centurion API, User Interface, a worker and a RabbitMQ server. once launched you can navigate to `http://127.0.0.1/` to start browsing the site.
You may need to run migrations if your not mounting your own DB. to do this run `docker exec -ti centurion-erp python manage.py migrate`
## Page speed tests
to run page speed tests (requires a working prometheus and grafa setup). use the following
``` bash
clear; \
K6_PROMETHEUS_RW_TREND_STATS="p(99),p(95),p(90),max,min" \
K6_PROMETHEUS_RW_SERVER_URL=http://<prometheus url>:9090/api/v1/write \
BASE_URL="http://127.0.0.1:8002" \
AUTH_TOKEN="< api token of superuser>" \
k6 run \
-o experimental-prometheus-rw \
--tag "commit=$(git rev-parse HEAD)" \
--tag "testid=<name of test for ref>" \
test/page_speed.js
```
## Tips / Handy info
- To obtain a list of models _(in in the same order as the file system)_ using the db shell `python3 manage.py dbshell` run the following sql command:
``` sql
SELECT model FROM django_content_type ORDER BY app_label ASC, model ASC;
```
# Old working docs
## Dev Environment
It's advised to setup a python virtual env for development. this can be done with the following commands.

View File

@ -32,14 +32,9 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_
**Stable Branch**
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage_functional.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_functional_test.json)
----
@ -48,13 +43,9 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage_functional.json&style=plastic)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_functional_test.json)
----
<br>

View File

@ -1,196 +1,7 @@
## Version 1.17.0
- Added setting for log files.
Enables user to specify a default path for centurion's logging. Add the following to your settings file `/etc/itsm/settings.py`
``` py
LOG_FILES = {
"centurion": "/var/log/centurion.log", # Normal Centurion Operations
"weblog": "/var/log/weblog.log", # All web requests made to Centurion
"rest_api": "/var/log/rest_api.log", # Rest API
"catch_all":"/var/log/catch-all.log" # A catch all log. Note: does not log anything that has already been logged.
}
```
With this new setting, the previous setting `LOGGING` will no longer function.
- Renamed `Organization` model to `Tenant` so as to reflect what is actually is.
- `robots.txt` file now being served from the API container at path `/robots.txt` with `User-agent: *` and `Disallow: /`
## Version 1.16.0
- Employees model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
- Ticket and Ticket Comment added behind feature flag `2025-00006` and will remain behind this flag until production ready.
- In preparation of the [Ticket and Ticket Comment model re-write](https://github.com/nofusscomputing/centurion_erp/issues/564)
- Depreciated Change Ticket
- Depreciated Ticket Comment Endpoint
- Depreciated Request Ticket
- Depreciated Incident Ticket
- Depreciated Problem Ticket
- Depreciated Project Task Ticket
These endpoints still work and will remain so until the new Ticket and Ticket Comment Models are production ready.
## Version 1.15.0
- Entities model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
- Roles model added behind feature flag `2025-00003` and will remain behind this flag until production ready.
- Accounting Module added behind feature flag `2025-00004` and will remain behind this flag until production ready.
## Version 1.14.0
- Git Repository and Git Group Models added behind feature flag `2025-00001`. They will remain behind this feature flag until the Git features are fully developed and ready for use.
## Version 1.13.0
- DevOps Module added.
- Feature Flagging Component added as par of the DevOps module.
## Version 1.11.0
**Note:** Migrations should be performed offline. **Failing to perform** an online migration, the option provided below will not be available if the migration crashes. Running the below commands to reset the database for the migrations to re-run will cause data loss if users are making changes to Centurion.
- History views removed from original Centurion interface.
- History views removed from API v1.
- A migration exists that will move the history from the old tables to the new ones.
if for some reason the migration crashes enter the following commands in the dbshell `python manage.py dbshell` and restart the migrations
``` sql
delete from access_organization_history;
delete from access_team_history;
delete from assistance_knowledge_base_history;
delete from assistance_knowledge_base_category_history;
delete from config_management_configgroups_history;
delete from config_management_configgroupsoftware_history;
delete from config_management_configgrouphosts_history;
delete from core_manufacturer_history;
delete from core_ticketcategory_history;
delete from core_ticketcommentcategory_history;
delete from itam_device_history;
delete from itam_devicemodel_history;
delete from itam_devicetype_history;
delete from itam_deviceoperatingsystem_history;
delete from itam_devicesoftware_history;
delete from itam_operatingsystem_history;
delete from itam_operatingsystemversion_history;
delete from itam_software_history;
delete from itam_softwareversion_history;
delete from itam_softwarecategory_history;
delete from itim_cluster_history;
delete from itim_clustertype_history;
delete from itim_port_history;
delete from itim_service_history;
delete from project_management_project_history;
delete from project_management_projectmilestone_history;
delete from project_management_projectstate_history;
delete from project_management_projecttype_history;
delete from settings_externallink_history;
delete from core_model_history;
```
The above commands truncate the data from the new history tables so the migration can run again.
## Version 1.10.0
- Nothing significant to report
## Version 1.9.0
- Nothing significant to report
## Version 1.8.0
- Prometheus exporter added. To enable metrics for the database you will have to update the database backend. see the [docs](https://nofusscomputing.com/projects/centurion_erp/administration/monitoring/#django-exporter-setup) for further information.
## Version 1.5.0
- When v1.4.0 was release the migrations were not merged. As part of the work conducted on this release the v1.4 migrations have been squashed. This should not have any effect on any system that when they updated to v1.4, they ran the migrations and they **completed successfully**. Upgrading from <1.4.0 to this release should also have no difficulties as the migrations required still exist. There are less of them, however with more work per migration.
!!! Note
If you require the previously squashed migrations for what ever reason. Clone the repo and go to commit 17f47040d6737905a1769eee5c45d9d15339fdbf, which is the commit prior to the squashing which is commit ca2da06d2cd393cabb7e172ad47dfb2dd922d952.
## Version 1.4.0
API redesign in preparation for moving the UI out of centurion to it's [own project](https://github.com/nofusscomputing/centurion_erp_ui). This release introduces a **Feature freeze** to the current UI. Only bug fixes will be done for the current UI.
API v2 is a beta release and is subject to change. On completion of the new UI, API v2 will more likely than not be set as stable.
- A large emphasis is being placed upon API stability. This is being achieved by ensuring the following:
- Actions can only be carried out by users whom have the correct permissions
- fields are of the correct type and visible when required as part of the API response
- Data validations work and notify the user of any issue
We are make the above possible by ensuring a more stringent test policy.
- New API will be at path `api/v2`.
- API v1 is now **Feature frozen** with only bug fixes being completed. It's recommended that you move to and start using API v2 as this has feature parity with API v1.
- API v1 is **depreciated**
- Depreciation of **ALL** API urls. API v1 Will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion.
# Version 1.3.0
!!! danger "Security"
As is currently the recommended method of deployment, the Centurion Container must be deployed behind a reverse proxy the conducts the SSL termination.
This release updates the docker container to be a production setup for deployment of Centurion. Prior to this version Centurion ERP was using a development setup for the webserver.
- Docker now uses SupervisorD for container
- Gunicorn WSGI setup for Centurion with NginX as the webserver.
- Container now has a health check.
- To setup container as "Worker", set `IS_WORKER='True'` environmental variable within container. _**Note:** You can still use command `celery -A app worker -l INFO`, although **not** recommended as the container health check will not be functioning_
## Version 1.0.0
# Version 1.0.0
Initial Release of Centurion ERP.
### Breaking changes
## Breaking changes
- Nil

17
app/.coveragerc Normal file
View File

@ -0,0 +1,17 @@
[run]
source = .
omit =
*migrations/*
*tests/*/*
[report]
omit =
*/tests/*/*
*/migrations/*
*apps.py
*manage.py
*__init__.py
*asgi*
*wsgi*
*admin.py
*urls.py

View File

@ -1,14 +1,7 @@
import django
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin
from access.models.tenant import Tenant as Organization
from access.models.team import Team
from access.models.team_user import TeamUsers
User = django.contrib.auth.get_user_model()
from .models import *
admin.site.unregister(Group)
@ -32,40 +25,6 @@ class OrganizationAdmin(admin.ModelAdmin):
list_filter = ["created"]
search_fields = ["team_name"]
admin.site.register(Organization,OrganizationAdmin)
class TeamUserInline(admin.TabularInline):
model = TeamUsers
extra = 0
readonly_fields = ['created', 'modified']
fields = ['team']
fk_name = 'user'
admin.site.unregister(User)
class UsrAdmin(UserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
("Personal info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
),
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
inlines = [TeamUserInline]
admin.site.register(User,UsrAdmin)

View File

@ -1,5 +1,6 @@
from django.db import models
from django.utils.timezone import now
from django.template.defaultfilters import slugify
class AutoCreatedField(models.DateTimeField):
"""
@ -10,20 +11,12 @@ class AutoCreatedField(models.DateTimeField):
"""
help_text = 'Date and time of creation'
verbose_name = 'Created'
def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", False)
kwargs.setdefault("default", now)
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
@ -35,18 +28,6 @@ class AutoLastModifiedField(AutoCreatedField):
"""
help_text = 'Date and time of last modification'
verbose_name = 'Modified'
def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = now()
@ -64,20 +45,6 @@ class AutoSlugField(models.SlugField):
"""
help_text = 'slug for this field'
verbose_name = 'Slug'
def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", self.help_text)
kwargs.setdefault("verbose_name", self.verbose_name)
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
if not model_instance.slug or model_instance.slug == '_':

View File

@ -1,9 +1,9 @@
from django import forms
from django.db.models import Q
from django.conf import settings
from app import settings
from access.models.tenant import Tenant as Organization
from access.models import Organization
from core.forms.common import CommonModelForm

View File

@ -1,13 +1,13 @@
from django import forms
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models.team import Team
from access.functions import permissions
from django.conf import settings
from access.models import Team
from core.forms.common import CommonModelForm
@ -66,4 +66,38 @@ class TeamForm(CommonModelForm):
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
self.fields['permissions'].queryset = permissions.permission_queryset()
apps = [
'access',
'assistance',
'config_management',
'core',
'django_celery_results',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'groupresult',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'add_taskresult',
'change_organization',
'change_taskresult',
'delete_organization',
'delete_taskresult',
]
self.fields['permissions'].queryset = Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -1,8 +1,8 @@
from django.db.models import Q
from django.conf import settings
from app import settings
from access.models.team_user import TeamUsers
from access.models import TeamUsers
from core.forms.common import CommonModelForm

View File

@ -1,61 +0,0 @@
from django.contrib.auth.models import Permission
def permission_queryset():
"""Filter Permissions to those used within the application
Returns:
list: Filtered queryset that only contains the used permissions
"""
apps = [
'access',
'accounting',
'assistance',
'config_management',
'core',
'devops',
'django_celery_results',
'human_resources',
'itam',
'itim',
'project_management',
'settings',
]
exclude_models = [
'appsettings',
'chordcounter',
'comment',
'groupresult',
'history',
'modelnotes',
'usersettings',
]
exclude_permissions = [
'add_checkin',
'add_history',
'add_organization',
'add_taskresult',
'add_ticketcommentaction',
'change_checkin',
'change_history',
'change_organization',
'change_taskresult',
'change_ticketcommentaction',
'delete_checkin',
'delete_history',
'delete_organization',
'delete_taskresult',
'delete_ticketcommentaction',
'view_checkin',
'view_history',
]
return Permission.objects.select_related('content_type').filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -1,160 +0,0 @@
import django
from django.contrib.auth.models import Group
from django.utils.deprecation import MiddlewareMixin
from access.models.tenant import Tenant as Organization
from access.models.team import Team
from settings.models.app_settings import AppSettings
User = django.contrib.auth.get_user_model()
class RequestTenancy(MiddlewareMixin):
"""Access Middleware
Serves the purpose of adding the users tenancy details to rhe request
object.
"""
def process_request(self, request):
request.app_settings = AppSettings.objects.select_related('global_organization').get(
owner_organization = None
)
request.tenancy = Tenancy(user = request.user, app_settings = request.app_settings)
class Tenancy:
user: User = None
groups: list([Group]) = None
_app_settings: AppSettings = None
_user_organizations: list([Organization]) = None
"""Cached User Organizations"""
_user_teams: list([Team]) = None
"""Cached User Teams"""
_user_permissions: list([str]) = None
"""Cached User User Permissions"""
def __init__(self, user: User, app_settings: AppSettings):
self.user = user
self. _app_settings = app_settings
self.groups = user.groups.select_related('team', 'team__organization').prefetch_related('team__permissions__content_type')
self._user_organizations = []
self._user_groups = []
self._user_teams = []
self._user_permissions = []
for group in self.groups:
if group.team not in self._user_teams:
self._user_teams += [ group.team ]
for permission in group.team.permissions.all():
permission_value = str( permission.content_type.app_label + '.' + permission.codename )
if permission_value not in self._user_permissions:
self._user_permissions += [ permission_value ]
if group.team.organization not in self._user_organizations:
self._user_organizations += [ group.team.organization ]
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
Returns:
bool: _description_
"""
is_member: bool = False
if organization is None:
return False
if int(organization) in self._user_organizations:
is_member = True
return is_member
def has_organization_permission(self, organization: Organization, permissions_required: str) -> bool:
""" Check if user has permission within organization.
Args:
organization (int): Organization to check.
permissions_required (list): if doing object level permissions, pass in required permission.
Returns:
bool: True for yes.
"""
has_permission: bool = False
if type(organization) is not Organization:
raise TypeError('Organization must be of type Organization')
if type(permissions_required) is not str:
raise TypeError('permissions_required must be of type str')
if not organization:
return has_permission
for team in self._user_teams:
if(
team.organization.id == int(organization)
or getattr(self._app_settings.global_organization, 'id', 0) == int(organization)
):
for permission in team.permissions.all():
assembled_permission = str(permission.content_type.app_label) + '.' + str( permission.codename )
if assembled_permission == permissions_required:
has_permission = True
return has_permission

View File

@ -44,7 +44,7 @@ class Migration(migrations.Migration):
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists])),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
],
options={
'verbose_name_plural': 'Teams',

View File

@ -1,89 +0,0 @@
# Generated by Django 5.1.2 on 2024-12-06 06:47
import access.models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='organization',
options={'ordering': ['name'], 'verbose_name': 'Organization', 'verbose_name_plural': 'Organizations'},
),
migrations.AlterModelOptions(
name='team',
options={'ordering': ['team_name'], 'verbose_name': 'Team', 'verbose_name_plural': 'Teams'},
),
migrations.AlterModelOptions(
name='teamusers',
options={'ordering': ['user'], 'verbose_name': 'Team User', 'verbose_name_plural': 'Team Users'},
),
migrations.AlterField(
model_name='organization',
name='id',
field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'),
),
migrations.AlterField(
model_name='organization',
name='manager',
field=models.ForeignKey(help_text='Manager for this organization', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Manager'),
),
migrations.AlterField(
model_name='organization',
name='model_notes',
field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='organization',
name='name',
field=models.CharField(help_text='Name of this Organization', max_length=50, unique=True, verbose_name='Name'),
),
migrations.AlterField(
model_name='team',
name='is_global',
field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'),
),
migrations.AlterField(
model_name='team',
name='model_notes',
field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization'),
),
migrations.AlterField(
model_name='team',
name='team_name',
field=models.CharField(help_text='Name to give this team', max_length=50, verbose_name='Name'),
),
migrations.AlterField(
model_name='teamusers',
name='id',
field=models.AutoField(help_text='ID of this Team User', primary_key=True, serialize=False, unique=True, verbose_name='ID'),
),
migrations.AlterField(
model_name='teamusers',
name='manager',
field=models.BooleanField(blank=True, default=False, help_text='Is this user to be a manager of this team', verbose_name='manager'),
),
migrations.AlterField(
model_name='teamusers',
name='team',
field=models.ForeignKey(help_text='Team user belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='team', to='access.team', verbose_name='Team'),
),
migrations.AlterField(
model_name='teamusers',
name='user',
field=models.ForeignKey(help_text='User who will be added to the team', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@ -1,49 +0,0 @@
# Generated by Django 5.1.5 on 2025-02-09 11:07
import access.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0002_alter_organization_options_alter_team_options_and_more'),
('core', '0012_modelnotes'),
]
operations = [
migrations.AlterField(
model_name='team',
name='organization',
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.team.Team.validatate_organization_exists], verbose_name='Organization'),
),
migrations.CreateModel(
name='OrganizationNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.organization', verbose_name='Model')),
],
options={
'verbose_name': 'Organization Note',
'verbose_name_plural': 'Organization Notes',
'db_table': 'access_organization_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
migrations.CreateModel(
name='TeamNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.team', verbose_name='Model')),
],
options={
'verbose_name': 'Team Note',
'verbose_name_plural': 'Team Notes',
'db_table': 'access_team_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.1.5 on 2025-02-20 13:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization_organizationnotes_teamnotes'),
('core', '0015_modelhistory_manufacturerhistory_and_more'),
]
operations = [
migrations.CreateModel(
name='OrganizationHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.organization', verbose_name='Model')),
],
options={
'verbose_name': 'Organization History',
'verbose_name_plural': 'Organization History',
'db_table': 'access_organization_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
migrations.CreateModel(
name='TeamHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.team', verbose_name='Model')),
],
options={
'verbose_name': 'Team History',
'verbose_name_plural': 'Team History',
'db_table': 'access_team_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
]

View File

@ -1,140 +0,0 @@
# Generated by Django 5.2 on 2025-04-10 02:34
import access.fields
import access.models.tenancy
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0004_organizationhistory_teamhistory'),
('auth', '0012_alter_user_first_name_max_length'),
('core', '0021_alter_ticketlinkeditem_item_type'),
]
operations = [
migrations.CreateModel(
name='Entity',
fields=[
('is_global', models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object')),
('model_notes', models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('entity_type', models.CharField(default='entity', help_text='Type this entity is', max_length=30, verbose_name='Entity Type')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
],
options={
'verbose_name': 'Entity',
'verbose_name_plural': 'Entities',
'ordering': ['created', 'modified', 'organization'],
},
),
migrations.CreateModel(
name='Person',
fields=[
('entity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.entity')),
('f_name', models.CharField(help_text='The persons first name', max_length=64, verbose_name='First Name')),
('m_name', models.CharField(blank=True, default=None, help_text='The persons middle name(s)', max_length=100, null=True, verbose_name='Middle Name(s)')),
('l_name', models.CharField(help_text='The persons Last name', max_length=64, verbose_name='Last Name')),
('dob', models.DateField(blank=True, default=None, help_text='The Persons Date of Birth (DOB)', null=True, verbose_name='DOB')),
],
options={
'verbose_name': 'Person',
'verbose_name_plural': 'People',
'ordering': ['l_name', 'm_name', 'f_name', 'dob'],
},
bases=('access.entity',),
),
migrations.CreateModel(
name='EntityHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.entity', verbose_name='Model')),
],
options={
'verbose_name': 'Entity History',
'verbose_name_plural': 'Entity History',
'db_table': 'access_entity_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
migrations.CreateModel(
name='EntityNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.entity', verbose_name='Model')),
],
options={
'verbose_name': 'Entity Note',
'verbose_name_plural': 'Entity Notes',
'db_table': 'access_entity_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
migrations.CreateModel(
name='Role',
fields=[
('model_notes', models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes')),
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('name', models.CharField(help_text='Name of this role', max_length=30, verbose_name='Name')),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
('permissions', models.ManyToManyField(blank=True, help_text='Permissions part of this role', related_name='roles', to='auth.permission', verbose_name='Permissions')),
],
options={
'verbose_name': 'Role',
'verbose_name_plural': 'Roles',
'ordering': ['organization', 'name'],
'unique_together': {('organization', 'name')},
},
),
migrations.CreateModel(
name='RoleHistory',
fields=[
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.role', verbose_name='Model')),
],
options={
'verbose_name': 'Role History',
'verbose_name_plural': 'Role History',
'db_table': 'access_role_history',
'ordering': ['-created'],
},
bases=('core.modelhistory',),
),
migrations.CreateModel(
name='RoleNotes',
fields=[
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.role', verbose_name='Model')),
],
options={
'verbose_name': 'Role Note',
'verbose_name_plural': 'Role Notes',
'db_table': 'access_role_notes',
'ordering': ['-created'],
},
bases=('core.modelnotes',),
),
migrations.CreateModel(
name='Contact',
fields=[
('person_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.person')),
('directory', models.BooleanField(blank=True, default=True, help_text='Show contact details in directory', verbose_name='Show in Directory')),
('email', models.EmailField(help_text='E-mail address for this person', max_length=254, unique=True, verbose_name='E-Mail')),
],
options={
'verbose_name': 'Contact',
'verbose_name_plural': 'Contacts',
'ordering': ['email'],
},
bases=('access.person',),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-14 11:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('access', '0005_entity_person_entityhistory_entitynotes_role_and_more'),
('assistance', '0005_knowledgebasecategoryhistory_knowledgebasehistory'),
('config_management', '0007_configgroupshistory_configgrouphostshistory_and_more'),
('core', '0022_ticketcommentbase_ticketbase_ticketcommentsolution_and_more'),
('devops', '0011_alter_gitgroup_unique_together_and_more'),
('itam', '0010_alter_software_organization'),
('itim', '0009_slmticket_requestticket'),
('project_management', '0005_projecthistory_projectmilestonehistory_and_more'),
('settings', '0011_appsettingshistory_externallinkhistory'),
]
operations = [
migrations.RenameModel(
old_name = 'Organization',
new_name = 'Tenant'
),
]

View File

@ -1,47 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-14 13:48
import access.models.team
import access.models.tenancy
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0007_rename_organization_tenant'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='tenant',
options={'ordering': ['name'], 'verbose_name': 'Tenant', 'verbose_name_plural': 'Tenants'},
),
migrations.AlterField(
model_name='entity',
name='organization',
field=models.ForeignKey(help_text='Tenancy this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.tenant', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Tenant'),
),
migrations.AlterField(
model_name='role',
name='organization',
field=models.ForeignKey(help_text='Tenancy this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.tenant', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Tenant'),
),
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='tenant',
name='manager',
field=models.ForeignKey(help_text='Manager for this Tenancy', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Manager'),
),
migrations.AlterField(
model_name='tenant',
name='name',
field=models.CharField(help_text='Name of this Tenancy', max_length=50, unique=True, verbose_name='Name'),
),
]

View File

@ -1,135 +0,0 @@
from django.contrib.auth.models import ContentType, Permission
from django.db import migrations
from access.models.team import Team
ContentType.DoesNotExist
def add_tenancy_permissions(apps, schema_editor):
print('')
print(f"Begin permission migration for rename of Organization to Tenant.")
try:
add_permission = Permission.objects.get(
codename = 'add_tenant',
content_type = ContentType.objects.get(
app_label = 'access',
model = 'tenant',
)
)
change_permission = Permission.objects.get(
codename = 'change_tenant',
content_type = ContentType.objects.get(
app_label = 'access',
model = 'tenant',
)
)
delete_permission = Permission.objects.get(
codename = 'delete_tenant',
content_type = ContentType.objects.get(
app_label = 'access',
model = 'tenant',
)
)
view_permission = Permission.objects.get(
codename = 'view_tenant',
content_type = ContentType.objects.get(
app_label = 'access',
model = 'tenant',
)
)
print(f' Searching for Teams.')
teams = Team.objects.select_related('group_ptr__permissions')
print(f'Found {str(len(teams))} Teams.')
for team in teams:
print(f' Processing Team {str(team.team_name)}.')
permissions = team.group_ptr.permissions.all()
print(f' Searching for Organization Permissions.')
print(f' Found {str(len(permissions))} Permissions.')
for permission in permissions:
if '_organization' not in permission.codename:
continue
action = str(permission.codename).split('_')[0]
print(f' Found Organization Permission {str(action)}')
if action == 'add':
team.group_ptr.permissions.add( add_permission )
print(f' Add Tenant Permission {str(action)}')
team.group_ptr.permissions.remove( permission )
print(f' Remove Organization Permission {str(action)}')
elif action == 'change':
team.group_ptr.permissions.add( change_permission )
print(f' Add Tenant Permission {str(action)}')
team.group_ptr.permissions.remove( permission )
print(f' Remove Organization Permission {str(action)}')
elif action == 'delete':
team.group_ptr.permissions.add( delete_permission )
print(f' Add Tenant Permission {str(action)}')
team.group_ptr.permissions.remove( permission )
print(f' Remove Organization Permission {str(action)}')
elif action == 'view':
team.group_ptr.permissions.add( view_permission )
print(f' Add Tenant Permission {str(action)}')
team.group_ptr.permissions.remove( permission )
print(f' Remove Organization Permission {str(action)}')
print(f' Completed Team {str(team.team_name)}.')
except ContentType.DoesNotExist:
# DB is new so no content types. no migration to be done.
pass
print(' Permission Migration Actions Complete.')
class Migration(migrations.Migration):
dependencies = [
('access', '0008_alter_tenant_options_alter_entity_organization_and_more'),
]
operations = [
migrations.RunPython(add_tenancy_permissions),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-16 09:58
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0009_migrate_organization_permission_tenant'),
]
operations = [
migrations.CreateModel(
name='Company',
fields=[
('entity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.entity')),
('name', models.CharField(help_text='The name of this entity', max_length=80, verbose_name='Name')),
],
options={
'verbose_name': 'Company',
'verbose_name_plural': 'Companies',
'ordering': ['name'],
'sub_model_type': 'company',
},
bases=('access.entity',),
),
migrations.AlterField(
model_name='entity',
name='entity_type',
field=models.CharField(help_text='Type this entity is', max_length=30, verbose_name='Entity Type'),
),
migrations.AlterField(
model_name='person',
name='dob',
field=models.DateField(blank=True, help_text='The Persons Date of Birth (DOB)', null=True, verbose_name='DOB'),
),
migrations.AlterField(
model_name='person',
name='m_name',
field=models.CharField(blank=True, help_text='The persons middle name(s)', max_length=100, null=True, verbose_name='Middle Name(s)'),
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-17 17:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('access', '0010_company_alter_entity_entity_type_alter_person_dob_and_more'),
('core', '0026_rename_manufacturerhistory_manufactureraudithistory'),
]
operations = [
migrations.RenameModel(
old_name='EntityHistory',
new_name='EntityAuditHistory',
),
migrations.RenameModel(
old_name='OrganizationHistory',
new_name='OrganizationAuditHistory',
),
migrations.RenameModel(
old_name='RoleHistory',
new_name='RoleAuditHistory',
),
migrations.RenameModel(
old_name='TeamHistory',
new_name='TeamAuditHistory',
),
]

View File

@ -1,30 +1,15 @@
from django.contrib.auth.mixins import AccessMixin
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
from .models import Organization, 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 = []
@ -41,24 +26,20 @@ class OrganizationMixin():
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
return self.parent_model.objects.get(pk=self.kwargs['pk'])
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:
if hasattr(self, 'parent_model'):
obj = self.get_parent_obj()
id = obj.get_organization().id
@ -80,10 +61,6 @@ class OrganizationMixin():
id = 0
if hasattr(self, 'instance') and id is None: # Form Instance
id = self.instance.get_organization()
except AttributeError:
@ -107,10 +84,6 @@ class OrganizationMixin():
pass
if id is not None:
self._object_organization = id
return id
@ -126,13 +99,9 @@ class OrganizationMixin():
is_member = False
if organization is None:
if organization in self.user_organizations():
return False
if int(organization) in self.user_organizations():
is_member = True
return True
return is_member
@ -142,10 +111,6 @@ class OrganizationMixin():
Override of 'PermissionRequiredMixin' method so that this mixin can obtain the required permission.
"""
if not hasattr(self, 'permission_required'):
return []
if self.permission_required is None:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the "
@ -182,10 +147,6 @@ class OrganizationMixin():
user_organizations = []
if hasattr(self, '_user_organizations'):
return self._user_organizations
teams = Team.objects
for group in self.request.user.groups.all():
@ -196,41 +157,18 @@ class OrganizationMixin():
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.
"""
def has_organization_permission(self, organization: int=None) -> bool:
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)
@ -244,7 +182,7 @@ class OrganizationMixin():
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):
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
return True
@ -260,7 +198,7 @@ class OrganizationMixin():
self.permission_required = permissions_required
organization_manager_models = [
'access.tenant',
'access.organization',
'access.team',
'access.teamusers',
]
@ -304,29 +242,21 @@ class OrganizationMixin():
return True
if permissions_required:
perms = self.get_permission_required()
perms = permissions_required
else:
perms = self.get_permission_required()
if self.has_organization_permission(permissions_required = perms):
if self.has_organization_permission():
return True
if self.request.user.has_perms(perms) and str(self.request.method).lower() == 'get':
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 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
return True
for required_permission in self.permission_required:
if required_permission.replace(
'view_', ''
) == 'access.tenant' and len(self.kwargs) == 0:
) == 'access.organization' and len(self.kwargs) == 0:
return True
@ -397,12 +327,6 @@ class OrganizationPermission(AccessMixin, OrganizationMixin):
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:

View File

@ -1,274 +0,0 @@
import django
from django.db import models
from access.models.tenant import Tenant as Organization
User = django.contrib.auth.get_user_model()
class OrganizationMixin:
"""Organization Tenancy Mixin
This class is intended to be included in **ALL** View / Viewset classes as
it contains the functions/methods required to conduct the permission
checking.
"""
_obj_organization: int = None
"""Cached Object Organization"""
def get_obj_organization(self, obj = None, request = None) -> Organization:
"""Fetch the objects Organization
Args:
obj (Model): Model of object
Raises:
ValueError: When `obj` and `request` are both missing
Returns:
Organization: Organization the object is from
None: No Organization was found
"""
if obj is None and request is None:
raise ValueError('Missing Parameter. obj or request must be supplied')
if self._obj_organization:
return self._obj_organization
if obj:
self._obj_organization = getattr(obj, 'organization', None)
if not self._obj_organization:
self._obj_organization = getattr(obj, 'get_organization', lambda: None)()
elif (
request
and not self.kwargs.get('pk', None)
):
if getattr(request.stream, 'method', '') != 'DELETE':
data = getattr(request, 'data', None)
if data:
data_organization = self.kwargs.get('organization_id', None)
if not data_organization:
data_organization = request.data.get('organization_id', None)
if not data_organization:
data_organization = request.data.get('organization', None)
if data_organization:
self._obj_organization = Organization.objects.get(
pk = int( data_organization )
)
elif self.kwargs.get('pk', None):
obj = self.model.objects.get( pk = self.kwargs.get('pk', None) )
if getattr(obj, 'organization', None):
self._obj_organization = obj.organization
elif str(self.model._meta.verbose_name).lower() == 'tenant':
self._obj_organization = obj
if self.get_parent_model(): # if defined is to overwrite object organization
parent_obj = self.get_parent_obj()
self._obj_organization = parent_obj.get_organization()
return self._obj_organization
def get_parent_model(self):
"""Get the Parent Model
This function exists so that dynamic parent models can be defined.
They are defined by overriding this method.
Returns:
Model: Parent Model
"""
return self.parent_model
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.get_parent_model().objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
def get_permission_organizations(self, permission: str ) -> list([ int ]):
"""Return Organization(s) the permission belongs to
Searches the users organizations for the required permission, if found
the organization is added to the list to return.
Args:
permission (str): Permission to search users organizations for
Returns:
Organizations (list): All Organizations where the permission was found.
"""
_permission_organizations: list = []
for team in self.request.tenancy._user_teams:
for team_permission in team.permissions.all():
permission_value = str( team_permission.content_type.app_label + '.' + team_permission.codename )
if permission_value == permission:
_permission_organizations += [ team.organization.id ]
return _permission_organizations
_permission_required: str = None
"""Cached Permissions required"""
def get_permission_required(self) -> str:
""" Get / Generate Permission Required
If there is a requirement that there be custom/dynamic permissions,
this function can be safely overridden.
Raises:
ValueError: Unable to determin the view action
Returns:
str: Permission in format `<app_name>.<action>_<model_name>`
"""
if self._permission_required:
return self._permission_required
if hasattr(self, 'get_dynamic_permissions'):
self._permission_required = self.get_dynamic_permissions()
if type(self._permission_required) is list:
self._permission_required = self._permission_required[0]
return self._permission_required
view_action: str = None
if(
self.action == 'create'
or getattr(self.request._stream, 'method', '') == 'POST'
):
view_action = 'add'
elif (
self.action == 'partial_update'
or self.action == 'update'
or getattr(self.request._stream, 'method', '') == 'PATCH'
or getattr(self.request._stream, 'method', '') == 'PUT'
):
view_action = 'change'
elif(
self.action == 'destroy'
or getattr(self.request._stream, 'method', '') == 'DELETE'
):
view_action = 'delete'
elif (
self.action == 'list'
):
view_action = 'view'
elif self.action == 'retrieve':
view_action = 'view'
elif self.action == 'metadata':
view_action = 'view'
elif self.action is None:
return False
if view_action is None:
raise ValueError('view_action could not be defined.')
permission = self.model._meta.app_label + '.' + view_action + '_' + self.model._meta.model_name
permission_required = permission
self._permission_required = permission_required
return self._permission_required
parent_model: models.Model = 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).
"""

View File

@ -1,319 +0,0 @@
import traceback
from rest_framework.permissions import DjangoObjectPermissions
from access.models.tenancy import Tenant, TenancyObject
from core import exceptions as centurion_exceptions
class OrganizationPermissionMixin(
DjangoObjectPermissions,
):
"""Tenant Permission Mixin
This class is to be used as the permission class for API `Views`/`ViewSets`.
In combination with the `TenantPermissionsMixin`, permission checking
will be done to ensure the user has the correct permissions to perform the
CRUD operation.
**Note:** If the user is not authenticated, they will be denied access
globally.
Permissions are broken down into two areas:
- `Tenancy` Objects
This object requires that the user have the correct permission and that
permission be assigned within the organiztion the object belongs to.
- `Non-Tenancy` Objects.
This object requires the the use have the correct permission assigned,
regardless of the organization the object is from. This includes objects
that have no organization.
"""
_is_tenancy_model: bool = None
def is_tenancy_model(self, view) -> bool:
"""Determin if the Model is a `Tenancy` Model
Will look at the model defined within the view unless a parent
model is found. If the latter is true, the parent_model will be used to
determin if the model is a `Tenancy` model
Args:
view (object): The View the HTTP request was mad to
Returns:
True (bool): Model is a Tenancy Model.
False (bool): Model is not a Tenancy model.
"""
if not self._is_tenancy_model:
if hasattr(view, 'model'):
self._is_tenancy_model = issubclass(view.model, TenancyObject)
if view.get_parent_model():
self._is_tenancy_model = issubclass(view.get_parent_model(), TenancyObject)
return self._is_tenancy_model
def has_permission(self, request, view):
""" Check if user has the required permission
Permission flow is as follows:
- Un-authenticated users. Access Denied
- Authenticated user whom make a request using wrong method. Access
Denied
- Authenticated user who is not in same organization as object. Access
Denied
- Authenticated user who is in same organization as object, however is
missing the correct permission. Access Denied
Depending upon user type, they will recieve different feedback. In order
they are:
- Non-authenticated users will **always** recieve HTTP/401
- Authenticated users who use an unsupported method, HTTP/405
- Authenticated users missing the correct permission recieve HTTP/403
Args:
request (object): The HTTP Request Object
view (_type_): The View/Viewset Object the request was made to
Raises:
PermissionDenied: User does not have the required permission.
NotAuthenticated: User is not logged into Centurion.
ValueError: Could not determin the view action.
Returns:
True (bool): User has the required permission.
False (bool): User does not have the required permission
"""
if request.user.is_anonymous:
raise centurion_exceptions.NotAuthenticated()
try:
if (
(
view.model.__name__ == 'UserSettings'
and request._user.id == int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id == int(view.kwargs.get('model_id', 0))
)
):
return True
elif (
(
view.model.__name__ == 'UserSettings'
and request._user.id != int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id != int(view.kwargs.get('model_id', 0))
)
):
return False
has_permission_required: bool = False
user_permissions = request.tenancy._user_permissions
permission_required = view.get_permission_required()
if permission_required and user_permissions:
# No permission_required couldnt get permissions
# No user_permissions, user missing the required permission
has_permission_required: bool = permission_required in user_permissions
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()
obj_organization: Tenant = view.get_obj_organization(
request = request
)
view_action: str = None
if(
view.action == 'create'
and request.method == 'POST'
):
view_action = 'add'
elif(
view.action == 'destroy'
and request.method == 'DELETE'
):
view_action = 'delete'
elif (
view.action == 'list'
):
view_action = 'view'
elif (
view.action == 'partial_update'
and request.method == 'PATCH'
):
view_action = 'change'
elif (
view.action == 'update'
and request.method == 'PUT'
):
view_action = 'change'
elif(
view.action == 'retrieve'
and request.method == 'GET'
):
view_action = 'view'
elif(
view.action == 'metadata'
and request.method == 'OPTIONS'
):
return True
if view_action is None:
raise ValueError('view_action could not be defined.')
if obj_organization is None or request.user.is_superuser:
return True
elif obj_organization is not None:
if request.tenancy.has_organization_permission(
organization = obj_organization,
permissions_required = view.get_permission_required()
):
return True
except ValueError as e:
# ToDo: This exception could be used in traces as it provides
# information as to dodgy requests. This exception is raised
# when the method does not match the view action.
print(traceback.format_exc())
except centurion_exceptions.Http404 as e:
# This exception genrally means that the user is not in the same
# organization as the object as objects are filtered to users
# organizations ONLY.
pass
except centurion_exceptions.ObjectDoesNotExist as e:
# This exception genrally means that the user is not in the same
# organization as the object as objects are filtered to users
# organizations ONLY.
pass
except centurion_exceptions.PermissionDenied as e:
# This Exception will be raised after this function has returned
# False.
pass
return False
def has_object_permission(self, request, view, obj):
try:
if request.user.is_anonymous:
return False
if (
(
view.model.__name__ == 'UserSettings'
and request._user.id == int(view.kwargs.get('pk', 0))
)
or (
view.model.__name__ == 'AuthToken'
and request._user.id == int(view.kwargs.get('model_id', 0))
)
):
return True
object_organization = view._obj_organization
if object_organization:
if(
int(object_organization)
in view.get_permission_organizations( view.get_permission_required() )
or request.user.is_superuser
or getattr(request.app_settings.global_organization, 'id', 0) == int(object_organization)
):
return True
elif not self.is_tenancy_model( view ) or request.user.is_superuser:
return True
except Exception as e:
print(traceback.format_exc())
return False

324
app/access/models.py Normal file
View File

@ -0,0 +1,324 @@
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User, Group, Permission
from django.forms import ValidationError
from .fields import *
from core.middleware.get_request import get_request
from core.mixin.history_save import SaveHistory
class Organization(SaveHistory):
class Meta:
verbose_name_plural = "Organizations"
ordering = ['name']
def save(self, *args, **kwargs):
if self.slug == '_':
self.slug = self.name.lower().replace(' ', '_')
super().save(*args, **kwargs)
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
name = models.CharField(
blank = False,
max_length = 50,
unique = True,
)
manager = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank = False,
null = True,
help_text = 'Organization Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
created = AutoCreatedField()
modified = AutoLastModifiedField()
def get_organization(self):
return self
def __str__(self):
return self.name
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
This function filters the data fetched from the database to that which is from the organizations
the user is a part of.
!!! danger "Requirement"
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
## Workflow
This functions workflow is as follows:
- Fetch the user from the request
- Check if the user is authenticated
- Iterate over the users teams
- Store unique organizations from users teams
- return results
Returns:
(queryset): **super user**: return unfiltered data.
(queryset): **not super user**: return data from the stored unique organizations.
"""
request = get_request()
user_organizations: list(str()) = []
if request:
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
if user.is_authenticated:
for team_user in TeamUsers.objects.filter(user=user):
if team_user.team.organization.name not in user_organizations:
if not user_organizations:
self.user_organizations = []
user_organizations += [ team_user.team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
return super().get_queryset()
class TenancyObject(SaveHistory):
""" Tenancy Model Abstrct class.
This class is for inclusion wihtin **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 = TenancyManager()
""" Multi-Tenanant Objects """
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('You must provide an organization')
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
blank = False,
null = True,
validators = [validatate_organization_exists],
)
is_global = models.BooleanField(
default = False,
blank = False
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
verbose_name = 'Notes',
)
def get_organization(self) -> Organization:
return self.organization
class Team(Group, TenancyObject):
class Meta:
# proxy = True
verbose_name_plural = "Teams"
ordering = ['team_name']
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
team_name = models.CharField(
verbose_name = 'Name',
blank = False,
max_length = 50,
unique = False,
default = ''
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
@property
def parent_object(self):
""" Fetch the parent object """
return self.organization
def permission_list(self) -> list:
permission_list = []
for permission in self.permissions.all():
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
continue
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
return [permission_list, self.permissions.all()]
def __str__(self):
return self.team_name
class TeamUsers(SaveHistory):
class Meta:
# proxy = True
verbose_name_plural = "Team Users"
ordering = ['user']
id = models.AutoField(
primary_key=True,
unique=True,
blank=False
)
team = models.ForeignKey(
Team,
related_name="team",
on_delete=models.CASCADE)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
manager = models.BooleanField(
verbose_name='manager',
default=False,
blank=True
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def delete(self, using=None, keep_parents=False):
""" Delete Team
Overrides, post-action
As teams are an extension of Groups, remove the user to the team.
"""
super().delete(using=using, keep_parents=keep_parents)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.remove(group)
def get_organization(self) -> Organization:
return self.team.organization
def save(self, *args, **kwargs):
""" Save Team
Overrides, post-action
As teams are an extension of groups, add the user to the matching group.
"""
super().save(*args, **kwargs)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.add(group)
@property
def parent_object(self):
""" Fetch the parent object """
return self.team
def __str__(self):
return self.user.username

View File

@ -1,4 +0,0 @@
from . import contact # pylint: disable=W0611:unused-import
from . import company_base # pylint: disable=W0611:unused-import
from . import person # pylint: disable=W0611:unused-import
from . import role # pylint: disable=W0611:unused-import

View File

@ -1,102 +0,0 @@
from django.db import models
from access.models.entity import Entity
class Company(
Entity
):
# This model is intended to be called `Organization`, however at the time of
# creation this was not possible as Tenant (ne Organization) still has
# references in code to `organization` witch clashes with the intended name of
# this model.
class Meta:
ordering = [
'name',
]
sub_model_type = 'company'
verbose_name = 'Company'
verbose_name_plural = 'Companies'
name = models.CharField(
blank = False,
help_text = 'The name of this entity',
max_length = 80,
unique = False,
verbose_name = 'Name'
)
def __str__(self) -> str:
return self.name
documentation = ''
history_model_name = 'company'
page_layout: dict = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'name',
],
"right": [
'model_notes',
'created',
'modified',
]
}
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Tickets",
"slug": "tickets",
"sections": [
{
"layout": "table",
"field": "tickets",
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
'name',
'organization',
'created',
]
def clean(self):
super().clean()

View File

@ -1,119 +0,0 @@
from django.db import models
from access.models.person import Person
class Contact(
Person
):
class Meta:
ordering = [
'email',
]
sub_model_type = 'contact'
verbose_name = 'Contact'
verbose_name_plural = 'Contacts'
directory = models.BooleanField(
blank = True,
default = True,
help_text = 'Show contact details in directory',
null = False,
verbose_name = 'Show in Directory',
)
email = models.EmailField(
blank = False,
help_text = 'E-mail address for this person',
unique = True,
verbose_name = 'E-Mail',
)
def __str__(self) -> str:
return self.f_name + ' ' + self.l_name
documentation = ''
history_model_name = 'contact'
page_layout: list = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'created',
'modified',
],
"right": [
'model_notes',
'directory',
]
},
{
"name": "Personal Details",
"layout": "double",
"left": [
'display_name',
'dob',
],
"right": [
'f_name',
'm_name',
'l_name',
]
},
{
"name": "",
"layout": "double",
"left": [
'email',
],
"right": [
'',
]
}
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
{
"field": "display_name",
"type": "link",
"key": "_self"
},
'f_name',
'l_name',
'email',
'organization',
'created',
]

View File

@ -1,246 +0,0 @@
from django.db import models
from rest_framework.reverse import reverse
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
class Entity(
TenancyObject
):
class Meta:
ordering = [
'created',
'modified',
'organization',
]
sub_model_type = 'entity'
verbose_name = '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',
max_length = 30,
unique = False,
verbose_name = 'Entity Type'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
def __str__(self) -> str:
related_model = self.get_related_model()
if related_model is None:
return f'{self.entity_type} {self.pk}'
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',
'display_name',
'created',
'modified',
]
def get_related_field_name(self) -> str:
meta = getattr(self, '_meta')
for related_object in getattr(meta, 'related_objects', []):
if not issubclass(related_object.related_model, Entity):
continue
if getattr(self, related_object.name, None):
if(
not str(related_object.name).endswith('history')
and not str(related_object.name).endswith('notes')
):
return related_object.name
return ''
def get_related_model(self):
"""Recursive model Fetch
Returns the lowest model found in a chain of inherited models.
Args:
model (models.Model, optional): Model to fetch the child model from. Defaults to None.
Returns:
models.Model: Lowset model found in inherited model chain
"""
related_model_name = self.get_related_field_name()
related_model = getattr(self, related_model_name, None)
if related_model_name == '':
related_model = None
elif related_model is None:
related_model = self
elif hasattr(related_model, 'get_related_field_name'):
if related_model.get_related_field_name() != '':
related_model = related_model.get_related_model()
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 EntityAuditHistory
history = super().save_history(
before = before,
after = after,
history_model = EntityAuditHistory
)
return history

View File

@ -1,53 +0,0 @@
from django.db import models
from access.models.entity import Entity
from core.models.model_history import ModelHistory
class EntityAuditHistory(
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

@ -1,45 +0,0 @@
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 +0,0 @@
from .tenant import Tenant as Organization # pylint: disable=W0611:unused-import

View File

@ -1,53 +0,0 @@
from django.db import models
from core.models.model_history import ModelHistory
from access.models.tenant import Tenant
class OrganizationAuditHistory(
ModelHistory
):
class Meta:
db_table = 'access_organization_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Organization History'
verbose_name_plural = 'Organization History'
model = models.ForeignKey(
Tenant,
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.organization import TenantBaseSerializer
model = TenantBaseSerializer(self.model, context = serializer_context)
return model

View File

@ -1,45 +0,0 @@
from django.db import models
from access.models.tenant import Tenant
from core.models.model_notes import ModelNotes
class OrganizationNotes(
ModelNotes
):
class Meta:
db_table = 'access_organization_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Organization Note'
verbose_name_plural = 'Organization Notes'
model = models.ForeignKey(
Tenant,
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,119 +0,0 @@
from django.db import models
from core.exceptions import ValidationError
from access.models.entity import Entity
class Person(
Entity
):
class Meta:
ordering = [
'l_name',
'm_name',
'f_name',
'dob',
]
sub_model_type = 'person'
verbose_name = 'Person'
verbose_name_plural = 'People'
f_name = models.CharField(
blank = False,
help_text = 'The persons first name',
max_length = 64,
unique = False,
verbose_name = 'First Name'
)
m_name = models.CharField(
blank = True,
help_text = 'The persons middle name(s)',
max_length = 100,
null = True,
unique = False,
verbose_name = 'Middle Name(s)'
)
l_name = models.CharField(
blank = False,
help_text = 'The persons Last name',
max_length = 64,
unique = False,
verbose_name = 'Last Name'
)
dob = models.DateField(
blank = True,
help_text = 'The Persons Date of Birth (DOB)',
null = True,
unique = False,
verbose_name = 'DOB',
)
def __str__(self) -> str:
return self.f_name + ' ' + self.l_name + f' (DOB: {self.dob})'
documentation = ''
history_model_name = 'person'
page_layout: dict = []
table_fields: list = [
'organization',
'f_name',
'l_name',
'dob',
'created',
]
def clean(self):
super().clean()
if self.dob is not None:
if self.pk:
duplicate_entry = Person.objects.filter(
f_name = self.f_name,
l_name = self.l_name,
).exclude(
pk = self.pk
)
else:
duplicate_entry = Person.objects.filter(
f_name = self.f_name,
l_name = self.l_name,
)
for entry in duplicate_entry:
if(
entry.f_name == self.f_name
and entry.m_name == self.m_name
and entry.l_name == self.l_name
and entry.dob == self.dob
):
raise ValidationError(
detail = {
'dob': f'Person {self.f_name} {self.l_name} already exists with this birthday {entry.dob}'
},
code = 'duplicate_person_on_dob'
)

View File

@ -1,174 +0,0 @@
from django.contrib.auth.models import Permission
from django.db import models
from access.fields import AutoCreatedField, AutoLastModifiedField
from access.models.tenancy import TenancyObject
class Role(
TenancyObject
):
class Meta:
ordering = [
'organization',
'name',
]
unique_together = [
'organization',
'name'
]
verbose_name = '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',
max_length = 30,
unique = False,
verbose_name = 'Name'
)
permissions = models.ManyToManyField(
Permission,
blank = True,
help_text = 'Permissions part of this role',
related_name = 'roles',
symmetrical = False,
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",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'name',
'created',
'modified',
],
"right": [
'model_notes',
]
},
{
"layout": "single",
"name": "Permissions",
"fields": [
"permissions",
]
},
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Tickets",
"slug": "tickets",
"sections": [
{
"layout": "table",
"field": "tickets",
}
],
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
'organization',
'name',
'created',
'modified',
]
_permissions: list[ Permission ] = None
_permissions_int: list[ int ] = None
def get_permissions(self, as_int_list = False ):
if self._permissions is None:
permissions = []
permissions_int = []
for permission in self.permissions:
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
def save_history(self, before: dict, after: dict) -> bool:
from access.models.role_history import RoleAuditHistory
history = super().save_history(
before = before,
after = after,
history_model = RoleAuditHistory
)
return history

View File

@ -1,53 +0,0 @@
from django.db import models
from core.models.model_history import ModelHistory
from access.models.role import Role
class RoleAuditHistory(
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

@ -1,45 +0,0 @@
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

@ -1,191 +0,0 @@
from django.db import models
from django.contrib.auth.models import Group
from rest_framework.reverse import reverse
from access.fields import (
AutoCreatedField,
AutoLastModifiedField
)
from access.models.tenant import Tenant
from access.models.tenancy import TenancyObject
from core import exceptions as centurion_exceptions
class Team(Group, TenancyObject):
class Meta:
ordering = [ 'team_name' ]
verbose_name = 'Team'
verbose_name_plural = "Teams"
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.organization_id:
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def validatate_organization_exists(self):
"""Ensure that the user did provide an organization
Raises:
ValidationError: User failed to supply organization.
"""
if not self:
raise centurion_exceptions.ValidationError('You must provide an organization')
team_name = models.CharField(
blank = False,
help_text = 'Name to give this team',
max_length = 50,
unique = False,
verbose_name = 'Name',
)
organization = models.ForeignKey(
Tenant,
blank = False,
help_text = 'Tenant this belongs to',
null = False,
on_delete = models.CASCADE,
validators = [validatate_organization_exists],
verbose_name = 'Tenant'
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
page_layout: dict = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'organization',
'team_name',
'created',
'modified',
],
"right": [
'model_notes',
]
},
{
"layout": "table",
"name": "Users",
"field": "users",
},
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
},
]
table_fields: list = [
'team_name',
'modified',
'created',
]
def get_url( self, request = None ) -> str:
if request:
return reverse(f"v2:_api_v2_organization_team-detail", request=request, kwargs = self.get_url_kwargs() )
return reverse(f"v2:_api_v2_organization_team-detail", kwargs = self.get_url_kwargs() )
def get_url_kwargs(self) -> dict:
"""Fetch the URL kwargs
Returns:
dict: kwargs required for generating the URL with `reverse`
"""
return {
'organization_id': self.organization.id,
'pk': self.id
}
def get_url_kwargs_notes(self) -> dict:
"""Fetch the URL kwargs for model notes
Returns:
dict: notes kwargs required for generating the URL with `reverse`
"""
return {
'organization_id': self.organization.id,
'model_id': self.id
}
# @property
# def parent_object(self):
# """ Fetch the parent object """
# return self.organization
def permission_list(self) -> list:
permission_list = []
for permission in self.permissions.all():
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
continue
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
return [permission_list, self.permissions.all()]
def __str__(self):
return self.organization.name + ', ' + self.team_name
def save_history(self, before: dict, after: dict) -> bool:
from access.models.team_history import TeamAuditHistory
history = super().save_history(
before = before,
after = after,
history_model = TeamAuditHistory
)
return history

View File

@ -1,53 +0,0 @@
from django.db import models
from core.models.model_history import ModelHistory
from access.models.team import Team
class TeamAuditHistory(
ModelHistory
):
class Meta:
db_table = 'access_team_history'
ordering = ModelHistory._meta.ordering
verbose_name = 'Team History'
verbose_name_plural = 'Team History'
model = models.ForeignKey(
Team,
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.teams import TeamBaseSerializer
model = TeamBaseSerializer(self.model, context = serializer_context)
return model

View File

@ -1,54 +0,0 @@
from django.db import models
from rest_framework.reverse import reverse
from access.models.team import Team
from core.models.model_notes import ModelNotes
class TeamNotes(
ModelNotes
):
class Meta:
db_table = 'access_team_notes'
ordering = ModelNotes._meta.ordering
verbose_name = 'Team Note'
verbose_name_plural = 'Team Notes'
model = models.ForeignKey(
Team,
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( self, request = None ) -> str:
kwargs = {
'organization_id': self.organization.pk,
'model_id': self.model.pk,
'pk': self.pk
}
if request:
return reverse("v2:_api_v2_team_note-detail", request=request, kwargs = kwargs )
return reverse("v2:_api_v2_team_note-detail", kwargs = kwargs )

View File

@ -1,152 +0,0 @@
import django
from django.conf import settings
from django.db import models
from django.contrib.auth.models import Group
from rest_framework.reverse import reverse
from access.fields import (
AutoCreatedField,
AutoLastModifiedField
)
from access.models.tenant import Tenant
from access.models.team import Team
from core.lib.feature_not_used import FeatureNotUsed
from core.mixin.history_save import SaveHistory
User = django.contrib.auth.get_user_model()
class TeamUsers(SaveHistory):
class Meta:
ordering = ['user']
verbose_name = "Team User"
verbose_name_plural = "Team Users"
id = models.AutoField(
blank=False,
help_text = 'ID of this Team User',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
team = models.ForeignKey(
Team,
blank = False,
help_text = 'Team user belongs to',
null = False,
on_delete=models.CASCADE,
related_name="team",
verbose_name = 'Team'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank = False,
help_text = 'User who will be added to the team',
null = False,
on_delete=models.CASCADE,
verbose_name = 'User'
)
manager = models.BooleanField(
blank=True,
default=False,
help_text = 'Is this user to be a manager of this team',
verbose_name='manager',
)
created = AutoCreatedField()
modified = AutoLastModifiedField()
page_layout: list = []
table_fields: list = [
'user',
'manager'
]
history_app_label: str = None
history_model_name: str = None
kb_model_name: str = None
note_basename: str = None
def delete(self, using=None, keep_parents=False):
""" Delete Team
Overrides, post-action
As teams are an extension of Groups, remove the user to the team.
"""
super().delete(using=using, keep_parents=keep_parents)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.remove(group)
def get_organization(self) -> Tenant:
return self.team.organization
def get_url( self, request = None ) -> str:
url_kwargs: dict = {
'organization_id': self.team.organization.id,
'team_id': self.team.id,
'pk': self.id
}
print(f'url kwargs are: {url_kwargs}')
if request:
return reverse(f"v2:_api_v2_organization_team_user-detail", request=request, kwargs = url_kwargs )
return reverse(f"v2:_api_v2_organization_team_user-detail", kwargs = url_kwargs )
def get_url_kwargs_notes(self):
return FeatureNotUsed
def save(self, *args, **kwargs):
""" Save Team
Overrides, post-action
As teams are an extension of groups, add the user to the matching group.
"""
super().save(*args, **kwargs)
group = Group.objects.get(pk=self.team.id)
user = User.objects.get(pk=self.user_id)
user.groups.add(group)
@property
def parent_object(self):
""" Fetch the parent object """
return self.team
def __str__(self):
return self.user.username

View File

@ -1,324 +0,0 @@
import logging
from django.db import models
from rest_framework.reverse import reverse
from access.models.tenant import Tenant
from core import exceptions as centurion_exceptions
from core.middleware.get_request import get_request
from core.mixin.history_save import SaveHistory
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
This function filters the data fetched from the database to that which is from the organizations
the user is a part of.
!!! danger "Requirement"
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
## Workflow
This functions workflow is as follows:
- Fetch the user from the request
- Check if the user is authenticated
- Iterate over the users teams
- Store unique organizations from users teams
- return results
Returns:
(queryset): **super user**: return unfiltered data.
(queryset): **not super user**: return data from the stored unique organizations.
"""
request = get_request()
user_organizations: list(str()) = []
has_tenant_field = False
if hasattr(self.model, 'organization'):
has_tenant_field = True
if request:
if request.app_settings.global_organization:
user_organizations += [ request.app_settings.global_organization.id ]
user = request.user
if user.is_authenticated:
for team in request.tenancy._user_teams:
if team.organization.id not in user_organizations:
# if not user_organizations:
# self.user_organizations = []
user_organizations += [ team.organization.id ]
if len(user_organizations) > 0 and not user.is_superuser:
if getattr(self.model, 'is_global', False) is True:
if has_tenant_field:
return super().get_queryset().select_related('organization').filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
else:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
|
models.Q(is_global = True)
)
else:
if has_tenant_field:
return super().get_queryset().select_related('organization').filter(
models.Q(organization__in=user_organizations)
)
else:
return super().get_queryset().filter(
models.Q(organization__in=user_organizations)
)
if has_tenant_field:
return super().get_queryset().select_related('organization')
else:
return super().get_queryset().select_related('organization')
class TenancyObject(SaveHistory):
""" Tenancy Model Abstrct class.
This class is for inclusion wihtin **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 = TenancyManager()
""" Multi-Tenanant Objects """
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 centurion_exceptions.ValidationError('You must provide an organization')
id = models.AutoField(
blank=False,
help_text = 'ID of the item',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
organization = models.ForeignKey(
Tenant,
blank = False,
help_text = 'Tenancy this belongs to',
null = False,
on_delete = models.CASCADE,
related_name = '+',
validators = [validatate_organization_exists],
verbose_name = 'Tenant'
)
is_global = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this a global object?',
verbose_name = 'Global Object'
)
model_notes = models.TextField(
blank = True,
default = None,
help_text = 'Tid bits of information',
null = True,
verbose_name = 'Notes',
)
def get_organization(self) -> Tenant:
return self.organization
app_namespace: str = None
"""Application namespace.
Specify the applications namespace i.e. `devops`, without including
the API version, i.e. `v2:devops`.
"""
history_app_label: str = None
"""History Model Application Label
This value is derived from `<model>._meta.app_label`. This value should
only be used when there is model inheritence.
"""
history_model_name: str = None
"""History Model Model Name
This value is derived from `<model>._meta.model_name`. This value should
only be used when there is model inheritence.
"""
kb_model_name: str = None
"""Model name to use for KB article linking
This value is derived from `<model>._meta.model_name`. This value should
only be used when there is model inheritence.
"""
_log: logging.Logger = None
def get_log(self):
if self._log is None:
self._log = logging.getLogger('centurion.' + self._meta.app_label)
return self._log
page_layout: list = None
note_basename: str = None
"""URL BaseName for the notes endpoint.
Don't specify the `app_namespace`, use property `app_namespace` above.
"""
def get_page_layout(self):
""" FEtch the page layout"""
return self.page_layout
def get_app_namespace(self) -> str:
"""Fetch the Application namespace if specified.
Returns:
str: Application namespace suffixed with colin `:`
None: No application namespace found.
"""
app_namespace = ''
if self.app_namespace:
app_namespace = self.app_namespace + ':'
return str(app_namespace)
def get_url( self, request = None ) -> str:
"""Fetch the models URL
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
Args:
request (object, optional): The request object that was made by the end user. Defaults to None.
Returns:
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
"""
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
if request:
return reverse(f"v2:" + self.get_app_namespace() + f"_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", kwargs = self.get_url_kwargs() )
def get_url_kwargs(self) -> dict:
"""Fetch the URL kwargs
Returns:
dict: kwargs required for generating the URL with `reverse`
"""
return {
'pk': self.id
}
def get_url_kwargs_notes(self) -> dict:
"""Fetch the URL kwargs for model notes
Returns:
dict: notes kwargs required for generating the URL with `reverse`
"""
return {
'model_id': self.id
}
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.clean()
if(
not getattr(self, 'organization', None)
and self._meta.model_name !='appsettingsaudithistory' # App Settings for
):
raise centurion_exceptions.ValidationError(
detail = {
'organization': 'Tenant is required'
},
code = 'required'
)
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)

View File

@ -1,247 +0,0 @@
# import django
import logging
from django.core.exceptions import (
ValidationError,
)
from django.db import models
# from rest_framework.reverse import reverse
from access.models.tenant import Tenant
# from core import exceptions as centurion_exceptions
# from core.mixin.history_save import SaveHistory
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']
# if user:
# tencies = user.get_tenancies(int_list = True)
# if len(tenancies) > 0 and not request.user.is_superuser:
# if hasattr(self.model, 'organization'):
# return super().get_queryset().select_related('organization').filter(
# models.Q(organization__in = tenancies)
# )
# return super().get_queryset().select_related('organization').filter(
# models.Q(organization__in = tenancies)
# )
request = None
if hasattr(self.model, 'context'):
request = self.model.context['request']
if request is not None:
tenancies: list(str()) = []
if request.app_settings.global_organization:
tenancies += [ request.app_settings.global_organization.id ]
if request.user.is_authenticated:
for team in request.tenancy._user_teams:
if team.organization.id in tenancies:
continue
tenancies += [ team.organization.id ]
if len(tenancies) > 0 and not request.user.is_superuser:
if hasattr(self.model, 'organization'):
return super().get_queryset().select_related('organization').filter(
models.Q(organization__in = tenancies)
)
return super().get_queryset().select_related('organization').filter(
models.Q(organization__in = tenancies)
)
return super().get_queryset().select_related('organization')
class TenancyObjectOld:
kb_model_name: str = None
"""Model name to use for KB article linking
This value is derived from `<model>._meta.model_name`. This value should
only be used when there is model inheritence.
"""
_log: logging.Logger = None
def get_log(self):
if self._log is None:
self._log = logging.getLogger('centurion.' + self._meta.app_label)
return self._log
page_layout: list = None
note_basename: str = None
"""URL BaseName for the notes endpoint.
Don't specify the `app_namespace`, use property `app_namespace` above.
"""
def get_page_layout(self):
""" FEtch the page layout"""
return self.page_layout
def get_app_namespace(self) -> str:
"""Fetch the Application namespace if specified.
Returns:
str: Application namespace suffixed with colin `:`
None: No application namespace found.
"""
app_namespace = ''
if self.app_namespace:
app_namespace = self.app_namespace + ':'
return str(app_namespace)
def get_url_kwargs_notes(self) -> dict:
"""Fetch the URL kwargs for model notes
Returns:
dict: notes kwargs required for generating the URL with `reverse`
"""
return {
'model_id': self.id
}
class TenancyAbstractModel(
# TenancyObjectOld,
# 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 = TenancyManager()
""" Multi-Tenanant Objects """
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'
)
id = models.AutoField(
blank=False,
help_text = 'ID of the item',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
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'
)
is_global = models.BooleanField(
blank = False,
default = False,
help_text = 'Is this a global object?',
null = False,
verbose_name = 'Global Object'
)
model_notes = models.TextField(
blank = True,
default = None, # ToDo: Remove this field
help_text = 'Tid bits of information',
null = True,
verbose_name = 'Notes',
)
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

@ -1,161 +0,0 @@
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.mixin.history_save import SaveHistory
User = django.contrib.auth.get_user_model()
class Tenant(SaveHistory):
class Meta:
verbose_name = "Tenant"
verbose_name_plural = "Tenants"
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,
help_text = 'ID of this item',
primary_key=True,
unique=True,
verbose_name = 'ID'
)
name = models.CharField(
blank = False,
help_text = 'Name of this Tenancy',
max_length = 50,
unique = True,
verbose_name = 'Name'
)
manager = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank = False,
help_text = 'Manager for this Tenancy',
null = True,
on_delete=models.SET_NULL,
verbose_name = 'Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
help_text = 'Tid bits of information',
null= True,
verbose_name = 'Notes',
)
slug = AutoSlugField()
created = AutoCreatedField()
modified = AutoLastModifiedField()
def get_organization(self):
return self
def __int__(self):
return self.id
def __str__(self):
return self.name
table_fields: list = [
'nbsp',
'name',
'created',
'modified',
'nbsp'
]
page_layout: list = [
{
"name": "Details",
"slug": "details",
"sections": [
{
"layout": "double",
"left": [
'name',
'manager',
'created',
'modified',
],
"right": [
'model_notes',
]
}
]
},
{
"name": "Teams",
"slug": "teams",
"sections": [
{
"layout": "table",
"field": "teams"
}
]
},
{
"name": "Knowledge Base",
"slug": "kb_articles",
"sections": [
{
"layout": "table",
"field": "knowledge_base",
}
]
},
{
"name": "Notes",
"slug": "notes",
"sections": []
}
]
def get_url( self, request = None ) -> str:
if request:
return reverse("v2:_api_v2_organization-detail", request=request, kwargs={'pk': self.id})
return reverse("v2:_api_v2_organization-detail", kwargs={'pk': self.id})
def save_history(self, before: dict, after: dict) -> bool:
from access.models.organization_history import OrganizationAuditHistory
history = super().save_history(
before = before,
after = after,
history_model = OrganizationAuditHistory
)
return history
Organization = Tenant

View File

@ -1,195 +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:
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`
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,90 +0,0 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer
from access.models.entity import Entity
from api.serializers import common
from access.serializers.organization import TenantBaseSerializer
@extend_schema_serializer(component_name = 'EntityBaseBaseSerializer')
class BaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = Entity
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
@extend_schema_serializer(component_name = 'EntityBaseModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Entity Base Model"""
_urls = serializers.SerializerMethodField('get_url')
class Meta:
model = Entity
fields = [
'id',
'organization',
'entity_type',
'display_name',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'EntityBaseViewSerializer')
class ViewSerializer(ModelSerializer):
"""Entity Base View Model"""
organization = TenantBaseSerializer(many=False, read_only=True)

View File

@ -1,70 +0,0 @@
from drf_spectacular.utils import extend_schema_serializer
from access.models.company_base import Company
from access.serializers.entity import (
BaseSerializer as BaseBaseSerializer,
ModelSerializer as BaseModelSerializer,
)
from access.serializers.organization import TenantBaseSerializer
class BaseSerializer(
BaseBaseSerializer,
):
pass
@extend_schema_serializer(component_name = 'CompanyEntityModelSerializer')
class ModelSerializer(
BaseSerializer,
BaseModelSerializer,
):
"""Company Model
This model inherits from the Entity base model.
"""
class Meta:
model = Company
fields = [
'id',
'entity_ptr_id',
'organization',
'entity_type',
'display_name',
'name',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'CompanyEntityViewSerializer')
class ViewSerializer(
ModelSerializer,
):
"""Company View Model
This model inherits from the Entity base model.
"""
organization = TenantBaseSerializer(many=False, read_only=True)

View File

@ -1,75 +0,0 @@
from drf_spectacular.utils import extend_schema_serializer
from access.models.contact import Contact
from access.serializers.entity_person import (
BaseSerializer as BaseBaseSerializer,
ModelSerializer as BaseModelSerializer,
)
from access.serializers.organization import TenantBaseSerializer
class BaseSerializer(
BaseBaseSerializer,
):
pass
@extend_schema_serializer(component_name = 'ContactEntityModelSerializer')
class ModelSerializer(
BaseSerializer,
BaseModelSerializer,
):
"""Contact Model
This model first inherits from Person then inherits from the Entity Base model.
"""
class Meta:
model = Contact
fields = [
'id',
'person_ptr_id',
'organization',
'entity_type',
'display_name',
'f_name',
'm_name',
'l_name',
'dob',
'email',
'directory',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'ContactEntityViewSerializer')
class ViewSerializer(
ModelSerializer,
):
"""Contact View Model
This model inherits from the Person model.
"""
organization = TenantBaseSerializer(many=False, read_only=True)

View File

@ -1,41 +0,0 @@
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

@ -1,73 +0,0 @@
from drf_spectacular.utils import extend_schema_serializer
from access.models.person import Person
from access.serializers.entity import (
BaseSerializer as BaseBaseSerializer,
ModelSerializer as BaseModelSerializer,
)
from access.serializers.organization import TenantBaseSerializer
class BaseSerializer(
BaseBaseSerializer,
):
pass
@extend_schema_serializer(component_name = 'PersonEntityModelSerializer')
class ModelSerializer(
BaseSerializer,
BaseModelSerializer,
):
"""Person Model
This model inherits from the Entity base model.
"""
class Meta:
model = Person
fields = [
'id',
'entity_ptr_id',
'organization',
'entity_type',
'display_name',
'f_name',
'm_name',
'l_name',
'dob',
'model_notes',
'is_global',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'entity_type',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'PersonEntityViewSerializer')
class ViewSerializer(
ModelSerializer,
):
"""Person View Model
This model inherits from the Entity base model.
"""
organization = TenantBaseSerializer(many=False, read_only=True)

View File

@ -1,105 +0,0 @@
from rest_framework.reverse import reverse
from rest_framework import serializers
from access.models.tenant import Tenant
from centurion.serializers.user import UserBaseSerializer
from core import fields as centurion_field
Organization = Tenant
class TenantBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.HyperlinkedIdentityField(
view_name="v2:_api_v2_organization-detail", format="html"
)
class Meta:
model = Tenant
fields = [
'id',
'display_name',
'name',
'url',
]
read_only_fields = [
'id',
'display_name',
'name',
'url',
]
class TenantModelSerializer(
TenantBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
return {
'_self': item.get_url( request = self._context['view'].request ),
'knowledge_base': reverse(
"v2:_api_v2_model_kb-list",
request=self._context['view'].request,
kwargs={
'model': self.Meta.model._meta.model_name,
'model_pk': 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}),
}
model_notes = centurion_field.MarkdownField( required = False )
class Meta:
model = Tenant
fields = '__all__'
fields = [
'id',
'display_name',
'name',
'model_notes',
'manager',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
class TenantViewSerializer(TenantModelSerializer):
pass
manager = UserBaseSerializer(many=False, read_only = True)

View File

@ -1,41 +0,0 @@
from access.models.organization_notes import OrganizationNotes
from core.serializers.model_notes import (
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

@ -1,114 +0,0 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from drf_spectacular.utils import extend_schema_serializer
from access.functions.permissions import permission_queryset
from access.models.role import Role
from access.serializers.organization import TenantBaseSerializer
from api.serializers import common
from centurion.serializers.permission import PermissionBaseSerializer
@extend_schema_serializer(component_name = 'RoleBaseSerializer')
class BaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = Role
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
@extend_schema_serializer(component_name = 'RoleModelSerializer')
class ModelSerializer(
common.CommonModelSerializer,
BaseSerializer
):
"""Role Base Model"""
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
get_url = super().get_url( item = item )
get_url.update({
'tickets': reverse(
"v2:_api_v2_item_tickets-list",
request=self._context['view'].request,
kwargs={
'item_class': self.Meta.model._meta.model_name,
'item_id': item.pk
}
)
})
return get_url
permissions = serializers.PrimaryKeyRelatedField(many = True, queryset=permission_queryset(), required = False)
class Meta:
model = Role
fields = [
'id',
'organization',
'display_name',
'name',
'permissions',
'model_notes',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
@extend_schema_serializer(component_name = 'RoleViewSerializer')
class ViewSerializer(ModelSerializer):
"""Role Base View Model"""
organization = TenantBaseSerializer( many=False, read_only=True )
permissions = PermissionBaseSerializer( many=True, read_only=True )

View File

@ -1,41 +0,0 @@
from access.models.role_notes import RoleNotes
from core.serializers.model_notes import (
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

@ -1,41 +0,0 @@
from access.models.team_notes import TeamNotes
from core.serializers.model_notes import (
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,101 +0,0 @@
from rest_framework import serializers
from access.models.team_user import TeamUsers
from api.serializers import common
from centurion.serializers.user import UserBaseSerializer
class TeamUserBaseSerializer(serializers.ModelSerializer):
display_name = serializers.SerializerMethodField('get_display_name')
def get_display_name(self, item) -> str:
return str( item )
url = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> str:
return item.get_url( request = self.context['view'].request )
class Meta:
model = TeamUsers
fields = [
'id',
'display_name',
'url',
]
read_only_fields = [
'id',
'display_name',
'url',
]
class TeamUserModelSerializer(
common.CommonModelSerializer,
TeamUserBaseSerializer
):
_urls = serializers.SerializerMethodField('get_url')
def get_url(self, item) -> dict:
get_url = super().get_url( item = item )
del get_url['history']
del get_url['knowledge_base']
return get_url
class Meta:
model = TeamUsers
fields = [
'id',
'display_name',
'manager',
'user',
'created',
'modified',
'_urls',
]
read_only_fields = [
'id',
'display_name',
'created',
'modified',
'_urls',
]
def is_valid(self, *, raise_exception=True) -> bool:
is_valid = False
is_valid = super().is_valid(raise_exception=raise_exception)
self.validated_data['team_id'] = int(self._context['view'].kwargs['team_id'])
return is_valid
class TeamUserViewSerializer(TeamUserModelSerializer):
user = UserBaseSerializer(read_only = True)

View File

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

View File

@ -1,3 +1,7 @@
import pytest
import unittest
from access.models import TenancyManager
@ -7,26 +11,78 @@ class 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_has_attr_get_organization(self):
""" TenancyObject attribute check
TenancyObject has function get_organization
"""
assert hasattr(self.model, 'get_organization')
# def test_history_save(self):
# """Confirm the desired intent for saving model history."""
def test_has_attr_is_global(self):
""" TenancyObject attribute check
# assert self.model.save_model_history == self.should_model_history_be_saved
TenancyObject has field is_global
"""
assert hasattr(self.model, 'is_global')
# @pytest.mark.skip(reason="to be written")
# def test_edit_no_organization_fails(self):
# """ Devices must be assigned an organization
def test_has_attr_model_notes(self):
""" TenancyObject attribute check
# Must not be able to edit an item without an organization
# """
# pass
TenancyObject has field model_notes
"""
assert hasattr(self.model, 'model_notes')
def test_has_attr_organization(self):
""" TenancyObject attribute check
TenancyObject has field organization
"""
assert hasattr(self.model, 'organization')
@pytest.mark.skip(reason="to be written")
def test_create_no_organization_fails(self):
""" Devices must be assigned an organization
Must not be able to create an item without an organization
"""
pass
@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
def test_has_attr_organization(self):
""" TenancyObject attribute check
TenancyObject has function objects
"""
assert hasattr(self.model, 'objects')
def test_attribute_is_type_objects(self):
""" Attribute Check
attribute `objects` must be set to `access.models.TenancyManager()`
"""
assert type(self.model.objects) is TenancyManager

View File

@ -1,24 +0,0 @@
import pytest
from access.models.company_base import Company
@pytest.fixture( scope = 'class')
def model(request):
request.cls.model = Company
yield request.cls.model
del request.cls.model
@pytest.fixture(scope='function')
def create_serializer():
from access.serializers.entity_company import ModelSerializer
yield ModelSerializer

View File

@ -1,72 +0,0 @@
from django.test import TestCase
from access.models.company_base import Company
from access.tests.functional.entity.test_functional_entity_metadata import (
EntityMetadataInheritedCases
)
class CompanyMetadataTestCases(
EntityMetadataInheritedCases,
):
add_data: dict = {
'name': 'Ian1'
}
kwargs_create_item: dict = {
'name': 'Ian2',
}
kwargs_create_item_diff_org: dict = {
'name': 'Ian3',
}
model = Company
class CompanyMetadataInheritedCases(
CompanyMetadataTestCases,
):
model = None
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
@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 CompanyMetadataTest(
CompanyMetadataTestCases,
TestCase,
):
pass

View File

@ -1,43 +0,0 @@
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

@ -1,46 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,24 +0,0 @@
import pytest
from access.models.contact import Contact
@pytest.fixture( scope = 'class')
def model(request):
request.cls.model = Contact
yield request.cls.model
del request.cls.model
@pytest.fixture(scope='function')
def create_serializer():
from access.serializers.entity_contact import ModelSerializer
yield ModelSerializer

View File

@ -1,60 +0,0 @@
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

@ -1,65 +0,0 @@
from django.test import TestCase
from access.models.contact import Contact
from access.tests.functional.person.test_functional_person_metadata import (
PersonMetadataInheritedCases
)
class ContactMetadataTestCases(
PersonMetadataInheritedCases,
):
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 ContactMetadataInheritedCases(
ContactMetadataTestCases,
):
model = None
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
@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 ContactMetadataTest(
ContactMetadataTestCases,
TestCase,
):
pass

View File

@ -1,70 +0,0 @@
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

@ -1,46 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,24 +0,0 @@
import pytest
from access.models.entity import Entity
@pytest.fixture( scope = 'class')
def model(request):
request.cls.model = Entity
yield request.cls.model
del request.cls.model
@pytest.fixture(scope='function')
def create_serializer():
from access.serializers.entity import ModelSerializer
yield ModelSerializer

View File

@ -1,78 +0,0 @@
from django.test import TestCase
from access.models.entity_history import Entity, EntityAuditHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class HistoryTestCases(
HistoryEntriesCommon,
):
field_name = 'model_notes'
history_model = EntityAuditHistory
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 EntityAuditHistoryInheritedCases(
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 EntityAuditHistoryTest(
HistoryTestCases,
TestCase,
):
pass

View File

@ -1,260 +0,0 @@
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 accounting.models.asset_base import AssetBase
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
User = django.contrib.auth.get_user_model()
class EntityMetadataTestCases(
MetadataAttributesFunctional,
):
add_data: dict = {}
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 = {}
kwargs_create_item_diff_org: dict = {}
model = Entity
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_entity_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 `AssetBase`
"""
assert issubclass(self.model, self.base_model)
class EntityMetadataInheritedCases(
EntityMetadataTestCases,
):
model = None
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
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 EntityMetadataTest(
EntityMetadataTestCases,
TestCase,
):
url_name = '_api_v2_entity'

View File

@ -1,89 +0,0 @@
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

@ -1,213 +0,0 @@
import django
import pytest
from rest_framework.exceptions import ValidationError
User = django.contrib.auth.get_user_model()
class MockView:
_has_import: bool = False
"""User Permission
get_permission_required() sets this to `True` when user has import permission.
"""
_has_purge: bool = False
"""User Permission
get_permission_required() sets this to `True` when user has purge permission.
"""
_has_triage: bool = False
"""User Permission
get_permission_required() sets this to `True` when user has triage permission.
"""
class EntitySerializerTestCases:
parameterized_test_data: dict = {
"model_notes": {
'will_create': True,
}
}
valid_data: dict = {
'model_notes': 'model notes field'
}
"""Valid data used by serializer to create object"""
@pytest.fixture( scope = 'class')
def setup_data(self,
request,
model,
django_db_blocker,
organization_one,
):
with django_db_blocker.unblock():
request.cls.organization = organization_one
valid_data = {}
for base in reversed(request.cls.__mro__):
if hasattr(base, 'valid_data'):
if base.valid_data is None:
continue
valid_data.update(**base.valid_data)
if len(valid_data) > 0:
request.cls.valid_data = valid_data
if 'organization' not in request.cls.valid_data:
request.cls.valid_data.update({
'organization': request.cls.organization.pk
})
request.cls.view_user = User.objects.create_user(username="cafs_test_user_view", password="password")
yield
with django_db_blocker.unblock():
request.cls.view_user.delete()
del request.cls.valid_data
@pytest.fixture( scope = 'class', autouse = True)
def class_setup(self,
setup_data,
):
pass
def test_serializer_valid_data(self, create_serializer):
"""Serializer Validation Check
Ensure that when creating an object with valid data, no validation
error occurs.
"""
view_set = MockView()
serializer = create_serializer(
context = {
'view': view_set,
},
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_valid_data_missing_field_is_valid(self, parameterized, param_key_test_data,
create_serializer,
param_value,
param_will_create,
):
"""Serializer Validation Check
Ensure that when creating an object with a user with import permission
and with valid data, no validation error occurs.
"""
valid_data = self.valid_data.copy()
del valid_data[param_value]
view_set = MockView()
view_set._has_import = True
serializer = create_serializer(
context = {
'view': view_set,
},
data = valid_data
)
is_valid = serializer.is_valid(raise_exception = False)
assert (
(
not param_will_create
and param_will_create == is_valid
)
or param_will_create == is_valid
)
class EntitySerializerInheritedCases(
EntitySerializerTestCases,
):
parameterized_test_data: dict = None
model = None
"""Model to test"""
valid_data: dict = None
"""Valid data used by serializer to create object"""
def test_serializer_valid_data_missing_field_raises_exception(self, parameterized, param_key_test_data,
create_serializer,
param_value,
param_exception_key,
):
"""Serializer Validation Check
Ensure that when creating an object with a user with import permission
and with valid data, no validation error occurs.
"""
valid_data = self.valid_data.copy()
del valid_data[param_value]
view_set = MockView()
with pytest.raises(ValidationError) as err:
serializer = create_serializer(
context = {
'view': view_set,
},
data = valid_data
)
is_valid = serializer.is_valid(raise_exception = True)
assert err.value.get_codes()[param_value][0] == param_exception_key
class EntitySerializerPyTest(
EntitySerializerTestCases,
):
parameterized_test_data: dict = None

View File

@ -1,266 +0,0 @@
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

@ -1,81 +0,0 @@
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

@ -1,162 +0,0 @@
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

@ -1,32 +0,0 @@
from django.test import TestCase
from access.models.organization_history import Tenant as Organization, OrganizationAuditHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class History(
HistoryEntriesCommon,
TestCase,
):
model = Organization
history_model = OrganizationAuditHistory
@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,94 +0,0 @@
import django
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.serializers.organization import (
Tenant,
TenantModelSerializer
)
User = django.contrib.auth.get_user_model()
class OrganizationValidationAPI(
TestCase,
):
model = Tenant
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
self.user = User.objects.create(username = 'org_user', password='random password')
self.valid_data = {
'name': 'valid_org_data',
'manager': self.user.id
}
self.item = self.model.objects.create(
name = 'random title',
)
def test_serializer_valid_data(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
serializer = TenantModelSerializer(
data = self.valid_data
)
assert serializer.is_valid(raise_exception = True)
def test_serializer_validation_no_name(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
data = self.valid_data.copy()
del data['name']
with pytest.raises(ValidationError) as err:
serializer = TenantModelSerializer(
data = data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['name'][0] == 'required'
def test_serializer_validation_manager_optional(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
data = self.valid_data.copy()
del data['manager']
serializer = TenantModelSerializer(
data = data
)
assert serializer.is_valid(raise_exception = True)

View File

@ -1,321 +0,0 @@
import django
import pytest
import unittest
import requests
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.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
User = django.contrib.auth.get_user_model()
class ViewSetBase:
model = Organization
app_namespace = 'v2'
url_name = '_api_v2_organization'
change_data = {'name': 'device'}
delete_data = {}
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
. create an organization that is different to item
2. Create a team
3. create teams with each permission: view, add, change, delete
4. create a user per team
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.other_org_item = organization
view_permissions = Permission.objects.get(
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
view_team = Team.objects.create(
team_name = 'view_team',
organization = organization,
)
view_team_b = Team.objects.create(
team_name = 'view_team',
organization = different_organization,
)
view_team.permissions.set([view_permissions])
view_team_b.permissions.set([view_permissions])
add_permissions = Permission.objects.get(
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
add_team = Team.objects.create(
team_name = 'add_team',
organization = organization,
)
add_team.permissions.set([add_permissions])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
change_team = Team.objects.create(
team_name = 'change_team',
organization = organization,
)
change_team.permissions.set([change_permissions])
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
delete_team = Team.objects.create(
team_name = 'delete_team',
organization = organization,
)
delete_team.permissions.set([delete_permissions])
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
self.view_user = User.objects.create_user(username="test_user_view", password="password")
teamuser = TeamUsers.objects.create(
team = view_team,
user = self.view_user
)
self.view_user_b = User.objects.create_user(username="test_user_view_b", password="password")
teamuser = TeamUsers.objects.create(
team = view_team_b,
user = self.view_user_b
)
self.url_view_kwargs = { 'pk': self.item.id }
self.add_data = {
'name': 'team_post',
}
self.super_add_user = User.objects.create_user(username="test_user_add_super", password="password", is_superuser = True)
self.add_user = User.objects.create_user(username="test_user_add", password="password")
teamuser = TeamUsers.objects.create(
team = add_team,
user = self.add_user
)
self.change_user = User.objects.create_user(username="test_user_change", password="password")
teamuser = TeamUsers.objects.create(
team = change_team,
user = self.change_user
)
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
teamuser = TeamUsers.objects.create(
team = delete_team,
user = self.delete_user
)
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
different_organization_team = Team.objects.create(
team_name = 'different_organization_team',
organization = different_organization,
)
different_organization_team.permissions.set([
view_permissions,
add_permissions,
change_permissions,
delete_permissions,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)
class OrganizationPermissionsAPI(
ViewSetBase,
APIPermissions,
TestCase
):
def test_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,
TestCase
):
pass
class OrganizationMetadata(
ViewSetBase,
MetadataAttributesFunctional,
MetaDataNavigationEntriesFunctional,
TestCase
):
menu_id = 'access'
menu_entry_id = 'tenant'

View File

@ -1,116 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,24 +0,0 @@
import pytest
from access.models.person import Person
@pytest.fixture( scope = 'class')
def model(request):
request.cls.model = Person
yield request.cls.model
del request.cls.model
@pytest.fixture(scope='function')
def create_serializer():
from access.serializers.entity_person import ModelSerializer
yield ModelSerializer

View File

@ -1,65 +0,0 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.functional.entity.test_functional_entity_history import (
EntityAuditHistoryInheritedCases
)
class PersonTestCases(
EntityAuditHistoryInheritedCases,
):
field_name = 'model_notes'
kwargs_create_obj: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
kwargs_delete_obj: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
model = Person
class PersonHistoryInheritedCases(
PersonTestCases,
):
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 PersonHistoryTest(
PersonTestCases,
TestCase,
):
pass

View File

@ -1,83 +0,0 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.functional.entity.test_functional_entity_metadata import (
EntityMetadataInheritedCases
)
from accounting.models.asset_base import AssetBase
class PersonMetadataTestCases(
EntityMetadataInheritedCases,
):
add_data: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Strange',
'dob': '2025-04-08',
}
kwargs_create_item: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
kwargs_create_item_diff_org: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
model = Person
class PersonMetadataInheritedCases(
PersonMetadataTestCases,
):
model = None
kwargs_create_item: dict = {}
kwargs_create_item_diff_org: dict = {}
@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 PersonMetadataTest(
PersonMetadataTestCases,
TestCase,
):
pass

View File

@ -1,91 +0,0 @@
import pytest
from access.tests.functional.entity.test_functional_entity_permission import (
EntityPermissionsAPIInheritedCases
)
class PersonPermissionsAPITestCases(
EntityPermissionsAPIInheritedCases,
):
add_data: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Strange',
'dob': '2025-04-08',
}
# app_namespace = 'v2'
# change_data = {}
# delete_data = {}
kwargs_create_item: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
kwargs_create_item_diff_org: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
# url_kwargs: dict = {}
# url_name = '_api_v2_entity'
# url_view_kwargs: dict = {}
class PersonPermissionsAPIInheritedCases(
PersonPermissionsAPITestCases,
):
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 PersonPermissionsAPIPyTest(
PersonPermissionsAPITestCases,
):
pass

View File

@ -1,111 +0,0 @@
import pytest
from rest_framework.exceptions import ValidationError
from access.tests.functional.entity.test_functional_entity_serializer import (
MockView,
EntitySerializerInheritedCases
)
class PersonSerializerTestCases(
EntitySerializerInheritedCases
):
parameterized_test_data: dict = {
"model_notes": {
'will_create': True,
},
"f_name": {
'will_create': False,
'exception_key': 'required'
},
"m_name": {
'will_create': True,
},
"l_name": {
'will_create': False,
'exception_key': 'required'
},
"dob": {
'will_create': True,
}
}
valid_data: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
"""Valid data used by serializer to create object"""
def test_serializer_validation_duplicate_f_name_l_name_dob(self, model, create_serializer):
"""Serializer Validation Check
Ensure that when creating with valid data and fields f_name, l_name and
dob already exists in the db a validation error occurs.
"""
valid_data = self.valid_data.copy()
valid_data['f_name'] = 'duplicate'
valid_data['organization'] = self.organization
obj = model.objects.create(
**valid_data
)
valid_data['organization'] = self.organization.id
if 'email' in valid_data: # Contact Entity
valid_data['email'] = 'abc@xyz.qwe'
if 'name' in valid_data: # Company Entity
valid_data['name'] = 'diff'
if 'employee_number' in valid_data: # Employee Entity
valid_data['employee_number'] = 13579
view_set = MockView()
with pytest.raises(ValidationError) as err:
serializer = create_serializer(
context = {
'view': view_set,
},
data = valid_data
)
serializer.is_valid(raise_exception = True)
serializer.save()
assert err.value.get_codes()['dob'] == 'duplicate_person_on_dob'
class PersonSerializerInheritedCases(
PersonSerializerTestCases,
):
parameterized_test_data: dict = None
valid_data: dict = None
"""Valid data used by serializer to create object"""
class PersonSerializerPyTest(
PersonSerializerTestCases,
):
parameterized_test_data: dict = None

View File

@ -1,67 +0,0 @@
from django.test import TestCase
from access.models.person import Person
from access.tests.functional.entity.test_functional_entity_viewset import (
EntityViewSetInheritedCases
)
class ViewSetTestCases(
EntityViewSetInheritedCases,
):
add_data: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Strange',
'dob': '2025-04-08',
}
kwargs_create_item: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Weird',
'dob': '2025-04-08',
}
kwargs_create_item_diff_org: dict = {
'f_name': 'Ian',
'm_name': 'Peter',
'l_name': 'Funny',
'dob': '2025-04-08',
}
model = Person
class PersonViewSetInheritedCases(
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 PersonViewSetTest(
ViewSetTestCases,
TestCase,
):
pass

View File

@ -1,55 +0,0 @@
from django.test import TestCase
from access.models.role_history import Role, RoleAuditHistory
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
class HistoryTestCases(
HistoryEntriesCommon,
):
history_model = RoleAuditHistory
kwargs_create_obj: dict = {}
kwargs_delete_obj: dict = {}
model = Role
@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 RoleAuditHistoryTest(
HistoryTestCases,
TestCase,
):
kwargs_create_obj: dict = {
'name': 'original_name'
}
kwargs_delete_obj: dict = {
'name': 'delete obj'
}

View File

@ -1,83 +0,0 @@
import pytest
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from access.models.tenant import Tenant as Organization
from access.serializers.role import Role, ModelSerializer
class ValidationSerializer(
TestCase,
):
model = Role
serializer = ModelSerializer
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an org
2. Create an item
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.diff_organization = Organization.objects.create(name='test_org_diff_org')
self.item = self.model.objects.create(
organization = self.organization,
name = 'one',
)
self.valid_data = {
'organization': self.organization.id,
'name': 'two',
'model_notes': 'dfsdfsd',
}
def test_serializer_validation_valid_data(self):
"""Serializer Validation Check
Ensure that if creating and no name is provided a validation error occurs
"""
serializer = self.serializer(
data = self.valid_data
)
assert serializer.is_valid( raise_exception = True )
def test_serializer_validation_no_name_exception(self):
"""Serializer Validation Check
Ensure that when creating and field name is not provided a
validation error occurs
"""
valid_data = self.valid_data.copy()
del valid_data['name']
with pytest.raises(ValidationError) as err:
serializer = self.serializer(
data = valid_data
)
serializer.is_valid(raise_exception = True)
assert err.value.get_codes()['name'][0] == 'required'

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