Merge pull request #156 from Nebucatnetzer/dev

Update master
This commit is contained in:
Andreas Zweili 2023-08-28 20:45:59 +02:00 committed by GitHub
commit c92d38bf62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1588 additions and 1510 deletions

View File

@ -1,4 +1,9 @@
[paths]
source = ${PROJECT_DIR-default .}/src/
[html]
directory = ${PROJECT_DIR-default .}/htmlcov
[run]
data_file = ${PROJECT_DIR-default .}/.coverage
omit =
*apps.py,
*migrations/*,

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
[*.sh]
indent_style = space
indent_size = 4
[.envrc]
indent_style = space
indent_size = 4

46
.envrc
View File

@ -1,2 +1,48 @@
use flake
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"

View File

@ -1,8 +0,0 @@
[flake8]
exclude =
*migrations*,
__init__.py,
*cache*,
venv/,
src/manage.py,
src/network_inventory/settings/*

View File

@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v18
- uses: cachix/cachix-action@v12
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
name: networkinventory
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
@ -28,11 +28,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v18
- uses: cachix/cachix-action@v12
with:
name: networkinventory
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Buid container
run: |
nix build .#container

View File

@ -11,11 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v18
- uses: cachix/cachix-action@v12
with:
name: networkinventory
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Test
run: |
nix flake check -L -j auto
run: nix develop --command bash -c "dev test"
env:
PROJECT_DIR: ${{ github.workspace }}

2
.gitignore vendored
View File

@ -173,7 +173,7 @@ migrations/
.vscode/
.pytest_cache/
htmlcov/
.second_run
.first_run
/src/static
.idea/

2
Procfile Normal file
View File

@ -0,0 +1,2 @@
web: python ./src/manage.py runserver 0.0.0.0:$WEBPORT
db: postgres -p $PGPORT

View File

@ -14,26 +14,62 @@ inventory over my various servers and other network equipment.
## Development Setup
There are two ways to work on this project.
For the first one you will need to install the Nix package manager[^1].
Afterwards you can enter the development environment with `nix develop`.
There is currently only one supported way to work with this repository. You
will need a Linux system (WSL might work) onto wich you install the Nix package
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
enter the virtual environment.
[^1]: https://nixos.org/download.html
[^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.
[^1]: https://nixos.org/download.html
[^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
To customise the application in the Docker container you can use environment

154
dev.sh
View File

@ -1,15 +1,15 @@
#!/usr/bin/env bash
run () {
setup
find . -name __pycache__ -o -name "*.pyc" -delete
sudo iptables -I INPUT -p tcp --dport 8000 -j ACCEPT
python ./src/manage.py runserver 0.0.0.0:8000
# Helper functions not exposed to the user {
# Load example data
_init() {
python ./src/manage.py loaddata src/network_inventory.yaml
}
setup () {
docker-compose -f docker-compose-development.yml up -d
if [ -f .second_run ]; then
# Setup the database
_setup() {
overmind start -l db -D
if [ -f .direnv/first_run ]; then
sleep 2
python ./src/manage.py collectstatic --noinput
python ./src/manage.py makemigrations
@ -34,59 +34,129 @@ setup () {
python ./src/manage.py loaddata nets
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')"
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
}
venv () {
nix build .#venv -o venv
_create_url() {
if [ -f /etc/wsl.conf ]; then
echo "http://localhost:$WEBPORT"
else
echo "http://$(hostname -f):$WEBPORT"
fi
}
#}
docker (){
nix build && docker load < result && docker run --rm -ti network-inventory:latest
# Main tasks start
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 () {
docker-compose -f docker-compose-development.yml down -v
stop() {
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
rm -rf htmlcov/
rm -f */migrations/0*.py
rm .second_run
rm -f .direnv/first_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 () {
clean
docker-compose -f docker-compose-development.yml down -v --rmi local
rm -r .venv
cleanall() {
git clean -xdf
}
descriptions["cleanall"]="Completly remove any files which are not checked into git."
tasks["cleanall"]=cleanall
init () {
python ./src/manage.py loaddata network_inventory.yaml
}
debug () {
debug() {
pytest --pdb --nomigrations --cov=. --cov-report=html ./src/
}
descriptions["debug"]="Run the tests and drop into the debugger on failure."
tasks["debug"]=debug
test (){
nix flake check
lint() {
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
if [ $# != 1 ]; then
echo "usage: $0 <task_name>"
echo "All tasks: ${tasks[@]}"
printf "usage: dev <task_name>\n\n"
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
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

View File

@ -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"

View File

@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1687709756,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
"type": "github"
},
"original": {
@ -16,12 +19,15 @@
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
@ -30,13 +36,34 @@
"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": {
"locked": {
"lastModified": 1670242877,
"narHash": "sha256-jBLh7dRHnbfvPPA9znOC6oQfKrCPJ0El8Zoe0BqnCjQ=",
"lastModified": 1688918189,
"narHash": "sha256-f8ZlJ67LgEUDnN7ZsAyd1/Fyby1VdOXWg4XY/irSGrQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6e51c97f1c849efdfd4f3b78a4870e6aa2da4198",
"rev": "408c0e8c15a1c9cf5c3226931b6f283c9867c484",
"type": "github"
},
"original": {
@ -49,16 +76,17 @@
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1670326426,
"narHash": "sha256-I5IscrjGuCbvpFIRoiappUwBBOq8OODvGLkapnn/ECA=",
"lastModified": 1693051011,
"narHash": "sha256-HNbuVCS/Fnl1YZOjBk9/MlIem+wM8fvIzTH0CVQrLSQ=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "293dd5c31167540193bf2b66cec636eecd1fc788",
"rev": "5b3a5151cf212021ff8d424f215fb030e4ff2837",
"type": "github"
},
"original": {
@ -73,6 +101,36 @@
"nixpkgs": "nixpkgs",
"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",

View File

@ -1,8 +1,8 @@
{
description = "A Python API for various tools I use at work.";
inputs = {
nixpkgs.url = github:NixOS/nixpkgs/nixos-unstable;
flake-utils.url = github:numtide/flake-utils;
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
@ -45,31 +45,19 @@
rec {
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.gnumake
pkgs.inventoryDevEnv
pkgs.poetry
pkgs.python310Packages.pip
pkgs.overmind
pkgs.postgresql_15
(pkgs.writeScriptBin "dev" "${builtins.readFile ./dev.sh}")
];
PYTHON_KEYRING_BACKEND = "keyring.backends.fail.Keyring";
shellHook = ''
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local
'';
};
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 {
dontPatch = true;
dontConfigure = true;
@ -82,10 +70,10 @@
checkPhase = ''
mkdir -p $out
pytest --ds=network_inventory.settings.ram_test \
-nauto \
--nomigrations \
--cov=./src \
./src
-nauto \
--nomigrations \
--cov=./src \
./src
'';
};
};
@ -102,7 +90,7 @@
pkgs.coreutils
inventory
(pkgs.writeShellScriptBin "start-inventory" ''
if [ -f .second_run ]; then
if [ -f .first_run ]; then
sleep 2
${pkgs.inventoryEnv}/bin/django-admin collectstatic --noinput
${pkgs.inventoryEnv}/bin/django-admin makemigrations
@ -127,7 +115,7 @@
${pkgs.inventoryEnv}/bin/django-admin loaddata nets
${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')"
touch .second_run
touch .first_run
fi
${pkgs.inventoryEnv}/bin/gunicorn network_inventory.wsgi:application --reload --bind 0.0.0.0:8000 --workers 3
'')

2078
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,18 @@
[tool.black]
line-length = 79
[tool.pylint]
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]
name = "network_inventory"
@ -11,36 +24,73 @@ packages = [
{ 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]
python = "^3.9"
Django = "^4.1.3"
django-crispy-forms = "^1.14.0"
django-filter = "^22.1"
django-filter = "^23.2"
django-floppyforms = "^1.9.0"
django-guardian = "^2.4.0"
django-htmx = "^1.13.0"
django-model-utils = "^4.2.0"
django-nested-admin = "^4.0.2"
django-tables2 = "^2.4.1"
django-tables2 = "^2.4.1,<2.6.0"
gunicorn = "^20.1.0"
psycopg2-binary = "^2.9.5"
PyYAML = "^6.0"
[tool.poetry.group.dev.dependencies]
autopep8 = "^2.0.0"
black = "^22.10.0"
coverage = "^6.5.0"
flake8 = "^6.0.0"
jedi = "^0.18.2"
mixer = "^7.2.2"
pep8 = "^1.7.1"
pylint = "^2.15.8"
pytest = "^7.2.0"
pytest-cov = "^4.0.0"
pytest-django = "^4.5.2"
pytest-xdist = "^3.1.0"
rope = "^1.5.1"
yapf = "^0.32.0"
python-lsp-server = "^1.7.3"
mypy = "^1.4.1"
django-stubs = "^4.2.3"
pylint-django = "^2.5.3"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -11,8 +11,6 @@ def backup_view_permission(old_fuction):
if user.has_perm("customers.view_customer", backup.computer.customer):
return old_fuction(request, pk)
else:
return HttpResponseForbidden(
"You're not allowed to access this device."
)
return HttpResponseForbidden("You're not allowed to access this device.")
return new_function

View File

@ -20,18 +20,12 @@ class Backup(models.Model):
computer = models.ForeignKey(
Computer, related_name="source_computer", on_delete=models.CASCADE
)
method = models.ForeignKey(
BackupMethod, models.SET_NULL, blank=True, null=True
)
software = models.ForeignKey(
Software, models.SET_NULL, blank=True, null=True
)
method = models.ForeignKey(BackupMethod, 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)
exec_time = models.TimeField(null=True, blank=True)
exec_days = models.ManyToManyField(Weekday, blank=True)
target_device = models.ManyToManyField(
Computer, through="TargetDevice", blank=True
)
target_device = models.ManyToManyField(Computer, through="TargetDevice", blank=True)
class Meta:
ordering = ["name"]
@ -50,9 +44,7 @@ class Backup(models.Model):
class TargetDevice(models.Model):
device = models.ForeignKey(
Computer, models.SET_NULL, blank=True, null=True
)
device = models.ForeignKey(Computer, models.SET_NULL, blank=True, null=True)
backup = models.ForeignKey(Backup, on_delete=models.CASCADE)
target_path = models.CharField(max_length=200, blank=True)

View File

@ -53,9 +53,7 @@ def test_backup_detail_view_with_target_device(create_admin_user):
software=mixer.SELECT,
method=mixer.SELECT,
)
mixer.blend(
"backups.TargetDevice", device=target_computer, backup=mixer.SELECT
)
mixer.blend("backups.TargetDevice", device=target_computer, backup=mixer.SELECT)
client = Client()
client.login(username="pharma-admin", password="password")
response = client.get("/backup/" + str(backup.id) + "/")
@ -79,9 +77,7 @@ def test_backup_detail_view_with_notification(create_admin_user):
client = Client()
client.login(username="pharma-admin", password="password")
response = client.get("/backup/" + str(backup.id) + "/")
assert response.status_code == 200 and helper.in_content(
response, notification
)
assert response.status_code == 200 and helper.in_content(response, notification)
def test_backup_detail_view_with_day_relation(create_admin_user):

View File

@ -23,9 +23,7 @@ def test_customer_backup_table(create_admin_user):
computer = mixer.blend("computers.Computer", customer=customer)
backup = mixer.blend("backups.Backup", computer=computer)
response = client.get("/customer/" + str(customer.id) + "/backups/")
assert response.status_code == 200 and helper.in_content(
response, backup.name
)
assert response.status_code == 200 and helper.in_content(response, backup.name)
def test_customer_backup_table_no_backup(create_admin_user):

View File

@ -3,9 +3,7 @@ from django.urls import path
from . import views
urlpatterns = [
path(
"customer/<int:pk>/backups/", views.backups_table_view, name="backups"
),
path("customer/<int:pk>/backups/", views.backups_table_view, name="backups"),
path("backup/<int:pk>/", views.backup_detail_view, name="backup"),
path(
"create/backup-for-computer/<int:pk>/",

View File

@ -47,7 +47,7 @@ def backup_detail_view(request, pk):
class BackupCreateView(LoginRequiredMixin, CreateView):
model = Backup
template_name = "backups/backup_create.html"
fields = "__all__"
fields = "__all__" # type: ignore
def get_success_url(self):
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
template_name = "backups/backup_confirm_delete.html"
@ -71,7 +71,7 @@ class BackupDeleteView(LoginRequiredMixin, DeleteView):
return reverse("computer", args=(self.object.computer.pk,))
class BackupDeleteFromTableView(LoginRequiredMixin, DeleteView):
class BackupDeleteFromTableView(LoginRequiredMixin, DeleteView): # type: ignore
model = Backup
template_name = "backups/backup_confirm_delete.html"

View File

@ -26,62 +26,64 @@ from .models import (
)
class SoftwareInLine(nested_admin.NestedStackedInline):
class SoftwareInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = ComputerSoftwareRelation
extra = 0
verbose_name_plural = "Software"
class RamInLine(nested_admin.NestedStackedInline):
class RamInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = ComputerRamRelation
extra = 0
verbose_name_plural = "RAM Modules"
class DiskInLine(nested_admin.NestedStackedInline):
class DiskInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = ComputerDiskRelation
extra = 0
verbose_name_plural = "Disks"
class DisksInRaidInLine(nested_admin.NestedStackedInline):
class DisksInRaidInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = DisksInRaid
extra = 0
verbose_name_plural = "Disks in RAID"
class CpusInLine(nested_admin.NestedStackedInline):
class CpusInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = ComputerCpuRelation
extra = 0
verbose_name_plural = "CPUs"
class GpusInLine(nested_admin.NestedStackedInline):
class GpusInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = ComputerGpuRelation
extra = 0
verbose_name_plural = "GPUs"
class RaidInLine(nested_admin.NestedStackedInline):
class RaidInLine(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = Raid
extra = 0
verbose_name_plural = "RAID"
inlines = (DisksInRaidInLine,)
class DeviceInNetInline(nested_admin.NestedStackedInline):
class DeviceInNetInline(nested_admin.NestedStackedInline): # pylint: disable=no-member
model = DeviceInNet
extra = 0
verbose_name_plural = "Nets"
class LicenseWithComputerInLine(nested_admin.NestedStackedInline):
class LicenseWithComputerInLine(
nested_admin.NestedStackedInline
): # pylint: disable=no-member
model = LicenseWithComputer
extra = 0
verbose_name_plural = "Licenses"
class ComputerAdmin(nested_admin.NestedModelAdmin):
class ComputerAdmin(nested_admin.NestedModelAdmin): # pylint: disable=no-member
list_display = ("name", "host")
inlines = (
SoftwareInLine,

View File

@ -41,15 +41,9 @@ class ComputerUpdateForm(forms.ModelForm):
def __init__(self, request, *args, **kwargs):
super(ComputerUpdateForm, self).__init__(*args, **kwargs)
customers = utils.objects_for_allowed_customers(
Customer, user=request.user
)
locations = utils.objects_for_allowed_customers(
Location, user=request.user
)
hosts = utils.objects_for_allowed_customers(
Computer, user=request.user
)
customers = utils.objects_for_allowed_customers(Customer, 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)
self.fields["customer"].queryset = customers
self.fields["location"].queryset = locations

View File

@ -23,12 +23,8 @@ class Computer(Device):
ram = models.ManyToManyField(Ram, through="ComputerRamRelation")
gpu = models.ManyToManyField(Gpu, through="ComputerGpuRelation")
disks = models.ManyToManyField(Disk, through="ComputerDiskRelation")
software = models.ManyToManyField(
Software, through="ComputerSoftwareRelation"
)
host = models.ForeignKey(
"self", null=True, blank=True, on_delete=models.CASCADE
)
software = models.ManyToManyField(Software, through="ComputerSoftwareRelation")
host = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
allocated_space = models.IntegerField(null=True, blank=True)
def __str__(self):

View File

@ -12,9 +12,7 @@ class RaidType(Category):
class Raid(models.Model):
usable_space = models.IntegerField(blank=True, null=True)
raid_type = models.ForeignKey(
RaidType, models.SET_NULL, blank=True, null=True
)
raid_type = models.ForeignKey(RaidType, models.SET_NULL, blank=True, null=True)
computer = models.ForeignKey(Computer, on_delete=models.CASCADE)
def __str__(self):

View File

@ -15,15 +15,11 @@ def test_computer_detail_view_not_logged_in():
def test_computer_detail_view(create_admin_user):
create_admin_user()
computer = mixer.blend(
"computers.Computer", customer=mixer.SELECT, os=mixer.SELECT
)
computer = mixer.blend("computers.Computer", customer=mixer.SELECT, os=mixer.SELECT)
client = Client()
client.login(username="pharma-admin", password="password")
response = client.get("/computer/" + str(computer.id) + "/")
assert response.status_code == 200 and helper.in_content(
response, computer
)
assert response.status_code == 200 and helper.in_content(response, computer)
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.login(username="pharma-admin", password="password")
response = client.get("/computer/" + str(computer.id) + "/")
assert response.status_code == 200 and helper.in_content(
response, "RAM Modules:"
)
assert response.status_code == 200 and helper.in_content(response, "RAM Modules:")
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)
raid_type = mixer.blend("computers.RaidType")
disk = mixer.blend("computers.Disk")
raid = mixer.blend(
"computers.Raid", computer=computer, raid_type=raid_type
)
raid = mixer.blend("computers.Raid", computer=computer, raid_type=raid_type)
mixer.blend("computers.DisksInRaid", raid=raid, disk=disk)
client = Client()
client.login(username="pharma-admin", password="password")

View File

@ -12,9 +12,7 @@ def test_computer_create_form(create_admin_user):
fixture = create_admin_user()
user = mixer.blend("core.InventoryUser", customer=fixture["customer"])
form = forms.ComputerCreateForm(user=user, data={})
assert (
form.is_valid() is False
), "Should be false because no data was given"
assert form.is_valid() is False, "Should be false because no data was given"
data = {"name": "pharma-pc1", "customer": 3}
form = forms.ComputerCreateForm(user=user, data=data)
@ -32,9 +30,7 @@ def test_computer_update_form(create_admin_user):
request = HttpRequest()
request.user = fixture["admin"]
form = forms.ComputerUpdateForm(request, data={})
assert (
form.is_valid() is False
), "Should be false because no data was given"
assert form.is_valid() is False, "Should be false because no data was given"
data = {"name": "pharma-pc1", "customer": 20356}
form = forms.ComputerUpdateForm(request, data=data)

View File

@ -27,6 +27,4 @@ def test_computer_list_view(create_admin_user):
client = Client()
client.login(username="pharma-admin", password="password")
response = client.get("/computers/all/")
assert response.status_code == 200 and helper.in_content(
response, computer
)
assert response.status_code == 200 and helper.in_content(response, computer)

View File

@ -21,9 +21,7 @@ def test_customer_computer_table(create_admin_user):
client.login(username="pharma-admin", password="password")
computer = mixer.blend("computers.Computer", customer=mixer.SELECT)
response = client.get("/customer/" + str(customer.id) + "/computers/")
assert response.status_code == 200 and helper.in_content(
response, computer
)
assert response.status_code == 200 and helper.in_content(response, computer)
def test_customer_computer_table_no_computer(create_admin_user):

View File

@ -41,9 +41,7 @@ from .tables import ComputersTable
@login_required
def computer_detail_view(request, pk):
device = utils.get_object_with_view_permission(
Computer, user=request.user, pk=pk
)
device = utils.get_object_with_view_permission(Computer, user=request.user, pk=pk)
disks_relations = ComputerDiskRelation.objects.filter(computer=pk)
warranty_relations = Warranty.objects.filter(device=pk)
ram_relations = ComputerRamRelation.objects.filter(computer=pk)
@ -76,9 +74,7 @@ def computer_detail_view(request, pk):
@login_required
def computers_table_view(request, pk):
table = ComputersTable(
utils.get_objects_for_customer(
Computer, user=request.user, customer_pk=pk
)
utils.get_objects_for_customer(Computer, user=request.user, customer_pk=pk)
)
RequestConfig(request).configure(table)
return render(
@ -139,9 +135,7 @@ def computer_update_view(request, pk):
A view to create a customer.
"""
template_name = "computers/computer_update.html"
computer = utils.get_object_with_view_permission(
Computer, user=request.user, pk=pk
)
computer = utils.get_object_with_view_permission(Computer, user=request.user, pk=pk)
if request.method == "POST":
form = ComputerUpdateForm(request, request.POST, instance=computer)
if form.is_valid():
@ -152,7 +146,7 @@ def computer_update_view(request, pk):
return TemplateResponse(request, template_name, {"form": form})
class ComputerDeleteView(LoginRequiredMixin, DeleteView):
class ComputerDeleteView(LoginRequiredMixin, DeleteView): # type: ignore
model = Computer
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
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
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
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
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
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
template_name = "computers/relation_confirm_delete.html"

View File

@ -22,9 +22,7 @@ def django_db_setup(django_db_setup, django_db_blocker):
def create_admin_user():
def _create_admin_user():
User = get_user_model()
admin = User.objects.create_user(
"pharma-admin", "admin@pharma.com", "password"
)
admin = User.objects.create_user("pharma-admin", "admin@pharma.com", "password")
customer = mixer.blend("customers.Customer")
group = Group.objects.create(name="Pharma Corp. Admin")
admin.groups.add(group)

View File

@ -15,9 +15,7 @@ def test_get_object_with_view_permission(create_admin_user):
fixture = create_admin_user()
customer = fixture["customer"]
admin = fixture["admin"]
object = utils.get_object_with_view_permission(
Customer, user=admin, pk=customer.id
)
object = utils.get_object_with_view_permission(Customer, user=admin, pk=customer.id)
assert object == customer
@ -26,9 +24,7 @@ def test_get_object_with_view_permission_device(create_admin_user):
customer = fixture["customer"]
admin = fixture["admin"]
device = mixer.blend(Device, customer=customer)
object = utils.get_object_with_view_permission(
Device, user=admin, pk=device.id
)
object = utils.get_object_with_view_permission(Device, user=admin, pk=device.id)
assert object == device
@ -37,9 +33,7 @@ def test_get_object_without_view_permission(create_admin_user):
customer = mixer.blend(Customer)
admin = fixture["admin"]
with pytest.raises(Http404):
utils.get_object_with_view_permission(
Customer, user=admin, pk=customer.id
)
utils.get_object_with_view_permission(Customer, user=admin, pk=customer.id)
def test_get_object_without_view_permission_device(create_admin_user):

View File

@ -16,9 +16,7 @@ def test_get_objects_for_customer_with_customer(create_admin_user):
customer = fixture["customer"]
admin = fixture["admin"]
with pytest.raises(Exception):
utils.get_objects_for_customer(
Customer, user=admin, customer_pk=customer.id
)
utils.get_objects_for_customer(Customer, user=admin, customer_pk=customer.id)
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)
admin = fixture["admin"]
with pytest.raises(Http404):
utils.get_objects_for_customer(
Customer, user=admin, customer_pk=customer.id
)
utils.get_objects_for_customer(Customer, user=admin, customer_pk=customer.id)
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"]
mixer.blend(Device, customer=customer)
with pytest.raises(Http404):
utils.get_objects_for_customer(
Device, user=admin, customer_pk=customer.id
)
utils.get_objects_for_customer(Device, user=admin, customer_pk=customer.id)

View File

@ -34,9 +34,7 @@ def _get_customers(user):
user : django.contrib.auth.models.User
"""
return get_objects_for_user(
user, "customers.view_customer", klass=Customer
)
return get_objects_for_user(user, "customers.view_customer", klass=Customer)
def get_object_with_view_permission(model, user=None, pk=None):

View File

@ -10,8 +10,6 @@ def customer_view_permission(old_function):
if user.has_perm("customers.view_customer", customer):
return old_function(request, pk)
else:
return HttpResponseForbidden(
"You're not allowed to access this page."
)
return HttpResponseForbidden("You're not allowed to access this page.")
return new_function