mirror of
https://github.com/Nebucatnetzer/network_inventory.git
synced 2024-06-30 21:21:01 +02:00
commit
ccafdf1c99
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -144,6 +144,7 @@ celerybeat-schedule
|
||||||
# virtualenv
|
# virtualenv
|
||||||
.venv
|
.venv
|
||||||
venv/
|
venv/
|
||||||
|
venv
|
||||||
ENV/
|
ENV/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.pythonPath": "./venv/bin/python",
|
"python.pythonPath": "./venv/bin/python3",
|
||||||
"python.linting.pylintEnabled": false,
|
"python.linting.pylintEnabled": false,
|
||||||
"python.linting.flake8Enabled": true,
|
"python.linting.flake8Enabled": true,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
|
|
23
Makefile
23
Makefile
|
@ -5,17 +5,13 @@ SHELL=/usr/bin/env bash
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: setup
|
run: setup
|
||||||
( \
|
( \
|
||||||
source venv/bin/activate; \
|
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
|
|
||||||
find . -name __pycache__ -o -name "*.pyc" -delete; \
|
find . -name __pycache__ -o -name "*.pyc" -delete; \
|
||||||
python manage.py runserver; \
|
python manage.py runserver; \
|
||||||
)
|
)
|
||||||
|
|
||||||
.PHONY: setup
|
.PHONY: setup
|
||||||
setup: ./venv
|
setup:
|
||||||
( \
|
( \
|
||||||
source venv/bin/activate; \
|
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
|
|
||||||
docker-compose -f docker-compose-development.yml up -d; \
|
docker-compose -f docker-compose-development.yml up -d; \
|
||||||
if [ -f .second_run ]; then \
|
if [ -f .second_run ]; then \
|
||||||
sleep 2; \
|
sleep 2; \
|
||||||
|
@ -46,12 +42,9 @@ setup: ./venv
|
||||||
fi; \
|
fi; \
|
||||||
)
|
)
|
||||||
|
|
||||||
./venv:
|
venv:
|
||||||
python3 -m venv venv
|
nix build .#venv -o venv
|
||||||
( \
|
|
||||||
source venv/bin/activate; \
|
|
||||||
pip3 install -r requirements/local.txt; \
|
|
||||||
)
|
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
@ -64,28 +57,22 @@ clean:
|
||||||
.PHONY: cleanall
|
.PHONY: cleanall
|
||||||
cleanall: clean
|
cleanall: clean
|
||||||
docker-compose -f docker-compose-development.yml down -v --rmi local
|
docker-compose -f docker-compose-development.yml down -v --rmi local
|
||||||
rm -rf venv/
|
rm venv
|
||||||
|
|
||||||
.PHONY: init
|
.PHONY: init
|
||||||
init:
|
init:
|
||||||
( \
|
( \
|
||||||
source venv/bin/activate; \
|
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
|
|
||||||
python manage.py loaddata network_inventory.yaml; \
|
python manage.py loaddata network_inventory.yaml; \
|
||||||
)
|
)
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
( \
|
( \
|
||||||
source venv/bin/activate; \
|
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
|
|
||||||
pytest -nauto --nomigrations --cov=. --cov-report=html; \
|
pytest -nauto --nomigrations --cov=. --cov-report=html; \
|
||||||
)
|
)
|
||||||
|
|
||||||
.PHONY: debug
|
.PHONY: debug
|
||||||
debug:
|
debug:
|
||||||
( \
|
( \
|
||||||
source venv/bin/activate; \
|
|
||||||
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
|
|
||||||
pytest --pdb --nomigrations --cov=. --cov-report=html; \
|
pytest --pdb --nomigrations --cov=. --cov-report=html; \
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,11 @@ from django import forms
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Submit, Button, Field
|
from crispy_forms.layout import Submit, Button
|
||||||
from crispy_forms.bootstrap import FormActions
|
from crispy_forms.bootstrap import FormActions
|
||||||
|
|
||||||
from .models import Customer
|
from core import utils
|
||||||
|
from .models import Customer, DummyLocation, Location
|
||||||
|
|
||||||
|
|
||||||
class CustomerForm(forms.ModelForm):
|
class CustomerForm(forms.ModelForm):
|
||||||
|
@ -30,3 +31,43 @@ class CustomerForm(forms.ModelForm):
|
||||||
Button('cancel', 'Cancel', css_class="btn btn-secondary",
|
Button('cancel', 'Cancel', css_class="btn btn-secondary",
|
||||||
onclick="closeModal()")
|
onclick="closeModal()")
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class LocationForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Location
|
||||||
|
fields = (
|
||||||
|
'name',
|
||||||
|
'customer'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop("user")
|
||||||
|
super(LocationForm, self).__init__(*args, **kwargs)
|
||||||
|
"""
|
||||||
|
If the user is not a superuser it's always assigned to a customer which
|
||||||
|
we can use to assign to the field.
|
||||||
|
"""
|
||||||
|
self.fields['customer'].queryset = (
|
||||||
|
utils.objects_for_allowed_customers(
|
||||||
|
Customer, user=user))
|
||||||
|
|
||||||
|
self.helper = FormHelper(self)
|
||||||
|
self.helper.attrs = {
|
||||||
|
'hx-post': reverse_lazy('htmx_create_location'),
|
||||||
|
'id': 'location-form',
|
||||||
|
}
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
Submit('save_location', 'Save'),
|
||||||
|
Button('cancel', 'Cancel', css_class="btn btn-secondary",
|
||||||
|
onclick="closeModal()")
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLocationForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = DummyLocation
|
||||||
|
fields = (
|
||||||
|
'location',
|
||||||
|
)
|
||||||
|
|
|
@ -33,3 +33,10 @@ class Location(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLocation(models.Model):
|
||||||
|
location = models.ForeignKey(Location, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.location
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% include "core/partials/modal.html" with form=form modal_title="Add Location"%}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% if valid %}
|
||||||
|
<div hx-swap-oob="true:#htmx-location-target">{{ form }}</div>
|
||||||
|
<div id="htmx-modal-position" hx-swap-oob="true"></div>
|
||||||
|
{% else %}
|
||||||
|
{{ form }}
|
||||||
|
{% endif %}
|
25
customers/tests/test_location_form.py
Normal file
25
customers/tests/test_location_form.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import pytest
|
||||||
|
from mixer.backend.django import mixer
|
||||||
|
|
||||||
|
from customers import forms
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_location_form(create_admin_user):
|
||||||
|
fixture = create_admin_user()
|
||||||
|
user = fixture['admin']
|
||||||
|
form = forms.LocationForm(user=user, data={})
|
||||||
|
assert form.is_valid() is False, (
|
||||||
|
"Should be false because no data was given")
|
||||||
|
|
||||||
|
data = {"name": "Main Office",
|
||||||
|
"customer": 3}
|
||||||
|
form = forms.LocationForm(user=user, data=data)
|
||||||
|
assert form.is_valid() is False, (
|
||||||
|
"Should be false because the customer doesn't exist.")
|
||||||
|
|
||||||
|
data = {"name": mixer.blend('customers.Location').name,
|
||||||
|
"customer": fixture['customer'].id}
|
||||||
|
form = forms.LocationForm(user=user, data=data)
|
||||||
|
assert form.is_valid() is True, ("Should be valid with the given data.")
|
40
customers/tests/test_location_form_view.py
Normal file
40
customers/tests/test_location_form_view.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from django.test import Client
|
||||||
|
from mixer.backend.django import mixer
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.tests import helper
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_htmx_create_location_view(create_admin_user):
|
||||||
|
create_admin_user()
|
||||||
|
client = Client()
|
||||||
|
client.login(username="pharma-admin", password="password")
|
||||||
|
url = '/create/location/'
|
||||||
|
response = client.get(url)
|
||||||
|
assert (response.status_code == 200
|
||||||
|
and helper.in_content(response, 'Add Location'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_htmx_create_location_view(create_admin_user):
|
||||||
|
create_admin_user()
|
||||||
|
client = Client()
|
||||||
|
client.login(username="pharma-admin", password="password")
|
||||||
|
data = {"name": mixer.faker.name(),
|
||||||
|
"save_location": 1}
|
||||||
|
response = client.post('/create/location/', data)
|
||||||
|
assert (response.status_code == 200
|
||||||
|
and helper.in_content(response, data["name"]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_htmx_create_location_view_invalid_form(create_admin_user):
|
||||||
|
create_admin_user()
|
||||||
|
client = Client()
|
||||||
|
client.login(username="pharma-admin", password="password")
|
||||||
|
data = {"name": "",
|
||||||
|
"save_location": 1}
|
||||||
|
response = client.post('/create/location/', data)
|
||||||
|
assert (response.status_code == 200
|
||||||
|
and helper.in_content(response, "This field is required."))
|
|
@ -11,4 +11,6 @@ urlpatterns = [
|
||||||
path('create/customer/',
|
path('create/customer/',
|
||||||
views.create_customer,
|
views.create_customer,
|
||||||
name='customer_create'),
|
name='customer_create'),
|
||||||
|
path('create/location/', views.htmx_create_location,
|
||||||
|
name='htmx_create_location')
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,13 +3,19 @@ from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.template.context_processors import csrf
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import DeleteView
|
from django.views.generic import DeleteView
|
||||||
|
|
||||||
|
from crispy_forms.utils import render_crispy_form
|
||||||
|
from crispy_forms.templatetags.crispy_forms_filters import as_crispy_field
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
from .forms import CustomerForm
|
from .forms import CustomerForm
|
||||||
from .models import Customer
|
from .forms import DummyLocationForm
|
||||||
|
from .forms import LocationForm
|
||||||
|
from .models import Customer, DummyLocation
|
||||||
from .tables import CustomersTable
|
from .tables import CustomersTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,3 +63,30 @@ class CustomerDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('customers')
|
return reverse('customers')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def htmx_create_location(request):
|
||||||
|
context = {}
|
||||||
|
user = request.user
|
||||||
|
if request.method == "POST" and 'save_location' in request.POST:
|
||||||
|
form = LocationForm(request.POST, user=user)
|
||||||
|
if form.is_valid():
|
||||||
|
location = form.save(commit=True)
|
||||||
|
dummy_model = DummyLocation()
|
||||||
|
dummy_model.location = location
|
||||||
|
dummy_form = DummyLocationForm(instance=dummy_model)
|
||||||
|
form_html = as_crispy_field(dummy_form["location"])
|
||||||
|
else:
|
||||||
|
context.update(csrf(request))
|
||||||
|
form.helper.attrs['hx-swap-oob'] = 'true'
|
||||||
|
form_html = render_crispy_form(form)
|
||||||
|
context["valid"] = form.is_valid()
|
||||||
|
context['form'] = form_html
|
||||||
|
template_path = "customers/partials/location_response.html"
|
||||||
|
return TemplateResponse(request, template_path, context)
|
||||||
|
|
||||||
|
form = LocationForm(user=user)
|
||||||
|
context["form"] = form
|
||||||
|
template_path = "customers/partials/location_create.html"
|
||||||
|
return TemplateResponse(request, template_path, context)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
import floppyforms.__future__ as forms
|
import floppyforms.__future__ as forms
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Submit, HTML, Field, Button, Div
|
from crispy_forms.layout import Layout, Submit, HTML, Button, Div
|
||||||
from crispy_forms.bootstrap import FormActions
|
from crispy_forms.bootstrap import FormActions
|
||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
@ -80,11 +80,25 @@ class DeviceUpdateForm(forms.ModelForm):
|
||||||
self.helper.layout = Layout(
|
self.helper.layout = Layout(
|
||||||
'name',
|
'name',
|
||||||
'customer',
|
'customer',
|
||||||
'location',
|
Div(
|
||||||
'user',
|
Div('location', id="htmx-location-target"),
|
||||||
Div(Field('category'),
|
|
||||||
HTML("""
|
HTML("""
|
||||||
<a hx-get="{% url 'device_category_create' %}" hx-swap="innerHTML" hx-target="#htmx-modal-position" href="" class="add" title="Add" data-toggle="tooltip"><i class="material-icons">add</i></a>
|
<a hx-get="{% url 'htmx_create_location' %}"
|
||||||
|
hx-swap="innerHTML" hx-target="#htmx-modal-position"
|
||||||
|
href=""
|
||||||
|
class="add" title="Add" data-toggle="tooltip"><i
|
||||||
|
class="material-icons">add</i></a>
|
||||||
|
"""),
|
||||||
|
css_class="input-group"),
|
||||||
|
'user',
|
||||||
|
Div(
|
||||||
|
Div('category', id="htmx-category-target"),
|
||||||
|
HTML("""
|
||||||
|
<a hx-get="{% url 'device_category_create' %}"
|
||||||
|
hx-swap="innerHTML" hx-target="#htmx-modal-position"
|
||||||
|
href=""
|
||||||
|
class="add" title="Add" data-toggle="tooltip"><i
|
||||||
|
class="material-icons">add</i></a>
|
||||||
"""),
|
"""),
|
||||||
css_class="input-group"),
|
css_class="input-group"),
|
||||||
'serialnumber',
|
'serialnumber',
|
||||||
|
@ -92,7 +106,8 @@ class DeviceUpdateForm(forms.ModelForm):
|
||||||
FormActions(
|
FormActions(
|
||||||
Submit('save_device', 'Save'),
|
Submit('save_device', 'Save'),
|
||||||
HTML(
|
HTML(
|
||||||
"""<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-secondary">Cancel</a>""")
|
"""<a href="{{ request.META.HTTP_REFERER }}"
|
||||||
|
class="btn btn-secondary">Cancel</a>""")
|
||||||
),
|
),
|
||||||
Div(css_id='htmx-modal-position', css_class="col")
|
Div(css_id='htmx-modal-position', css_class="col")
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% if valid %}
|
{% if valid %}
|
||||||
<div hx-swap-oob="true:#div_id_category">{{ form }}</div>
|
<div hx-swap-oob="true:#htmx-category-target">{{ form }}</div>
|
||||||
<div id="htmx-modal-position" hx-swap-oob="true"></div>
|
<div id="htmx-modal-position" hx-swap-oob="true"></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import pytest
|
||||||
from mixer.backend.django import mixer
|
from mixer.backend.django import mixer
|
||||||
|
|
||||||
from core.tests import helper
|
from core.tests import helper
|
||||||
from devices.models import DeviceCategory
|
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
|
@ -36,5 +36,5 @@ urlpatterns = [
|
||||||
name='device_in_net_delete'),
|
name='device_in_net_delete'),
|
||||||
path('warranties/', views.warranties_view, name='warranties'),
|
path('warranties/', views.warranties_view, name='warranties'),
|
||||||
path('create/devices/category/', views.htmx_create_device_cagetory,
|
path('create/devices/category/', views.htmx_create_device_cagetory,
|
||||||
name='device_category_create')
|
name='device_category_create'),
|
||||||
]
|
]
|
||||||
|
|
29
flake.nix
29
flake.nix
|
@ -15,20 +15,31 @@
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
machNix = mach-nix.lib."${system}";
|
machNix = mach-nix.lib."${system}";
|
||||||
in
|
devEnvironment = machNix.mkPython {
|
||||||
{
|
|
||||||
devShell = machNix.mkPythonShell {
|
|
||||||
packagesExtra = with pkgs; [ pkgs.gnumake ];
|
|
||||||
requirements = builtins.readFile ./requirements/local.txt;
|
requirements = builtins.readFile ./requirements/local.txt;
|
||||||
_.pytest-cov.propagatedBuildInputs.mod = pySelf: self: oldVal: oldVal ++ [ pySelf.tomli ];
|
_.pytest-cov.propagatedBuildInputs.mod = pySelf: self: oldVal: oldVal ++ [ pySelf.tomli ];
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
devEnvironment
|
||||||
|
pkgs.gnumake
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
packages.venv = devEnvironment;
|
||||||
defaultPackage = (machNix.mkDockerImage {
|
defaultPackage = (machNix.mkDockerImage {
|
||||||
packagesExtra = with pkgs; [ pkgs.bash ];
|
packagesExtra = with pkgs;
|
||||||
|
[ pkgs.bash ];
|
||||||
requirements = builtins.readFile ./requirements/docker.txt;
|
requirements = builtins.readFile ./requirements/docker.txt;
|
||||||
_.pytest-cov.propagatedBuildInputs.mod = pySelf: self: oldVal: oldVal ++ [ pySelf.tomli ];
|
_.pytest-cov.propagatedBuildInputs.mod = pySelf: self: oldVal: oldVal ++ [ pySelf.tomli ];
|
||||||
}).override (oldAttrs: {
|
}).override
|
||||||
name = "network-inventory";
|
(oldAttrs: {
|
||||||
config.Cmd = [ "run.sh" ];
|
name = "network-inventory";
|
||||||
});
|
config.Cmd = [ "run.sh" ];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user