Merge pull request #65 from Nebucatnetzer/dev

2022-03-23
This commit is contained in:
Andreas Zweili 2022-03-23 21:28:00 +01:00 committed by GitHub
commit ccafdf1c99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 209 additions and 40 deletions

1
.envrc
View File

@ -1 +1,2 @@
use flake use flake
eval "$shellHook"

1
.gitignore vendored
View File

@ -144,6 +144,7 @@ celerybeat-schedule
# virtualenv # virtualenv
.venv .venv
venv/ venv/
venv
ENV/ ENV/
# Spyder project settings # Spyder project settings

View File

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

View File

@ -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; \
) )

View File

@ -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',
)

View File

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

View File

@ -0,0 +1 @@
{% include "core/partials/modal.html" with form=form modal_title="Add Location"%}

View File

@ -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 %}

View 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.")

View 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."))

View File

@ -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')
] ]

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
] ]

View File

@ -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" ];
});
}); });
} }