commit
c92d38bf62
|
@ -1,4 +1,9 @@
|
||||||
|
[paths]
|
||||||
|
source = ${PROJECT_DIR-default .}/src/
|
||||||
|
[html]
|
||||||
|
directory = ${PROJECT_DIR-default .}/htmlcov
|
||||||
[run]
|
[run]
|
||||||
|
data_file = ${PROJECT_DIR-default .}/.coverage
|
||||||
omit =
|
omit =
|
||||||
*apps.py,
|
*apps.py,
|
||||||
*migrations/*,
|
*migrations/*,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[.envrc]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
46
.envrc
46
.envrc
|
@ -1,2 +1,48 @@
|
||||||
use flake
|
use flake
|
||||||
eval "$shellHook"
|
eval "$shellHook"
|
||||||
|
layout_postgres() {
|
||||||
|
export PGDATA="$(direnv_layout_dir)/postgres"
|
||||||
|
export PGHOST="$PGDATA"
|
||||||
|
if [[ ! -d "$PGDATA" ]]; then
|
||||||
|
initdb
|
||||||
|
echo -e "listen_addresses = 'localhost'\nunix_socket_directories = '$PGHOST'" >>"$PGDATA/postgresql.conf"
|
||||||
|
echo "CREATE DATABASE django;" | postgres --single -E postgres
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
layout postgres
|
||||||
|
|
||||||
|
layout_poetry() {
|
||||||
|
PYPROJECT_TOML="${PYPROJECT_TOML:-pyproject.toml}"
|
||||||
|
if [[ ! -f "$PYPROJECT_TOML" ]]; then
|
||||||
|
log_status "No pyproject.toml found. Executing \`poetry init\` to create a \`$PYPROJECT_TOML\` first."
|
||||||
|
poetry init
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d ".venv" ]]; then
|
||||||
|
VIRTUAL_ENV="$(pwd)/.venv"
|
||||||
|
else
|
||||||
|
VIRTUAL_ENV=$(
|
||||||
|
poetry env info --path 2>/dev/null
|
||||||
|
true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then
|
||||||
|
log_status "No virtual environment exists. Executing \`poetry install\` to create one."
|
||||||
|
poetry install
|
||||||
|
VIRTUAL_ENV=$(poetry env info --path)
|
||||||
|
fi
|
||||||
|
|
||||||
|
PATH_add "$VIRTUAL_ENV/bin"
|
||||||
|
export POETRY_ACTIVE=1
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! has nix; then
|
||||||
|
layout poetry
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROJECT_DIR=$(pwd)
|
||||||
|
export WEBPORT=$(($RANDOM + 1100))
|
||||||
|
export PGPORT=$(($WEBPORT + 100))
|
||||||
|
watch_file "$PGDATA/postgresql.conf"
|
||||||
|
|
8
.flake8
8
.flake8
|
@ -1,8 +0,0 @@
|
||||||
[flake8]
|
|
||||||
exclude =
|
|
||||||
*migrations*,
|
|
||||||
__init__.py,
|
|
||||||
*cache*,
|
|
||||||
venv/,
|
|
||||||
src/manage.py,
|
|
||||||
src/network_inventory/settings/*
|
|
|
@ -10,8 +10,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: cachix/install-nix-action@v18
|
- uses: cachix/install-nix-action@v22
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
with:
|
with:
|
||||||
name: networkinventory
|
name: networkinventory
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
@ -28,11 +28,8 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: cachix/install-nix-action@v18
|
- uses: cachix/install-nix-action@v22
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
with:
|
|
||||||
name: networkinventory
|
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
|
||||||
- name: Buid container
|
- name: Buid container
|
||||||
run: |
|
run: |
|
||||||
nix build .#container
|
nix build .#container
|
||||||
|
|
|
@ -11,11 +11,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: cachix/install-nix-action@v18
|
- uses: cachix/install-nix-action@v22
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
with:
|
|
||||||
name: networkinventory
|
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: nix develop --command bash -c "dev test"
|
||||||
nix flake check -L -j auto
|
env:
|
||||||
|
PROJECT_DIR: ${{ github.workspace }}
|
||||||
|
|
|
@ -173,7 +173,7 @@ migrations/
|
||||||
.vscode/
|
.vscode/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.second_run
|
.first_run
|
||||||
|
|
||||||
/src/static
|
/src/static
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
web: python ./src/manage.py runserver 0.0.0.0:$WEBPORT
|
||||||
|
db: postgres -p $PGPORT
|
64
README.md
64
README.md
|
@ -14,26 +14,62 @@ inventory over my various servers and other network equipment.
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
There are two ways to work on this project.
|
There is currently only one supported way to work with this repository. You
|
||||||
For the first one you will need to install the Nix package manager[^1].
|
will need a Linux system (WSL might work) onto wich you install the Nix package
|
||||||
Afterwards you can enter the development environment with `nix develop`.
|
manager with Flakes enabled[^1] and direnv[^3]. Afterwards you can enter the
|
||||||
|
development environment with `direnv allow`.
|
||||||
|
|
||||||
For the other way you have to install poetry[^2] and then run `poetry shell` to
|
[^1]: https://nixos.org/download.html
|
||||||
enter the virtual environment.
|
[^3]: https://direnv.net/
|
||||||
|
|
||||||
|
After you've entered the development environment with either method you can
|
||||||
|
start the development server with `dev run`. This will start a PostgreSQL
|
||||||
|
database running and start the Django development server.
|
||||||
|
|
||||||
|
_It will prompt you for your sudo password because it opens port 8000 in your
|
||||||
|
firewall. This is because I sometimes develope from my iPad on my notebook and
|
||||||
|
with this tweak I can access the dev server running on my notebook._
|
||||||
|
|
||||||
|
You can then access the project in the browser under the FQDN of your
|
||||||
|
computer. E.g. `http://mypc.domain.local:8000`.
|
||||||
|
|
||||||
|
In case you want a fresh start or remove the project you can just remove the
|
||||||
|
`.direnv` directory at the root of the project. All the data of the PostgreSQL
|
||||||
|
database is stored there together with the symlinks to the Nix store.
|
||||||
|
|
||||||
|
In case you want to tweak something these are the applications use do build the
|
||||||
|
development environment:
|
||||||
|
|
||||||
|
- Nix package manager
|
||||||
|
- direnv
|
||||||
|
- overmind[^4]
|
||||||
|
|
||||||
|
The `dev` command is a simple BASH script called `dev.sh` at the root of the
|
||||||
|
project.
|
||||||
|
|
||||||
|
[^4]: https://github.com/DarthSim/overmind
|
||||||
|
|
||||||
|
Run the `dev` command without an argument to see all options.
|
||||||
|
|
||||||
|
> Why aren't you using Docker/containers for development.
|
||||||
|
|
||||||
|
_I think containers have their uses but developing with them is in my opinion a
|
||||||
|
pain in the ass. You just can't easily interact with the tools inside the
|
||||||
|
container and you have to hack around to get your editor working with it.
|
||||||
|
In addition they aren't fully reproducable. Nix solves all of these
|
||||||
|
problems. Overmind then comes into play to orchestrate the few tasks that are
|
||||||
|
required to get a development environment up an running._
|
||||||
|
|
||||||
|
**Manual way**
|
||||||
|
|
||||||
|
The manual way you have to install poetry[^2] and then run `poetry shell` to
|
||||||
|
enter the virtual environment. You will then need a local PostgreSQL server or
|
||||||
|
modify the settings so that you can use your prefered database.
|
||||||
|
|
||||||
Please note that I will only use and test the first method.
|
Please note that I will only use and test the first method.
|
||||||
|
|
||||||
[^1]: https://nixos.org/download.html
|
|
||||||
|
|
||||||
[^2]: https://python-poetry.org
|
[^2]: https://python-poetry.org
|
||||||
|
|
||||||
After you've entered the development environment with either method you can
|
|
||||||
start the server. With the nix version you can start it with `dev run`. With
|
|
||||||
poetry `./dev.sh run`. This will start a PostgreSQL database running inside a
|
|
||||||
docker container and start the Django development server. You can then access
|
|
||||||
it in the browser under the FQDN of your computer. E.g. `mypc.domain.local`.
|
|
||||||
Run the `dev` command without an argument to see all options.
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
To customise the application in the Docker container you can use environment
|
To customise the application in the Docker container you can use environment
|
||||||
|
|
154
dev.sh
154
dev.sh
|
@ -1,15 +1,15 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
run () {
|
# Helper functions not exposed to the user {
|
||||||
setup
|
# Load example data
|
||||||
find . -name __pycache__ -o -name "*.pyc" -delete
|
_init() {
|
||||||
sudo iptables -I INPUT -p tcp --dport 8000 -j ACCEPT
|
python ./src/manage.py loaddata src/network_inventory.yaml
|
||||||
python ./src/manage.py runserver 0.0.0.0:8000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup () {
|
# Setup the database
|
||||||
docker-compose -f docker-compose-development.yml up -d
|
_setup() {
|
||||||
if [ -f .second_run ]; then
|
overmind start -l db -D
|
||||||
|
if [ -f .direnv/first_run ]; then
|
||||||
sleep 2
|
sleep 2
|
||||||
python ./src/manage.py collectstatic --noinput
|
python ./src/manage.py collectstatic --noinput
|
||||||
python ./src/manage.py makemigrations
|
python ./src/manage.py makemigrations
|
||||||
|
@ -34,59 +34,129 @@ setup () {
|
||||||
python ./src/manage.py loaddata nets
|
python ./src/manage.py loaddata nets
|
||||||
python ./src/manage.py loaddata softwares
|
python ./src/manage.py loaddata softwares
|
||||||
python ./src/manage.py shell -c "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')"
|
python ./src/manage.py shell -c "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')"
|
||||||
touch .second_run
|
_init
|
||||||
|
touch .direnv/first_run
|
||||||
|
fi
|
||||||
|
overmind quit
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
_open_url() {
|
||||||
|
if [[ ! -z "${DEFAULT_BROWSER}" ]]; then
|
||||||
|
$DEFAULT_BROWSER $url
|
||||||
|
elif type explorer.exe &>/dev/null; then
|
||||||
|
explorer.exe $url
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
venv () {
|
_create_url() {
|
||||||
nix build .#venv -o venv
|
if [ -f /etc/wsl.conf ]; then
|
||||||
|
echo "http://localhost:$WEBPORT"
|
||||||
|
else
|
||||||
|
echo "http://$(hostname -f):$WEBPORT"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
#}
|
||||||
|
|
||||||
docker (){
|
# Main tasks start
|
||||||
nix build && docker load < result && docker run --rm -ti network-inventory:latest
|
declare -A tasks
|
||||||
|
declare -A descriptions
|
||||||
|
|
||||||
|
run() {
|
||||||
|
_setup
|
||||||
|
find . -name __pycache__ -o -name "*.pyc" -delete
|
||||||
|
url=$(_create_url)
|
||||||
|
sudo iptables -I INPUT -p tcp --dport $WEBPORT -j ACCEPT
|
||||||
|
overmind start -D
|
||||||
|
printf "\n---\n webserver: $url\n---\n"
|
||||||
|
_open_url $url
|
||||||
}
|
}
|
||||||
|
descriptions["run"]="Start the webserver."
|
||||||
|
tasks["run"]=run
|
||||||
|
descriptions["start"]="Alias for run."
|
||||||
|
tasks["start"]=run
|
||||||
|
|
||||||
clean () {
|
stop() {
|
||||||
docker-compose -f docker-compose-development.yml down -v
|
overmind quit
|
||||||
|
}
|
||||||
|
descriptions["stop"]="Stop the webserver and DB."
|
||||||
|
tasks["stop"]=stop
|
||||||
|
|
||||||
|
venv() {
|
||||||
|
nix build .#venv -o .venv
|
||||||
|
}
|
||||||
|
descriptions["venv"]="Build a pseudo venv that editors like VS Code can use."
|
||||||
|
tasks["venv"]=venv
|
||||||
|
|
||||||
|
build-container() {
|
||||||
|
nix build && docker load <result
|
||||||
|
}
|
||||||
|
descriptions["build-container"]="Build and load OCI container."
|
||||||
|
tasks["build-container"]=build-container
|
||||||
|
|
||||||
|
clean() {
|
||||||
find . \( -name __pycache__ -o -name "*.pyc" \) -delete
|
find . \( -name __pycache__ -o -name "*.pyc" \) -delete
|
||||||
rm -rf htmlcov/
|
rm -rf htmlcov/
|
||||||
rm -f */migrations/0*.py
|
rm -f .direnv/first_run
|
||||||
rm .second_run
|
rm -f src/*/migrations/0*.py
|
||||||
|
rm -rf .direnv/postgres/
|
||||||
}
|
}
|
||||||
|
descriptions["clean"]="Reset the project to a fresh state including the database."
|
||||||
|
tasks["clean"]=clean
|
||||||
|
|
||||||
cleanall () {
|
cleanall() {
|
||||||
clean
|
git clean -xdf
|
||||||
docker-compose -f docker-compose-development.yml down -v --rmi local
|
|
||||||
rm -r .venv
|
|
||||||
}
|
}
|
||||||
|
descriptions["cleanall"]="Completly remove any files which are not checked into git."
|
||||||
|
tasks["cleanall"]=cleanall
|
||||||
|
|
||||||
init () {
|
debug() {
|
||||||
python ./src/manage.py loaddata network_inventory.yaml
|
|
||||||
}
|
|
||||||
|
|
||||||
debug () {
|
|
||||||
pytest --pdb --nomigrations --cov=. --cov-report=html ./src/
|
pytest --pdb --nomigrations --cov=. --cov-report=html ./src/
|
||||||
}
|
}
|
||||||
|
descriptions["debug"]="Run the tests and drop into the debugger on failure."
|
||||||
|
tasks["debug"]=debug
|
||||||
|
|
||||||
test (){
|
lint() {
|
||||||
nix flake check
|
echo "Running pylint"
|
||||||
|
pylint \
|
||||||
|
--rc-file="$PROJECT_DIR/pyproject.toml" \
|
||||||
|
-j 0 \
|
||||||
|
-E "$PROJECT_DIR/src"
|
||||||
|
echo "Running mypy"
|
||||||
|
mypy --config-file="$PROJECT_DIR/pyproject.toml" "$PROJECT_DIR/src"
|
||||||
}
|
}
|
||||||
|
descriptions["lint"]="Run the linters against the src directory."
|
||||||
|
tasks["lint"]=lint
|
||||||
|
|
||||||
tasks=("clean" "cleanall" "debug" "docker" "run" "test" "venv")
|
test() {
|
||||||
|
DJANGO_SETTINGS_MODULE=network_inventory.settings.ram_test pytest \
|
||||||
|
-nauto \
|
||||||
|
--nomigrations \
|
||||||
|
--cov-config="$PROJECT_DIR/.coveragerc" \
|
||||||
|
--cov-report=html \
|
||||||
|
"$PROJECT_DIR/src"
|
||||||
|
}
|
||||||
|
descriptions["test"]="Run the tests in the RAM DB and write a coverage report."
|
||||||
|
tasks["test"]=test
|
||||||
|
|
||||||
|
update() {
|
||||||
|
poetry update --lock
|
||||||
|
}
|
||||||
|
descriptions["update"]="Update the dependencies."
|
||||||
|
tasks["update"]=update
|
||||||
|
|
||||||
# only one task at a time
|
# only one task at a time
|
||||||
if [ $# != 1 ]; then
|
if [ $# != 1 ]; then
|
||||||
echo "usage: $0 <task_name>"
|
printf "usage: dev <task_name>\n\n"
|
||||||
echo "All tasks: ${tasks[@]}"
|
for task in "${!tasks[@]}"; do
|
||||||
|
echo "$task - ${descriptions[$task]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
else
|
||||||
|
# Check if task is available
|
||||||
|
if [[ -v "tasks[$1]" ]]; then
|
||||||
|
${tasks["$1"]}
|
||||||
|
else
|
||||||
|
echo "Task not found."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"${tasks[0]}") clean;;
|
|
||||||
"${tasks[1]}") cleanall;;
|
|
||||||
"${tasks[2]}") debug;;
|
|
||||||
"${tasks[3]}") docker;;
|
|
||||||
"${tasks[4]}") run;;
|
|
||||||
"${tasks[5]}") test;;
|
|
||||||
"${tasks[6]}") venv;;
|
|
||||||
esac
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=network_inventory
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data/
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
82
flake.lock
82
flake.lock
|
@ -1,12 +1,15 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1667395993,
|
"lastModified": 1687709756,
|
||||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -16,12 +19,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils_2": {
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1667395993,
|
"lastModified": 1689068808,
|
||||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -30,13 +36,34 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-github-actions": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"poetry2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688870561,
|
||||||
|
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nix-github-actions",
|
||||||
|
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nix-github-actions",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1670242877,
|
"lastModified": 1688918189,
|
||||||
"narHash": "sha256-jBLh7dRHnbfvPPA9znOC6oQfKrCPJ0El8Zoe0BqnCjQ=",
|
"narHash": "sha256-f8ZlJ67LgEUDnN7ZsAyd1/Fyby1VdOXWg4XY/irSGrQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6e51c97f1c849efdfd4f3b78a4870e6aa2da4198",
|
"rev": "408c0e8c15a1c9cf5c3226931b6f283c9867c484",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -49,16 +76,17 @@
|
||||||
"poetry2nix": {
|
"poetry2nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_2",
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nix-github-actions": "nix-github-actions",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1670326426,
|
"lastModified": 1693051011,
|
||||||
"narHash": "sha256-I5IscrjGuCbvpFIRoiappUwBBOq8OODvGLkapnn/ECA=",
|
"narHash": "sha256-HNbuVCS/Fnl1YZOjBk9/MlIem+wM8fvIzTH0CVQrLSQ=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "poetry2nix",
|
"repo": "poetry2nix",
|
||||||
"rev": "293dd5c31167540193bf2b66cec636eecd1fc788",
|
"rev": "5b3a5151cf212021ff8d424f215fb030e4ff2837",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -73,6 +101,36 @@
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"poetry2nix": "poetry2nix"
|
"poetry2nix": "poetry2nix"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
34
flake.nix
34
flake.nix
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
description = "A Python API for various tools I use at work.";
|
description = "A Python API for various tools I use at work.";
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = github:NixOS/nixpkgs/nixos-unstable;
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
flake-utils.url = github:numtide/flake-utils;
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
poetry2nix = {
|
poetry2nix = {
|
||||||
url = "github:nix-community/poetry2nix";
|
url = "github:nix-community/poetry2nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
@ -45,31 +45,19 @@
|
||||||
rec {
|
rec {
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
pkgs.gnumake
|
|
||||||
pkgs.inventoryDevEnv
|
pkgs.inventoryDevEnv
|
||||||
pkgs.poetry
|
pkgs.poetry
|
||||||
pkgs.python310Packages.pip
|
pkgs.python310Packages.pip
|
||||||
|
pkgs.overmind
|
||||||
|
pkgs.postgresql_15
|
||||||
(pkgs.writeScriptBin "dev" "${builtins.readFile ./dev.sh}")
|
(pkgs.writeScriptBin "dev" "${builtins.readFile ./dev.sh}")
|
||||||
];
|
];
|
||||||
|
PYTHON_KEYRING_BACKEND = "keyring.backends.fail.Keyring";
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local
|
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
checks = {
|
checks = {
|
||||||
lint = pkgs.stdenv.mkDerivation {
|
|
||||||
dontPatch = true;
|
|
||||||
dontConfigure = true;
|
|
||||||
dontBuild = true;
|
|
||||||
dontInstall = true;
|
|
||||||
doCheck = true;
|
|
||||||
name = "lint";
|
|
||||||
src = ./.;
|
|
||||||
checkInputs = [ pkgs.inventoryDevEnv ];
|
|
||||||
checkPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
flake8 . --count --show-source --statistics
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
tests = pkgs.stdenv.mkDerivation {
|
tests = pkgs.stdenv.mkDerivation {
|
||||||
dontPatch = true;
|
dontPatch = true;
|
||||||
dontConfigure = true;
|
dontConfigure = true;
|
||||||
|
@ -82,10 +70,10 @@
|
||||||
checkPhase = ''
|
checkPhase = ''
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
pytest --ds=network_inventory.settings.ram_test \
|
pytest --ds=network_inventory.settings.ram_test \
|
||||||
-nauto \
|
-nauto \
|
||||||
--nomigrations \
|
--nomigrations \
|
||||||
--cov=./src \
|
--cov=./src \
|
||||||
./src
|
./src
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -102,7 +90,7 @@
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
inventory
|
inventory
|
||||||
(pkgs.writeShellScriptBin "start-inventory" ''
|
(pkgs.writeShellScriptBin "start-inventory" ''
|
||||||
if [ -f .second_run ]; then
|
if [ -f .first_run ]; then
|
||||||
sleep 2
|
sleep 2
|
||||||
${pkgs.inventoryEnv}/bin/django-admin collectstatic --noinput
|
${pkgs.inventoryEnv}/bin/django-admin collectstatic --noinput
|
||||||
${pkgs.inventoryEnv}/bin/django-admin makemigrations
|
${pkgs.inventoryEnv}/bin/django-admin makemigrations
|
||||||
|
@ -127,7 +115,7 @@
|
||||||
${pkgs.inventoryEnv}/bin/django-admin loaddata nets
|
${pkgs.inventoryEnv}/bin/django-admin loaddata nets
|
||||||
${pkgs.inventoryEnv}/bin/django-admin loaddata softwares
|
${pkgs.inventoryEnv}/bin/django-admin loaddata softwares
|
||||||
${pkgs.inventoryEnv}/bin/django-admin shell -c "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')"
|
${pkgs.inventoryEnv}/bin/django-admin shell -c "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')"
|
||||||
touch .second_run
|
touch .first_run
|
||||||
fi
|
fi
|
||||||
${pkgs.inventoryEnv}/bin/gunicorn network_inventory.wsgi:application --reload --bind 0.0.0.0:8000 --workers 3
|
${pkgs.inventoryEnv}/bin/gunicorn network_inventory.wsgi:application --reload --bind 0.0.0.0:8000 --workers 3
|
||||||
'')
|
'')
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,18 @@
|
||||||
[tool.black]
|
[tool.pylint]
|
||||||
line-length = 79
|
max-line-length = 88
|
||||||
|
load-plugins = [
|
||||||
|
"pylint_django",
|
||||||
|
]
|
||||||
|
good-names = [
|
||||||
|
"pk",
|
||||||
|
"ip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pylint."MESSAGES CONTROL"]
|
||||||
|
disable = [
|
||||||
|
"missing-function-docstring",
|
||||||
|
"missing-class-docstring",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "network_inventory"
|
name = "network_inventory"
|
||||||
|
@ -11,36 +24,73 @@ packages = [
|
||||||
{ include = "src" },
|
{ include = "src" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = [
|
||||||
|
"tests/",
|
||||||
|
]
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
mypy_path = "./src"
|
||||||
|
# Start off with these
|
||||||
|
warn_unused_configs = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
|
||||||
|
# Getting these passing should be easy
|
||||||
|
strict_equality = true
|
||||||
|
strict_concatenate = true
|
||||||
|
|
||||||
|
# Strongly recommend enabling this one as soon as you can
|
||||||
|
#check_untyped_defs = true
|
||||||
|
|
||||||
|
# These shouldn't be too much additional work, but may be tricky to
|
||||||
|
# get passing if you use a lot of untyped libraries
|
||||||
|
#disallow_subclassing_any = true
|
||||||
|
#disallow_untyped_decorators = true
|
||||||
|
#disallow_any_generics = true
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "network_inventory.settings.local"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"nested_admin.*",
|
||||||
|
"django_tables2.*",
|
||||||
|
"floppyforms.*",
|
||||||
|
"django_filters.*",
|
||||||
|
"crispy_forms.*",
|
||||||
|
"mixer.*",
|
||||||
|
"guardian.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[tool.poetry.group.main.dependencies]
|
[tool.poetry.group.main.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.9"
|
||||||
Django = "^4.1.3"
|
Django = "^4.1.3"
|
||||||
django-crispy-forms = "^1.14.0"
|
django-crispy-forms = "^1.14.0"
|
||||||
django-filter = "^22.1"
|
django-filter = "^23.2"
|
||||||
django-floppyforms = "^1.9.0"
|
django-floppyforms = "^1.9.0"
|
||||||
django-guardian = "^2.4.0"
|
django-guardian = "^2.4.0"
|
||||||
django-htmx = "^1.13.0"
|
django-htmx = "^1.13.0"
|
||||||
django-model-utils = "^4.2.0"
|
django-model-utils = "^4.2.0"
|
||||||
django-nested-admin = "^4.0.2"
|
django-nested-admin = "^4.0.2"
|
||||||
django-tables2 = "^2.4.1"
|
django-tables2 = "^2.4.1,<2.6.0"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
psycopg2-binary = "^2.9.5"
|
psycopg2-binary = "^2.9.5"
|
||||||
PyYAML = "^6.0"
|
PyYAML = "^6.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
autopep8 = "^2.0.0"
|
|
||||||
black = "^22.10.0"
|
black = "^22.10.0"
|
||||||
coverage = "^6.5.0"
|
coverage = "^6.5.0"
|
||||||
flake8 = "^6.0.0"
|
|
||||||
jedi = "^0.18.2"
|
|
||||||
mixer = "^7.2.2"
|
mixer = "^7.2.2"
|
||||||
pep8 = "^1.7.1"
|
|
||||||
pylint = "^2.15.8"
|
pylint = "^2.15.8"
|
||||||
pytest = "^7.2.0"
|
pytest = "^7.2.0"
|
||||||
pytest-cov = "^4.0.0"
|
pytest-cov = "^4.0.0"
|
||||||
pytest-django = "^4.5.2"
|
pytest-django = "^4.5.2"
|
||||||
pytest-xdist = "^3.1.0"
|
pytest-xdist = "^3.1.0"
|
||||||
rope = "^1.5.1"
|
python-lsp-server = "^1.7.3"
|
||||||
yapf = "^0.32.0"
|
mypy = "^1.4.1"
|
||||||
|
django-stubs = "^4.2.3"
|
||||||
|
pylint-django = "^2.5.3"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
|
@ -11,8 +11,6 @@ def backup_view_permission(old_fuction):
|
||||||
if user.has_perm("customers.view_customer", backup.computer.customer):
|
if user.has_perm("customers.view_customer", backup.computer.customer):
|
||||||
return old_fuction(request, pk)
|
return old_fuction(request, pk)
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden("You're not allowed to access this device.")
|
||||||
"You're not allowed to access this device."
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
|
@ -20,18 +20,12 @@ class Backup(models.Model):
|
||||||
computer = models.ForeignKey(
|
computer = models.ForeignKey(
|
||||||
Computer, related_name="source_computer", on_delete=models.CASCADE
|
Computer, related_name="source_computer", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
method = models.ForeignKey(
|
method = models.ForeignKey(BackupMethod, models.SET_NULL, blank=True, null=True)
|
||||||
BackupMethod, models.SET_NULL, blank=True, null=True
|
software = models.ForeignKey(Software, models.SET_NULL, blank=True, null=True)
|
||||||
)
|
|
||||||
software = models.ForeignKey(
|
|
||||||
Software, models.SET_NULL, blank=True, null=True
|
|
||||||
)
|
|
||||||
source_path = models.CharField(max_length=200, blank=True)
|
source_path = models.CharField(max_length=200, blank=True)
|
||||||
exec_time = models.TimeField(null=True, blank=True)
|
exec_time = models.TimeField(null=True, blank=True)
|
||||||
exec_days = models.ManyToManyField(Weekday, blank=True)
|
exec_days = models.ManyToManyField(Weekday, blank=True)
|
||||||
target_device = models.ManyToManyField(
|
target_device = models.ManyToManyField(Computer, through="TargetDevice", blank=True)
|
||||||
Computer, through="TargetDevice", blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
@ -50,9 +44,7 @@ class Backup(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class TargetDevice(models.Model):
|
class TargetDevice(models.Model):
|
||||||
device = models.ForeignKey(
|
device = models.ForeignKey(Computer, models.SET_NULL, blank=True, null=True)
|
||||||
Computer, models.SET_NULL, blank=True, null=True
|
|
||||||
)
|
|
||||||
backup = models.ForeignKey(Backup, on_delete=models.CASCADE)
|
backup = models.ForeignKey(Backup, on_delete=models.CASCADE)
|
||||||
target_path = models.CharField(max_length=200, blank=True)
|
target_path = models.CharField(max_length=200, blank=True)
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,7 @@ def test_backup_detail_view_with_target_device(create_admin_user):
|
||||||
software=mixer.SELECT,
|
software=mixer.SELECT,
|
||||||
method=mixer.SELECT,
|
method=mixer.SELECT,
|
||||||
)
|
)
|
||||||
mixer.blend(
|
mixer.blend("backups.TargetDevice", device=target_computer, backup=mixer.SELECT)
|
||||||
"backups.TargetDevice", device=target_computer, backup=mixer.SELECT
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/backup/" + str(backup.id) + "/")
|
response = client.get("/backup/" + str(backup.id) + "/")
|
||||||
|
@ -79,9 +77,7 @@ def test_backup_detail_view_with_notification(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/backup/" + str(backup.id) + "/")
|
response = client.get("/backup/" + str(backup.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, notification)
|
||||||
response, notification
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_backup_detail_view_with_day_relation(create_admin_user):
|
def test_backup_detail_view_with_day_relation(create_admin_user):
|
||||||
|
|
|
@ -23,9 +23,7 @@ def test_customer_backup_table(create_admin_user):
|
||||||
computer = mixer.blend("computers.Computer", customer=customer)
|
computer = mixer.blend("computers.Computer", customer=customer)
|
||||||
backup = mixer.blend("backups.Backup", computer=computer)
|
backup = mixer.blend("backups.Backup", computer=computer)
|
||||||
response = client.get("/customer/" + str(customer.id) + "/backups/")
|
response = client.get("/customer/" + str(customer.id) + "/backups/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, backup.name)
|
||||||
response, backup.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_customer_backup_table_no_backup(create_admin_user):
|
def test_customer_backup_table_no_backup(create_admin_user):
|
||||||
|
|
|
@ -3,9 +3,7 @@ from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path("customer/<int:pk>/backups/", views.backups_table_view, name="backups"),
|
||||||
"customer/<int:pk>/backups/", views.backups_table_view, name="backups"
|
|
||||||
),
|
|
||||||
path("backup/<int:pk>/", views.backup_detail_view, name="backup"),
|
path("backup/<int:pk>/", views.backup_detail_view, name="backup"),
|
||||||
path(
|
path(
|
||||||
"create/backup-for-computer/<int:pk>/",
|
"create/backup-for-computer/<int:pk>/",
|
||||||
|
|
|
@ -47,7 +47,7 @@ def backup_detail_view(request, pk):
|
||||||
class BackupCreateView(LoginRequiredMixin, CreateView):
|
class BackupCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = Backup
|
model = Backup
|
||||||
template_name = "backups/backup_create.html"
|
template_name = "backups/backup_create.html"
|
||||||
fields = "__all__"
|
fields = "__all__" # type: ignore
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("computer", args=(self.computer.pk,))
|
return reverse("computer", args=(self.computer.pk,))
|
||||||
|
@ -63,7 +63,7 @@ class BackupCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BackupDeleteView(LoginRequiredMixin, DeleteView):
|
class BackupDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Backup
|
model = Backup
|
||||||
template_name = "backups/backup_confirm_delete.html"
|
template_name = "backups/backup_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class BackupDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
return reverse("computer", args=(self.object.computer.pk,))
|
return reverse("computer", args=(self.object.computer.pk,))
|
||||||
|
|
||||||
|
|
||||||
class BackupDeleteFromTableView(LoginRequiredMixin, DeleteView):
|
class BackupDeleteFromTableView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Backup
|
model = Backup
|
||||||
template_name = "backups/backup_confirm_delete.html"
|
template_name = "backups/backup_confirm_delete.html"
|
||||||
|
|
||||||
|
|
|
@ -26,62 +26,64 @@ from .models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareInLine(nested_admin.NestedStackedInline):
|
class SoftwareInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = ComputerSoftwareRelation
|
model = ComputerSoftwareRelation
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Software"
|
verbose_name_plural = "Software"
|
||||||
|
|
||||||
|
|
||||||
class RamInLine(nested_admin.NestedStackedInline):
|
class RamInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = ComputerRamRelation
|
model = ComputerRamRelation
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "RAM Modules"
|
verbose_name_plural = "RAM Modules"
|
||||||
|
|
||||||
|
|
||||||
class DiskInLine(nested_admin.NestedStackedInline):
|
class DiskInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = ComputerDiskRelation
|
model = ComputerDiskRelation
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Disks"
|
verbose_name_plural = "Disks"
|
||||||
|
|
||||||
|
|
||||||
class DisksInRaidInLine(nested_admin.NestedStackedInline):
|
class DisksInRaidInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = DisksInRaid
|
model = DisksInRaid
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Disks in RAID"
|
verbose_name_plural = "Disks in RAID"
|
||||||
|
|
||||||
|
|
||||||
class CpusInLine(nested_admin.NestedStackedInline):
|
class CpusInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = ComputerCpuRelation
|
model = ComputerCpuRelation
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "CPUs"
|
verbose_name_plural = "CPUs"
|
||||||
|
|
||||||
|
|
||||||
class GpusInLine(nested_admin.NestedStackedInline):
|
class GpusInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = ComputerGpuRelation
|
model = ComputerGpuRelation
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "GPUs"
|
verbose_name_plural = "GPUs"
|
||||||
|
|
||||||
|
|
||||||
class RaidInLine(nested_admin.NestedStackedInline):
|
class RaidInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = Raid
|
model = Raid
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "RAID"
|
verbose_name_plural = "RAID"
|
||||||
inlines = (DisksInRaidInLine,)
|
inlines = (DisksInRaidInLine,)
|
||||||
|
|
||||||
|
|
||||||
class DeviceInNetInline(nested_admin.NestedStackedInline):
|
class DeviceInNetInline(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = DeviceInNet
|
model = DeviceInNet
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Nets"
|
verbose_name_plural = "Nets"
|
||||||
|
|
||||||
|
|
||||||
class LicenseWithComputerInLine(nested_admin.NestedStackedInline):
|
class LicenseWithComputerInLine(
|
||||||
|
nested_admin.NestedStackedInline
|
||||||
|
): # pylint: disable=no-member
|
||||||
model = LicenseWithComputer
|
model = LicenseWithComputer
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Licenses"
|
verbose_name_plural = "Licenses"
|
||||||
|
|
||||||
|
|
||||||
class ComputerAdmin(nested_admin.NestedModelAdmin):
|
class ComputerAdmin(nested_admin.NestedModelAdmin): # pylint: disable=no-member
|
||||||
list_display = ("name", "host")
|
list_display = ("name", "host")
|
||||||
inlines = (
|
inlines = (
|
||||||
SoftwareInLine,
|
SoftwareInLine,
|
||||||
|
|
|
@ -41,15 +41,9 @@ class ComputerUpdateForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super(ComputerUpdateForm, self).__init__(*args, **kwargs)
|
super(ComputerUpdateForm, self).__init__(*args, **kwargs)
|
||||||
customers = utils.objects_for_allowed_customers(
|
customers = utils.objects_for_allowed_customers(Customer, user=request.user)
|
||||||
Customer, user=request.user
|
locations = utils.objects_for_allowed_customers(Location, user=request.user)
|
||||||
)
|
hosts = utils.objects_for_allowed_customers(Computer, user=request.user)
|
||||||
locations = utils.objects_for_allowed_customers(
|
|
||||||
Location, user=request.user
|
|
||||||
)
|
|
||||||
hosts = utils.objects_for_allowed_customers(
|
|
||||||
Computer, user=request.user
|
|
||||||
)
|
|
||||||
users = utils.objects_for_allowed_customers(User, user=request.user)
|
users = utils.objects_for_allowed_customers(User, user=request.user)
|
||||||
self.fields["customer"].queryset = customers
|
self.fields["customer"].queryset = customers
|
||||||
self.fields["location"].queryset = locations
|
self.fields["location"].queryset = locations
|
||||||
|
|
|
@ -23,12 +23,8 @@ class Computer(Device):
|
||||||
ram = models.ManyToManyField(Ram, through="ComputerRamRelation")
|
ram = models.ManyToManyField(Ram, through="ComputerRamRelation")
|
||||||
gpu = models.ManyToManyField(Gpu, through="ComputerGpuRelation")
|
gpu = models.ManyToManyField(Gpu, through="ComputerGpuRelation")
|
||||||
disks = models.ManyToManyField(Disk, through="ComputerDiskRelation")
|
disks = models.ManyToManyField(Disk, through="ComputerDiskRelation")
|
||||||
software = models.ManyToManyField(
|
software = models.ManyToManyField(Software, through="ComputerSoftwareRelation")
|
||||||
Software, through="ComputerSoftwareRelation"
|
host = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
|
||||||
)
|
|
||||||
host = models.ForeignKey(
|
|
||||||
"self", null=True, blank=True, on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
allocated_space = models.IntegerField(null=True, blank=True)
|
allocated_space = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -12,9 +12,7 @@ class RaidType(Category):
|
||||||
|
|
||||||
class Raid(models.Model):
|
class Raid(models.Model):
|
||||||
usable_space = models.IntegerField(blank=True, null=True)
|
usable_space = models.IntegerField(blank=True, null=True)
|
||||||
raid_type = models.ForeignKey(
|
raid_type = models.ForeignKey(RaidType, models.SET_NULL, blank=True, null=True)
|
||||||
RaidType, models.SET_NULL, blank=True, null=True
|
|
||||||
)
|
|
||||||
computer = models.ForeignKey(Computer, on_delete=models.CASCADE)
|
computer = models.ForeignKey(Computer, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -15,15 +15,11 @@ def test_computer_detail_view_not_logged_in():
|
||||||
|
|
||||||
def test_computer_detail_view(create_admin_user):
|
def test_computer_detail_view(create_admin_user):
|
||||||
create_admin_user()
|
create_admin_user()
|
||||||
computer = mixer.blend(
|
computer = mixer.blend("computers.Computer", customer=mixer.SELECT, os=mixer.SELECT)
|
||||||
"computers.Computer", customer=mixer.SELECT, os=mixer.SELECT
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/computer/" + str(computer.id) + "/")
|
response = client.get("/computer/" + str(computer.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, computer)
|
||||||
response, computer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_computer_detail_view_not_found(create_admin_user):
|
def test_computer_detail_view_not_found(create_admin_user):
|
||||||
|
@ -43,9 +39,7 @@ def test_computer_detail_view_ram_relation(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/computer/" + str(computer.id) + "/")
|
response = client.get("/computer/" + str(computer.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, "RAM Modules:")
|
||||||
response, "RAM Modules:"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_computer_detail_view_raid_relation(create_admin_user):
|
def test_computer_detail_view_raid_relation(create_admin_user):
|
||||||
|
@ -53,9 +47,7 @@ def test_computer_detail_view_raid_relation(create_admin_user):
|
||||||
computer = mixer.blend("computers.Computer", customer=mixer.SELECT)
|
computer = mixer.blend("computers.Computer", customer=mixer.SELECT)
|
||||||
raid_type = mixer.blend("computers.RaidType")
|
raid_type = mixer.blend("computers.RaidType")
|
||||||
disk = mixer.blend("computers.Disk")
|
disk = mixer.blend("computers.Disk")
|
||||||
raid = mixer.blend(
|
raid = mixer.blend("computers.Raid", computer=computer, raid_type=raid_type)
|
||||||
"computers.Raid", computer=computer, raid_type=raid_type
|
|
||||||
)
|
|
||||||
mixer.blend("computers.DisksInRaid", raid=raid, disk=disk)
|
mixer.blend("computers.DisksInRaid", raid=raid, disk=disk)
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
|
|
|
@ -12,9 +12,7 @@ def test_computer_create_form(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
||||||
form = forms.ComputerCreateForm(user=user, data={})
|
form = forms.ComputerCreateForm(user=user, data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
data = {"name": "pharma-pc1", "customer": 3}
|
data = {"name": "pharma-pc1", "customer": 3}
|
||||||
form = forms.ComputerCreateForm(user=user, data=data)
|
form = forms.ComputerCreateForm(user=user, data=data)
|
||||||
|
@ -32,9 +30,7 @@ def test_computer_update_form(create_admin_user):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.user = fixture["admin"]
|
request.user = fixture["admin"]
|
||||||
form = forms.ComputerUpdateForm(request, data={})
|
form = forms.ComputerUpdateForm(request, data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
data = {"name": "pharma-pc1", "customer": 20356}
|
data = {"name": "pharma-pc1", "customer": 20356}
|
||||||
form = forms.ComputerUpdateForm(request, data=data)
|
form = forms.ComputerUpdateForm(request, data=data)
|
||||||
|
|
|
@ -27,6 +27,4 @@ def test_computer_list_view(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/computers/all/")
|
response = client.get("/computers/all/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, computer)
|
||||||
response, computer
|
|
||||||
)
|
|
||||||
|
|
|
@ -21,9 +21,7 @@ def test_customer_computer_table(create_admin_user):
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
computer = mixer.blend("computers.Computer", customer=mixer.SELECT)
|
computer = mixer.blend("computers.Computer", customer=mixer.SELECT)
|
||||||
response = client.get("/customer/" + str(customer.id) + "/computers/")
|
response = client.get("/customer/" + str(customer.id) + "/computers/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, computer)
|
||||||
response, computer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_customer_computer_table_no_computer(create_admin_user):
|
def test_customer_computer_table_no_computer(create_admin_user):
|
||||||
|
|
|
@ -41,9 +41,7 @@ from .tables import ComputersTable
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def computer_detail_view(request, pk):
|
def computer_detail_view(request, pk):
|
||||||
device = utils.get_object_with_view_permission(
|
device = utils.get_object_with_view_permission(Computer, user=request.user, pk=pk)
|
||||||
Computer, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
disks_relations = ComputerDiskRelation.objects.filter(computer=pk)
|
disks_relations = ComputerDiskRelation.objects.filter(computer=pk)
|
||||||
warranty_relations = Warranty.objects.filter(device=pk)
|
warranty_relations = Warranty.objects.filter(device=pk)
|
||||||
ram_relations = ComputerRamRelation.objects.filter(computer=pk)
|
ram_relations = ComputerRamRelation.objects.filter(computer=pk)
|
||||||
|
@ -76,9 +74,7 @@ def computer_detail_view(request, pk):
|
||||||
@login_required
|
@login_required
|
||||||
def computers_table_view(request, pk):
|
def computers_table_view(request, pk):
|
||||||
table = ComputersTable(
|
table = ComputersTable(
|
||||||
utils.get_objects_for_customer(
|
utils.get_objects_for_customer(Computer, user=request.user, customer_pk=pk)
|
||||||
Computer, user=request.user, customer_pk=pk
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
RequestConfig(request).configure(table)
|
RequestConfig(request).configure(table)
|
||||||
return render(
|
return render(
|
||||||
|
@ -139,9 +135,7 @@ def computer_update_view(request, pk):
|
||||||
A view to create a customer.
|
A view to create a customer.
|
||||||
"""
|
"""
|
||||||
template_name = "computers/computer_update.html"
|
template_name = "computers/computer_update.html"
|
||||||
computer = utils.get_object_with_view_permission(
|
computer = utils.get_object_with_view_permission(Computer, user=request.user, pk=pk)
|
||||||
Computer, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = ComputerUpdateForm(request, request.POST, instance=computer)
|
form = ComputerUpdateForm(request, request.POST, instance=computer)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -152,7 +146,7 @@ def computer_update_view(request, pk):
|
||||||
return TemplateResponse(request, template_name, {"form": form})
|
return TemplateResponse(request, template_name, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
class ComputerDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Computer
|
model = Computer
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -178,7 +172,7 @@ class ComputerRamRelationCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComputerRamRelationDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerRamRelationDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerRamRelation
|
model = ComputerRamRelation
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -205,7 +199,7 @@ class ComputerCpuRelationCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComputerCpuRelationDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerCpuRelationDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerCpuRelation
|
model = ComputerCpuRelation
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -232,7 +226,7 @@ class ComputerGpuRelationCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComputerGpuRelationDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerGpuRelationDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerGpuRelation
|
model = ComputerGpuRelation
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -259,7 +253,7 @@ class ComputerDiskRelationCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComputerDiskRelationDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerDiskRelationDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerDiskRelation
|
model = ComputerDiskRelation
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -286,7 +280,7 @@ class ComputerSoftwareRelationCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComputerSoftwareRelationDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerSoftwareRelationDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerSoftwareRelation
|
model = ComputerSoftwareRelation
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -313,7 +307,7 @@ class RaidCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RaidDeleteView(LoginRequiredMixin, DeleteView):
|
class RaidDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Raid
|
model = Raid
|
||||||
template_name = "computers/relation_confirm_delete.html"
|
template_name = "computers/relation_confirm_delete.html"
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,7 @@ def django_db_setup(django_db_setup, django_db_blocker):
|
||||||
def create_admin_user():
|
def create_admin_user():
|
||||||
def _create_admin_user():
|
def _create_admin_user():
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
admin = User.objects.create_user(
|
admin = User.objects.create_user("pharma-admin", "admin@pharma.com", "password")
|
||||||
"pharma-admin", "admin@pharma.com", "password"
|
|
||||||
)
|
|
||||||
customer = mixer.blend("customers.Customer")
|
customer = mixer.blend("customers.Customer")
|
||||||
group = Group.objects.create(name="Pharma Corp. Admin")
|
group = Group.objects.create(name="Pharma Corp. Admin")
|
||||||
admin.groups.add(group)
|
admin.groups.add(group)
|
||||||
|
|
|
@ -15,9 +15,7 @@ def test_get_object_with_view_permission(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
customer = fixture["customer"]
|
customer = fixture["customer"]
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
object = utils.get_object_with_view_permission(
|
object = utils.get_object_with_view_permission(Customer, user=admin, pk=customer.id)
|
||||||
Customer, user=admin, pk=customer.id
|
|
||||||
)
|
|
||||||
assert object == customer
|
assert object == customer
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,9 +24,7 @@ def test_get_object_with_view_permission_device(create_admin_user):
|
||||||
customer = fixture["customer"]
|
customer = fixture["customer"]
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
device = mixer.blend(Device, customer=customer)
|
device = mixer.blend(Device, customer=customer)
|
||||||
object = utils.get_object_with_view_permission(
|
object = utils.get_object_with_view_permission(Device, user=admin, pk=device.id)
|
||||||
Device, user=admin, pk=device.id
|
|
||||||
)
|
|
||||||
assert object == device
|
assert object == device
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,9 +33,7 @@ def test_get_object_without_view_permission(create_admin_user):
|
||||||
customer = mixer.blend(Customer)
|
customer = mixer.blend(Customer)
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
with pytest.raises(Http404):
|
with pytest.raises(Http404):
|
||||||
utils.get_object_with_view_permission(
|
utils.get_object_with_view_permission(Customer, user=admin, pk=customer.id)
|
||||||
Customer, user=admin, pk=customer.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_object_without_view_permission_device(create_admin_user):
|
def test_get_object_without_view_permission_device(create_admin_user):
|
||||||
|
|
|
@ -16,9 +16,7 @@ def test_get_objects_for_customer_with_customer(create_admin_user):
|
||||||
customer = fixture["customer"]
|
customer = fixture["customer"]
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
utils.get_objects_for_customer(
|
utils.get_objects_for_customer(Customer, user=admin, customer_pk=customer.id)
|
||||||
Customer, user=admin, customer_pk=customer.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_objects_for_customer_device(create_admin_user):
|
def test_get_objects_for_customer_device(create_admin_user):
|
||||||
|
@ -37,9 +35,7 @@ def test_get_all_objects_for_unallowed_customers(create_admin_user):
|
||||||
customer = mixer.blend(Customer)
|
customer = mixer.blend(Customer)
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
with pytest.raises(Http404):
|
with pytest.raises(Http404):
|
||||||
utils.get_objects_for_customer(
|
utils.get_objects_for_customer(Customer, user=admin, customer_pk=customer.id)
|
||||||
Customer, user=admin, customer_pk=customer.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_all_objects_for_unallowed_customers_device(create_admin_user):
|
def test_get_all_objects_for_unallowed_customers_device(create_admin_user):
|
||||||
|
@ -48,6 +44,4 @@ def test_get_all_objects_for_unallowed_customers_device(create_admin_user):
|
||||||
admin = fixture["admin"]
|
admin = fixture["admin"]
|
||||||
mixer.blend(Device, customer=customer)
|
mixer.blend(Device, customer=customer)
|
||||||
with pytest.raises(Http404):
|
with pytest.raises(Http404):
|
||||||
utils.get_objects_for_customer(
|
utils.get_objects_for_customer(Device, user=admin, customer_pk=customer.id)
|
||||||
Device, user=admin, customer_pk=customer.id
|
|
||||||
)
|
|
||||||
|
|
|
@ -34,9 +34,7 @@ def _get_customers(user):
|
||||||
|
|
||||||
user : django.contrib.auth.models.User
|
user : django.contrib.auth.models.User
|
||||||
"""
|
"""
|
||||||
return get_objects_for_user(
|
return get_objects_for_user(user, "customers.view_customer", klass=Customer)
|
||||||
user, "customers.view_customer", klass=Customer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_with_view_permission(model, user=None, pk=None):
|
def get_object_with_view_permission(model, user=None, pk=None):
|
||||||
|
|
|
@ -10,8 +10,6 @@ def customer_view_permission(old_function):
|
||||||
if user.has_perm("customers.view_customer", customer):
|
if user.has_perm("customers.view_customer", customer):
|
||||||
return old_function(request, pk)
|
return old_function(request, pk)
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden("You're not allowed to access this page.")
|
||||||
"You're not allowed to access this page."
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
|
@ -43,4 +43,4 @@ class DummyLocation(models.Model):
|
||||||
location = models.ForeignKey(Location, on_delete=models.CASCADE)
|
location = models.ForeignKey(Location, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.location
|
return self.location.name
|
||||||
|
|
|
@ -6,9 +6,7 @@ from core.tables import CoreTable
|
||||||
|
|
||||||
class CustomersTable(CoreTable):
|
class CustomersTable(CoreTable):
|
||||||
name = tables.LinkColumn("customer", args=[A("pk")])
|
name = tables.LinkColumn("customer", args=[A("pk")])
|
||||||
nets = tables.LinkColumn(
|
nets = tables.LinkColumn("nets", text="Nets", args=[A("pk")], orderable=False)
|
||||||
"nets", text="Nets", args=[A("pk")], orderable=False
|
|
||||||
)
|
|
||||||
computers = tables.LinkColumn(
|
computers = tables.LinkColumn(
|
||||||
"computers", text="Computers", args=[A("pk")], orderable=False
|
"computers", text="Computers", args=[A("pk")], orderable=False
|
||||||
)
|
)
|
||||||
|
@ -21,12 +19,8 @@ class CustomersTable(CoreTable):
|
||||||
licenses = tables.LinkColumn(
|
licenses = tables.LinkColumn(
|
||||||
"licenses", text="Licenses", args=[A("pk")], orderable=False
|
"licenses", text="Licenses", args=[A("pk")], orderable=False
|
||||||
)
|
)
|
||||||
users = tables.LinkColumn(
|
users = tables.LinkColumn("users", text="Users", args=[A("pk")], orderable=False)
|
||||||
"users", text="Users", args=[A("pk")], orderable=False
|
groups = tables.LinkColumn("groups", text="Groups", args=[A("pk")], orderable=False)
|
||||||
)
|
|
||||||
groups = tables.LinkColumn(
|
|
||||||
"groups", text="Groups", args=[A("pk")], orderable=False
|
|
||||||
)
|
|
||||||
project_manager = tables.Column(verbose_name="Project Manager")
|
project_manager = tables.Column(verbose_name="Project Manager")
|
||||||
delete = tables.LinkColumn(
|
delete = tables.LinkColumn(
|
||||||
"customer_delete",
|
"customer_delete",
|
||||||
|
|
|
@ -42,24 +42,12 @@ def test_customer_list_view(create_admin_user):
|
||||||
assert (
|
assert (
|
||||||
response.status_code == 200
|
response.status_code == 200
|
||||||
and helper.in_content(response, customer)
|
and helper.in_content(response, customer)
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/nets/")
|
||||||
response, "/customer/" + str(customer.id) + "/nets/"
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/computers/")
|
||||||
)
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/devices/")
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/backups/")
|
||||||
response, "/customer/" + str(customer.id) + "/computers/"
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/licenses/")
|
||||||
)
|
and helper.in_content(response, "/customer/" + str(customer.id) + "/users/")
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer.id) + "/devices/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer.id) + "/backups/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer.id) + "/licenses/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer.id) + "/users/"
|
|
||||||
)
|
|
||||||
and helper.in_content(response, project_manager)
|
and helper.in_content(response, project_manager)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,41 +63,21 @@ def test_customer_list_view_multiple_customers(create_admin_user):
|
||||||
assert (
|
assert (
|
||||||
response.status_code == 200
|
response.status_code == 200
|
||||||
and helper.in_content(response, customer1)
|
and helper.in_content(response, customer1)
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/nets/")
|
||||||
response, "/customer/" + str(customer1.id) + "/nets/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
and helper.in_content(
|
||||||
response, "/customer/" + str(customer1.id) + "/computers/"
|
response, "/customer/" + str(customer1.id) + "/computers/"
|
||||||
)
|
)
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/devices/")
|
||||||
response, "/customer/" + str(customer1.id) + "/devices/"
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/backups/")
|
||||||
)
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/licenses/")
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/users/")
|
||||||
response, "/customer/" + str(customer1.id) + "/backups/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer1.id) + "/licenses/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer1.id) + "/users/"
|
|
||||||
)
|
|
||||||
and helper.in_content(response, customer2)
|
and helper.in_content(response, customer2)
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer2.id) + "/nets/")
|
||||||
response, "/customer/" + str(customer2.id) + "/nets/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
and helper.in_content(
|
||||||
response, "/customer/" + str(customer2.id) + "/computers/"
|
response, "/customer/" + str(customer2.id) + "/computers/"
|
||||||
)
|
)
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer2.id) + "/devices/")
|
||||||
response, "/customer/" + str(customer2.id) + "/devices/"
|
and helper.in_content(response, "/customer/" + str(customer2.id) + "/backups/")
|
||||||
)
|
and helper.in_content(response, "/customer/" + str(customer2.id) + "/licenses/")
|
||||||
and helper.in_content(
|
and helper.in_content(response, "/customer/" + str(customer1.id) + "/users/")
|
||||||
response, "/customer/" + str(customer2.id) + "/backups/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer2.id) + "/licenses/"
|
|
||||||
)
|
|
||||||
and helper.in_content(
|
|
||||||
response, "/customer/" + str(customer1.id) + "/users/"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,9 +10,7 @@ def test_location_form(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
user = fixture["admin"]
|
user = fixture["admin"]
|
||||||
form = forms.LocationForm(user=user, data={})
|
form = forms.LocationForm(user=user, data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
data = {"name": "Main Office", "customer": 3}
|
data = {"name": "Main Office", "customer": 3}
|
||||||
form = forms.LocationForm(user=user, data=data)
|
form = forms.LocationForm(user=user, data=data)
|
||||||
|
|
|
@ -14,9 +14,7 @@ def test_load_htmx_create_location_view(create_admin_user):
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
url = "/create/location/"
|
url = "/create/location/"
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, "Add Location")
|
||||||
response, "Add Location"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_htmx_create_location_view(create_admin_user):
|
def test_htmx_create_location_view(create_admin_user):
|
||||||
|
@ -25,9 +23,7 @@ def test_htmx_create_location_view(create_admin_user):
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
data = {"name": mixer.faker.name(), "save_location": 1}
|
data = {"name": mixer.faker.name(), "save_location": 1}
|
||||||
response = client.post("/create/location/", data)
|
response = client.post("/create/location/", data)
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, data["name"])
|
||||||
response, data["name"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_htmx_create_location_view_invalid_form(create_admin_user):
|
def test_htmx_create_location_view_invalid_form(create_admin_user):
|
||||||
|
|
|
@ -24,9 +24,7 @@ def customers_table_view(request):
|
||||||
customers = utils.objects_for_allowed_customers(Customer, request.user)
|
customers = utils.objects_for_allowed_customers(Customer, request.user)
|
||||||
table = CustomersTable(customers)
|
table = CustomersTable(customers)
|
||||||
RequestConfig(request).configure(table)
|
RequestConfig(request).configure(table)
|
||||||
return render(
|
return render(request, "customers/customer_list.html", {"customers": table})
|
||||||
request, "customers/customer_list.html", {"customers": table}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -44,23 +42,17 @@ def create_customer(request):
|
||||||
)
|
)
|
||||||
form = CustomerForm()
|
form = CustomerForm()
|
||||||
context = {"form": form}
|
context = {"form": form}
|
||||||
return TemplateResponse(
|
return TemplateResponse(request, "customers/partials/customer_create.html", context)
|
||||||
request, "customers/partials/customer_create.html", context
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def customer_detail_view(request, pk):
|
def customer_detail_view(request, pk):
|
||||||
customer = utils.get_object_with_view_permission(
|
customer = utils.get_object_with_view_permission(Customer, user=request.user, pk=pk)
|
||||||
Customer, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
context = {"customer": customer}
|
context = {"customer": customer}
|
||||||
return TemplateResponse(
|
return TemplateResponse(request, "customers/customer_details.html", context)
|
||||||
request, "customers/customer_details.html", context
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerDeleteView(LoginRequiredMixin, DeleteView):
|
class CustomerDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Customer
|
model = Customer
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
|
|
@ -12,13 +12,13 @@ from .models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceInNetInline(nested_admin.NestedStackedInline):
|
class DeviceInNetInline(nested_admin.NestedStackedInline): # pylint: disable=no-member
|
||||||
model = DeviceInNet
|
model = DeviceInNet
|
||||||
extra = 0
|
extra = 0
|
||||||
verbose_name_plural = "Nets"
|
verbose_name_plural = "Nets"
|
||||||
|
|
||||||
|
|
||||||
class DeviceAdmin(nested_admin.NestedModelAdmin):
|
class DeviceAdmin(nested_admin.NestedModelAdmin): # pylint: disable=no-member
|
||||||
inlines = (DeviceInNetInline,)
|
inlines = (DeviceInNetInline,)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ def device_view_permission(old_function):
|
||||||
if user.has_perm("customers.view_customer", device.customer):
|
if user.has_perm("customers.view_customer", device.customer):
|
||||||
return old_function(request, pk)
|
return old_function(request, pk)
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden("You're not allowed to access this device.")
|
||||||
"You're not allowed to access this device."
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
|
@ -69,12 +69,8 @@ class DeviceUpdateForm(forms.ModelForm):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super(DeviceUpdateForm, self).__init__(*args, **kwargs)
|
super(DeviceUpdateForm, self).__init__(*args, **kwargs)
|
||||||
customers = utils.objects_for_allowed_customers(
|
customers = utils.objects_for_allowed_customers(Customer, user=request.user)
|
||||||
Customer, user=request.user
|
locations = utils.objects_for_allowed_customers(Location, user=request.user)
|
||||||
)
|
|
||||||
locations = utils.objects_for_allowed_customers(
|
|
||||||
Location, user=request.user
|
|
||||||
)
|
|
||||||
users = utils.objects_for_allowed_customers(User, user=request.user)
|
users = utils.objects_for_allowed_customers(User, user=request.user)
|
||||||
self.fields["customer"].queryset = customers
|
self.fields["customer"].queryset = customers
|
||||||
self.fields["location"].queryset = locations
|
self.fields["location"].queryset = locations
|
||||||
|
|
|
@ -38,9 +38,7 @@ class DeviceCategory(Category):
|
||||||
|
|
||||||
class HardwareModel(models.Model):
|
class HardwareModel(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(DeviceManufacturer, on_delete=models.CASCADE)
|
||||||
DeviceManufacturer, on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
@ -56,19 +54,13 @@ class Device(models.Model):
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
DeviceCategory, on_delete=models.SET_NULL, null=True, blank=True
|
DeviceCategory, on_delete=models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
Owner, on_delete=models.SET_NULL, null=True, blank=True
|
|
||||||
)
|
|
||||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
DeviceManufacturer, models.SET_NULL, null=True, blank=True
|
DeviceManufacturer, models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
model = models.ForeignKey(
|
model = models.ForeignKey(HardwareModel, models.SET_NULL, null=True, blank=True)
|
||||||
HardwareModel, models.SET_NULL, null=True, blank=True
|
location = models.ForeignKey(Location, models.SET_NULL, null=True, blank=True)
|
||||||
)
|
|
||||||
location = models.ForeignKey(
|
|
||||||
Location, models.SET_NULL, null=True, blank=True
|
|
||||||
)
|
|
||||||
user = models.ForeignKey(User, models.SET_NULL, null=True, blank=True)
|
user = models.ForeignKey(User, models.SET_NULL, null=True, blank=True)
|
||||||
installation_date = models.DateField(null=True, blank=True)
|
installation_date = models.DateField(null=True, blank=True)
|
||||||
net = models.ManyToManyField(Net, through="DeviceInNet")
|
net = models.ManyToManyField(Net, through="DeviceInNet")
|
||||||
|
|
|
@ -16,9 +16,7 @@ class WarrantyType(Category):
|
||||||
|
|
||||||
|
|
||||||
class Warranty(models.Model):
|
class Warranty(models.Model):
|
||||||
customer = models.ForeignKey(
|
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, blank=True)
|
||||||
Customer, on_delete=models.CASCADE, blank=True
|
|
||||||
)
|
|
||||||
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
||||||
valid_from = models.DateField()
|
valid_from = models.DateField()
|
||||||
valid_until = models.DateField()
|
valid_until = models.DateField()
|
||||||
|
|
|
@ -12,9 +12,7 @@ def test_device_create_form(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
||||||
form = forms.DeviceCreateForm(user=user, data={})
|
form = forms.DeviceCreateForm(user=user, data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
data = {"name": "pharma-device1", "customer": 3}
|
data = {"name": "pharma-device1", "customer": 3}
|
||||||
form = forms.DeviceCreateForm(user=user, data=data)
|
form = forms.DeviceCreateForm(user=user, data=data)
|
||||||
|
@ -32,9 +30,7 @@ def test_device_update_form(create_admin_user):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.user = fixture["admin"]
|
request.user = fixture["admin"]
|
||||||
form = forms.DeviceUpdateForm(request, data={})
|
form = forms.DeviceUpdateForm(request, data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
data = {"name": "pharma-device1", "customer": 3}
|
data = {"name": "pharma-device1", "customer": 3}
|
||||||
form = forms.DeviceUpdateForm(request, data=data)
|
form = forms.DeviceUpdateForm(request, data=data)
|
||||||
|
@ -50,9 +46,7 @@ def test_device_update_form(create_admin_user):
|
||||||
def test_device_create_form_duplicate_device(create_admin_user):
|
def test_device_create_form_duplicate_device(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
|
||||||
mixer.blend(
|
mixer.blend("devices.Device", name="pharma-device1", customer=fixture["customer"])
|
||||||
"devices.Device", name="pharma-device1", customer=fixture["customer"]
|
|
||||||
)
|
|
||||||
data = {"name": "pharma-device1", "customer": fixture["customer"].id}
|
data = {"name": "pharma-device1", "customer": fixture["customer"].id}
|
||||||
form = forms.DeviceCreateForm(user=user, data=data)
|
form = forms.DeviceCreateForm(user=user, data=data)
|
||||||
assert (
|
assert (
|
||||||
|
|
|
@ -35,9 +35,7 @@ def test_load_device_update_view(create_admin_user):
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
device = mixer.blend("devices.Device", customer=mixer.SELECT)
|
device = mixer.blend("devices.Device", customer=mixer.SELECT)
|
||||||
response = client.get("/update/device/{}/".format(device.pk))
|
response = client.get("/update/device/{}/".format(device.pk))
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, device.name)
|
||||||
response, device.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_device_update_view(create_admin_user):
|
def test_device_update_view(create_admin_user):
|
||||||
|
@ -187,9 +185,7 @@ def test_device_in_net_update_view(create_admin_user):
|
||||||
"mac_address": "",
|
"mac_address": "",
|
||||||
"ip_status": "1",
|
"ip_status": "1",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post("/update/device-in-net/{}/".format(device_in_net.pk), data)
|
||||||
"/update/device-in-net/{}/".format(device_in_net.pk), data
|
|
||||||
)
|
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
device_in_net.refresh_from_db()
|
device_in_net.refresh_from_db()
|
||||||
assert device_in_net.ip == data["ip"]
|
assert device_in_net.ip == data["ip"]
|
||||||
|
|
|
@ -9,9 +9,7 @@ pytestmark = pytest.mark.django_db
|
||||||
def test_warranty_create_form(create_admin_user):
|
def test_warranty_create_form(create_admin_user):
|
||||||
create_admin_user()
|
create_admin_user()
|
||||||
form = forms.WarrantyCreateForm(data={})
|
form = forms.WarrantyCreateForm(data={})
|
||||||
assert (
|
assert form.is_valid() is False, "Should be false because no data was given"
|
||||||
form.is_valid() is False
|
|
||||||
), "Should be false because no data was given"
|
|
||||||
|
|
||||||
device = mixer.blend("devices.Device")
|
device = mixer.blend("devices.Device")
|
||||||
|
|
||||||
|
@ -33,8 +31,7 @@ def test_warranty_create_form(create_admin_user):
|
||||||
form.is_valid() is False
|
form.is_valid() is False
|
||||||
), "Should be false because valid from is before valid until"
|
), "Should be false because valid from is before valid until"
|
||||||
assert (
|
assert (
|
||||||
"Valid from date must be before valid until date"
|
"Valid from date must be before valid until date" == form.errors["__all__"][0]
|
||||||
== form.errors["__all__"][0]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,6 +48,5 @@ def test_warranty_update_form(create_admin_user):
|
||||||
form = forms.WarrantyUpdateForm(data=data)
|
form = forms.WarrantyUpdateForm(data=data)
|
||||||
assert form.is_valid() is False
|
assert form.is_valid() is False
|
||||||
assert (
|
assert (
|
||||||
"Valid from date must be before valid until date"
|
"Valid from date must be before valid until date" == form.errors["__all__"][0]
|
||||||
== form.errors["__all__"][0]
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,9 +39,7 @@ def test_warranties_view_plenty_of_time(create_admin_user):
|
||||||
user.save()
|
user.save()
|
||||||
device = mixer.blend("devices.Device", customer=fixture["customer"])
|
device = mixer.blend("devices.Device", customer=fixture["customer"])
|
||||||
more_than_one_year = datetime.date(datetime.today() + timedelta(400))
|
more_than_one_year = datetime.date(datetime.today() + timedelta(400))
|
||||||
mixer.blend(
|
mixer.blend("devices.Warranty", device=device, valid_until=more_than_one_year)
|
||||||
"devices.Warranty", device=device, valid_until=more_than_one_year
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/warranties/")
|
response = client.get("/warranties/")
|
||||||
|
@ -77,9 +75,7 @@ def test_warranties_view_warranty_one_year_till_expiration(create_admin_user):
|
||||||
user.save()
|
user.save()
|
||||||
device = mixer.blend("devices.Device", customer=fixture["customer"])
|
device = mixer.blend("devices.Device", customer=fixture["customer"])
|
||||||
not_one_year_more = datetime.date(datetime.today() + timedelta(200))
|
not_one_year_more = datetime.date(datetime.today() + timedelta(200))
|
||||||
mixer.blend(
|
mixer.blend("devices.Warranty", device=device, valid_until=not_one_year_more)
|
||||||
"devices.Warranty", device=device, valid_until=not_one_year_more
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/warranties/")
|
response = client.get("/warranties/")
|
||||||
|
|
|
@ -4,9 +4,7 @@ from . import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path("customer/<int:pk>/devices/", views.devices_table_view, name="devices"),
|
||||||
"customer/<int:pk>/devices/", views.devices_table_view, name="devices"
|
|
||||||
),
|
|
||||||
path("device/<int:pk>/", views.device_detail_view, name="device"),
|
path("device/<int:pk>/", views.device_detail_view, name="device"),
|
||||||
path(
|
path(
|
||||||
"manufacturer/<int:pk>/",
|
"manufacturer/<int:pk>/",
|
||||||
|
|
|
@ -60,16 +60,12 @@ def device_detail_view(request, pk):
|
||||||
def devices_table_view(request, pk):
|
def devices_table_view(request, pk):
|
||||||
table = DevicesTable(Device.objects.filter(customer=pk))
|
table = DevicesTable(Device.objects.filter(customer=pk))
|
||||||
RequestConfig(request).configure(table)
|
RequestConfig(request).configure(table)
|
||||||
return render(
|
return render(request, "devices/device_list.html", {"devices": table, "pk": pk})
|
||||||
request, "devices/device_list.html", {"devices": table, "pk": pk}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def warranties_view(request):
|
def warranties_view(request):
|
||||||
table = WarrantiesTable(
|
table = WarrantiesTable(utils.objects_for_allowed_customers(Warranty, request.user))
|
||||||
utils.objects_for_allowed_customers(Warranty, request.user)
|
|
||||||
)
|
|
||||||
RequestConfig(request).configure(table)
|
RequestConfig(request).configure(table)
|
||||||
return render(request, "devices/warranties_list.html", {"devices": table})
|
return render(request, "devices/warranties_list.html", {"devices": table})
|
||||||
|
|
||||||
|
@ -111,9 +107,7 @@ def device_update_view(request, pk):
|
||||||
"""
|
"""
|
||||||
template_name = "devices/device_update.html"
|
template_name = "devices/device_update.html"
|
||||||
request.session["device_to_update"] = pk
|
request.session["device_to_update"] = pk
|
||||||
device = utils.get_object_with_view_permission(
|
device = utils.get_object_with_view_permission(Device, user=request.user, pk=pk)
|
||||||
Device, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
if request.method == "POST" and "save_device" in request.POST:
|
if request.method == "POST" and "save_device" in request.POST:
|
||||||
form = DeviceUpdateForm(request, request.POST, instance=device)
|
form = DeviceUpdateForm(request, request.POST, instance=device)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -124,7 +118,7 @@ def device_update_view(request, pk):
|
||||||
return TemplateResponse(request, template_name, {"form": form})
|
return TemplateResponse(request, template_name, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
class DeviceDeleteView(LoginRequiredMixin, DeleteView):
|
class DeviceDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Device
|
model = Device
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -160,7 +154,7 @@ class WarrantyUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
return self.request.POST.get("previous_page")
|
return self.request.POST.get("previous_page")
|
||||||
|
|
||||||
|
|
||||||
class WarrantyDeleteView(LoginRequiredMixin, DeleteView):
|
class WarrantyDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Warranty
|
model = Warranty
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -195,7 +189,7 @@ class DeviceInNetUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
return self.request.POST.get("previous_page")
|
return self.request.POST.get("previous_page")
|
||||||
|
|
||||||
|
|
||||||
class DeviceInNetDeleteView(LoginRequiredMixin, DeleteView):
|
class DeviceInNetDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = DeviceInNet
|
model = DeviceInNet
|
||||||
template_name = "devices/device_in_net_confirm_delete.html"
|
template_name = "devices/device_in_net_confirm_delete.html"
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,7 @@ class LicenseWithUser(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(fields=["user", "license"], name="user per license")
|
||||||
fields=["user", "license"], name="user per license"
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -61,9 +61,7 @@ def test_customer_license_table_no_license(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/customer/" + str(customer.id) + "/licenses/")
|
response = client.get("/customer/" + str(customer.id) + "/licenses/")
|
||||||
assert response.status_code == 200 and helper.not_in_content(
|
assert response.status_code == 200 and helper.not_in_content(response, customer)
|
||||||
response, customer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_customer_license_table_no_permission(create_admin_user):
|
def test_customer_license_table_no_permission(create_admin_user):
|
||||||
|
|
|
@ -41,7 +41,7 @@ def licenses_table_view(request, pk):
|
||||||
class LicenseWithComputerCreateView(LoginRequiredMixin, CreateView):
|
class LicenseWithComputerCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = LicenseWithComputer
|
model = LicenseWithComputer
|
||||||
template_name = "licenses/license_with_computer_create.html"
|
template_name = "licenses/license_with_computer_create.html"
|
||||||
fields = "__all__"
|
fields = "__all__" # type: ignore
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("computer", args=(self.computer.pk,))
|
return reverse("computer", args=(self.computer.pk,))
|
||||||
|
@ -57,7 +57,7 @@ class LicenseWithComputerCreateView(LoginRequiredMixin, CreateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LicenseWithComputerDeleteView(LoginRequiredMixin, DeleteView):
|
class LicenseWithComputerDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = LicenseWithComputer
|
model = LicenseWithComputer
|
||||||
template_name = "licenses/license_with_computer_confirm_delete.html"
|
template_name = "licenses/license_with_computer_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ class LicenseWithComputerDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
return reverse("computer", args=(self.object.computer.pk,))
|
return reverse("computer", args=(self.object.computer.pk,))
|
||||||
|
|
||||||
|
|
||||||
class UserLicenseDeleteView(LoginRequiredMixin, DeleteView):
|
class UserLicenseDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = UserLicense
|
model = UserLicense
|
||||||
template_name = "licenses/license_confirm_delete.html"
|
template_name = "licenses/license_confirm_delete.html"
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class UserLicenseDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
return reverse("licenses", args=(self.object.customer.pk,))
|
return reverse("licenses", args=(self.object.customer.pk,))
|
||||||
|
|
||||||
|
|
||||||
class ComputerLicenseDeleteView(LoginRequiredMixin, DeleteView):
|
class ComputerLicenseDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = ComputerLicense
|
model = ComputerLicense
|
||||||
template_name = "licenses/license_confirm_delete.html"
|
template_name = "licenses/license_confirm_delete.html"
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault(
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "network_inventory.settings")
|
||||||
"DJANGO_SETTINGS_MODULE", "network_inventory.settings"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
|
@ -11,8 +11,6 @@ def net_view_permission(old_fuction):
|
||||||
if user.has_perm("customers.view_customer", net.customer):
|
if user.has_perm("customers.view_customer", net.customer):
|
||||||
return old_fuction(request, pk)
|
return old_fuction(request, pk)
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden("You're not allowed to access this device.")
|
||||||
"You're not allowed to access this device."
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
|
@ -15,9 +15,7 @@ def test_net_detail_view_no_permission(create_admin_user):
|
||||||
net = mixer.blend("nets.Net")
|
net = mixer.blend("nets.Net")
|
||||||
customer = mixer.blend("customers.Customer")
|
customer = mixer.blend("customers.Customer")
|
||||||
device = mixer.blend("computers.Computer", customer=customer)
|
device = mixer.blend("computers.Computer", customer=customer)
|
||||||
mixer.blend(
|
mixer.blend("devices.DeviceInNet", device=device, net=net, ip="10.7.89.101")
|
||||||
"devices.DeviceInNet", device=device, net=net, ip="10.7.89.101"
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/net/" + str(net.id) + "/")
|
response = client.get("/net/" + str(net.id) + "/")
|
||||||
|
@ -28,9 +26,7 @@ def test_net_detail_view(create_admin_user):
|
||||||
fixture = create_admin_user()
|
fixture = create_admin_user()
|
||||||
net = mixer.blend("nets.Net", customer=mixer.SELECT)
|
net = mixer.blend("nets.Net", customer=mixer.SELECT)
|
||||||
device = mixer.blend("computers.Computer", customer=fixture["customer"])
|
device = mixer.blend("computers.Computer", customer=fixture["customer"])
|
||||||
device_in_net = DeviceInNet.objects.create(
|
device_in_net = DeviceInNet.objects.create(device=device, net=net, ip="10.7.89.101")
|
||||||
device=device, net=net, ip="10.7.89.101"
|
|
||||||
)
|
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/net/" + str(net.id) + "/")
|
response = client.get("/net/" + str(net.id) + "/")
|
||||||
|
|
|
@ -29,12 +29,10 @@ def net_detail_view(request, pk):
|
||||||
net = get_object_or_404(Net, pk=pk)
|
net = get_object_or_404(Net, pk=pk)
|
||||||
table = NetDetailTable(DeviceInNet.objects.filter(net=net))
|
table = NetDetailTable(DeviceInNet.objects.filter(net=net))
|
||||||
RequestConfig(request).configure(table)
|
RequestConfig(request).configure(table)
|
||||||
return render(
|
return render(request, "nets/net_details.html", {"table": table, "net": net})
|
||||||
request, "nets/net_details.html", {"table": table, "net": net}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NetDeleteView(LoginRequiredMixin, DeleteView):
|
class NetDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = Net
|
model = Net
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
from socket import gethostname
|
|
||||||
from socket import gethostbyname_ex
|
|
||||||
|
|
||||||
from .base import *
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
|
||||||
gethostname(),
|
|
||||||
] + list(set(gethostbyname_ex(gethostname())[2]))
|
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
"http://localhost:8080",
|
|
||||||
]
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
|
|
||||||
|
|
||||||
DEBUG = os.environ.get("DJANGO_DEBUG")
|
|
||||||
CRISPY_FAIL_SILENTLY = not DEBUG
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
|
||||||
"NAME": "postgres",
|
|
||||||
"USER": "postgres",
|
|
||||||
"HOST": "db",
|
|
||||||
"PORT": 5432,
|
|
||||||
"PASSWORD": "password",
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,12 +2,15 @@ from socket import gethostname
|
||||||
from socket import gethostbyname
|
from socket import gethostbyname
|
||||||
from socket import getfqdn
|
from socket import getfqdn
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
"localhost",
|
"localhost",
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
"0.0.0.0",
|
||||||
getfqdn(),
|
getfqdn(),
|
||||||
gethostname(),
|
gethostname(),
|
||||||
gethostbyname(gethostname()),
|
gethostbyname(gethostname()),
|
||||||
|
@ -26,10 +29,9 @@ CRISPY_FAIL_SILENTLY = not DEBUG
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": "postgres",
|
"NAME": "django",
|
||||||
"USER": "postgres",
|
"USER": os.environ.get("USER"),
|
||||||
"HOST": "localhost",
|
"HOST": os.environ.get("PGHOST"),
|
||||||
"PORT": 5432,
|
"PORT": os.environ.get("PGPORT"),
|
||||||
"PASSWORD": "password",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,6 @@ def user_view_permission(old_fuction):
|
||||||
if user.has_perm("customers.view_customer", inventory_user.customer):
|
if user.has_perm("customers.view_customer", inventory_user.customer):
|
||||||
return old_fuction(request, pk)
|
return old_fuction(request, pk)
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden(
|
return HttpResponseForbidden("You're not allowed to access this device.")
|
||||||
"You're not allowed to access this device."
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_function
|
return new_function
|
||||||
|
|
|
@ -37,9 +37,7 @@ def test_customer_user_table_no_user(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/customer/" + str(customer.id) + "/users/")
|
response = client.get("/customer/" + str(customer.id) + "/users/")
|
||||||
assert response.status_code == 200 and helper.not_in_content(
|
assert response.status_code == 200 and helper.not_in_content(response, customer)
|
||||||
response, customer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_customer_user_table_no_permission(create_admin_user):
|
def test_customer_user_table_no_permission(create_admin_user):
|
||||||
|
|
|
@ -61,6 +61,4 @@ def test_group_detail_view_with_child_group(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/group/" + str(group.id) + "/")
|
response = client.get("/group/" + str(group.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, child_group)
|
||||||
response, child_group
|
|
||||||
)
|
|
||||||
|
|
|
@ -38,9 +38,7 @@ def test_user_detail_view_group(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/user/" + str(user.id) + "/")
|
response = client.get("/user/" + str(user.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, "Groups")
|
||||||
response, "Groups"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_detail_view_mail_alias(create_admin_user):
|
def test_user_detail_view_mail_alias(create_admin_user):
|
||||||
|
@ -50,9 +48,7 @@ def test_user_detail_view_mail_alias(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/user/" + str(user.id) + "/")
|
response = client.get("/user/" + str(user.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, "Mail Alias")
|
||||||
response, "Mail Alias"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_detail_view_license(create_admin_user):
|
def test_user_detail_view_license(create_admin_user):
|
||||||
|
@ -63,9 +59,7 @@ def test_user_detail_view_license(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/user/" + str(user.id) + "/")
|
response = client.get("/user/" + str(user.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, "License")
|
||||||
response, "License"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_detail_view_computer(create_admin_user):
|
def test_user_detail_view_computer(create_admin_user):
|
||||||
|
@ -75,9 +69,7 @@ def test_user_detail_view_computer(create_admin_user):
|
||||||
client = Client()
|
client = Client()
|
||||||
client.login(username="pharma-admin", password="password")
|
client.login(username="pharma-admin", password="password")
|
||||||
response = client.get("/user/" + str(user.id) + "/")
|
response = client.get("/user/" + str(user.id) + "/")
|
||||||
assert response.status_code == 200 and helper.in_content(
|
assert response.status_code == 200 and helper.in_content(response, computer)
|
||||||
response, computer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_detail_view_no_permission(create_admin_user):
|
def test_user_detail_view_no_permission(create_admin_user):
|
||||||
|
|
|
@ -52,7 +52,7 @@ def user_detail_view(request, pk):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserDeleteView(LoginRequiredMixin, DeleteView):
|
class UserDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -64,9 +64,7 @@ class UserDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
def groups_table_view(request, pk):
|
def groups_table_view(request, pk):
|
||||||
customer = get_object_or_404(Customer, pk=pk)
|
customer = get_object_or_404(Customer, pk=pk)
|
||||||
groups_table = GroupsTable(
|
groups_table = GroupsTable(
|
||||||
utils.get_objects_for_customer(
|
utils.get_objects_for_customer(Group, user=request.user, customer_pk=pk)
|
||||||
Group, user=request.user, customer_pk=pk
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
RequestConfig(request).configure(groups_table)
|
RequestConfig(request).configure(groups_table)
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
|
@ -81,9 +79,7 @@ def groups_table_view(request, pk):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def group_detail_view(request, pk):
|
def group_detail_view(request, pk):
|
||||||
group = utils.get_object_with_view_permission(
|
group = utils.get_object_with_view_permission(Group, user=request.user, pk=pk)
|
||||||
Group, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
users = group.user_set.all()
|
users = group.user_set.all()
|
||||||
groups = Group.objects.filter(parent_group=group)
|
groups = Group.objects.filter(parent_group=group)
|
||||||
print(groups)
|
print(groups)
|
||||||
|
@ -96,9 +92,7 @@ def group_detail_view(request, pk):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def delete_group(request, pk):
|
def delete_group(request, pk):
|
||||||
group = utils.get_object_with_view_permission(
|
group = utils.get_object_with_view_permission(Group, user=request.user, pk=pk)
|
||||||
Group, user=request.user, pk=pk
|
|
||||||
)
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
group.delete()
|
group.delete()
|
||||||
return redirect("groups", pk=group.customer.pk)
|
return redirect("groups", pk=group.customer.pk)
|
||||||
|
|
Loading…
Reference in New Issue