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
eval "$shellHook"

1
.gitignore vendored
View File

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

View File

@ -1,7 +1,7 @@
{
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.pythonPath": "./venv/bin/python",
"python.pythonPath": "./venv/bin/python3",
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true,

View File

@ -5,17 +5,13 @@ SHELL=/usr/bin/env bash
.PHONY: run
run: setup
( \
source venv/bin/activate; \
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
find . -name __pycache__ -o -name "*.pyc" -delete; \
python manage.py runserver; \
)
.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; \
if [ -f .second_run ]; then \
sleep 2; \
@ -46,12 +42,9 @@ setup: ./venv
fi; \
)
./venv:
python3 -m venv venv
( \
source venv/bin/activate; \
pip3 install -r requirements/local.txt; \
)
venv:
nix build .#venv -o venv
.PHONY: clean
clean:
@ -64,28 +57,22 @@ clean:
.PHONY: cleanall
cleanall: clean
docker-compose -f docker-compose-development.yml down -v --rmi local
rm -rf venv/
rm venv
.PHONY: init
init:
( \
source venv/bin/activate; \
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
python manage.py loaddata network_inventory.yaml; \
)
.PHONY: test
test:
( \
source venv/bin/activate; \
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
pytest -nauto --nomigrations --cov=. --cov-report=html; \
)
.PHONY: debug
debug:
( \
source venv/bin/activate; \
export DJANGO_SETTINGS_MODULE=network_inventory.settings.local; \
pytest --pdb --nomigrations --cov=. --cov-report=html; \
)

View File

@ -2,10 +2,11 @@ from django import forms
from django.urls import reverse_lazy
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 .models import Customer
from core import utils
from .models import Customer, DummyLocation, Location
class CustomerForm(forms.ModelForm):
@ -30,3 +31,43 @@ class CustomerForm(forms.ModelForm):
Button('cancel', 'Cancel', css_class="btn btn-secondary",
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):
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/',
views.create_customer,
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.http.response import HttpResponse
from django.shortcuts import render
from django.template.context_processors import csrf
from django.template.response import TemplateResponse
from django.urls import reverse
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 .forms import CustomerForm
from .models import Customer
from .forms import DummyLocationForm
from .forms import LocationForm
from .models import Customer, DummyLocation
from .tables import CustomersTable
@ -57,3 +63,30 @@ class CustomerDeleteView(LoginRequiredMixin, DeleteView):
def get_success_url(self):
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
import floppyforms.__future__ as forms
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 core import utils
@ -80,11 +80,25 @@ class DeviceUpdateForm(forms.ModelForm):
self.helper.layout = Layout(
'name',
'customer',
'location',
'user',
Div(Field('category'),
Div(
Div('location', id="htmx-location-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>
<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"),
'serialnumber',
@ -92,7 +106,8 @@ class DeviceUpdateForm(forms.ModelForm):
FormActions(
Submit('save_device', 'Save'),
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")
)

View File

@ -1,5 +1,5 @@
{% 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>
{% else %}
{{ form }}

View File

@ -4,7 +4,6 @@ import pytest
from mixer.backend.django import mixer
from core.tests import helper
from devices.models import DeviceCategory
pytestmark = pytest.mark.django_db

View File

@ -36,5 +36,5 @@ urlpatterns = [
name='device_in_net_delete'),
path('warranties/', views.warranties_view, name='warranties'),
path('create/devices/category/', views.htmx_create_device_cagetory,
name='device_category_create')
name='device_category_create'),
]

View File

@ -15,20 +15,31 @@
let
pkgs = nixpkgs.legacyPackages.${system};
machNix = mach-nix.lib."${system}";
in
{
devShell = machNix.mkPythonShell {
packagesExtra = with pkgs; [ pkgs.gnumake ];
devEnvironment = machNix.mkPython {
requirements = builtins.readFile ./requirements/local.txt;
_.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 {
packagesExtra = with pkgs; [ pkgs.bash ];
packagesExtra = with pkgs;
[ pkgs.bash ];
requirements = builtins.readFile ./requirements/docker.txt;
_.pytest-cov.propagatedBuildInputs.mod = pySelf: self: oldVal: oldVal ++ [ pySelf.tomli ];
}).override (oldAttrs: {
name = "network-inventory";
config.Cmd = [ "run.sh" ];
});
}).override
(oldAttrs: {
name = "network-inventory";
config.Cmd = [ "run.sh" ];
});
});
}