@ -32,7 +32,6 @@ class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
@ -41,3 +40,8 @@ class Manufacturer(TenancyObject, ManufacturerCommonFields, SaveHistory):
|
||||
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
||||
|
@ -14,6 +14,7 @@ class DeviceForm(forms.ModelForm):
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'device_model',
|
||||
'serial_number',
|
||||
'uuid',
|
||||
'device_type',
|
||||
|
39
app/itam/migrations/0009_devicemodel_device_device_model.py
Normal file
39
app/itam/migrations/0009_devicemodel_device_device_model.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.0.6 on 2024-05-23 12:05
|
||||
|
||||
import access.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0002_alter_team_organization'),
|
||||
('core', '0005_manufacturer'),
|
||||
('itam', '0008_alter_device_organization_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DeviceModel',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', access.fields.AutoSlugField()),
|
||||
('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['manufacturer', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='device_model',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel'),
|
||||
),
|
||||
]
|
@ -5,41 +5,13 @@ from access.models import TenancyObject
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
from itam.models.device_common import DeviceCommonFields, DeviceCommonFieldsName
|
||||
from itam.models.device_models import DeviceModel
|
||||
from itam.models.software import Software, SoftwareVersion
|
||||
from itam.models.operating_system import OperatingSystemVersion
|
||||
|
||||
|
||||
class DeviceCommonFields(TenancyObject, models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
|
||||
class DeviceCommonFieldsName(DeviceCommonFields):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
|
||||
|
||||
class DeviceType(DeviceCommonFieldsName):
|
||||
|
||||
@ -69,6 +41,14 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
)
|
||||
|
||||
device_model = models.ForeignKey(
|
||||
DeviceModel,
|
||||
on_delete=models.CASCADE,
|
||||
default = None,
|
||||
null = True,
|
||||
blank= True
|
||||
|
||||
)
|
||||
|
||||
device_type = models.ForeignKey(
|
||||
DeviceType,
|
||||
|
35
app/itam/models/device_common.py
Normal file
35
app/itam/models/device_common.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
|
||||
|
||||
class DeviceCommonFields(TenancyObject, models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
|
||||
class DeviceCommonFieldsName(DeviceCommonFields):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
33
app/itam/models/device_models.py
Normal file
33
app/itam/models/device_models.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
from itam.models.device_common import DeviceCommonFieldsName
|
||||
|
||||
from access.models import TenancyObject
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
from core.models.manufacturer import Manufacturer
|
||||
|
||||
|
||||
|
||||
class DeviceModel(DeviceCommonFieldsName, SaveHistory):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'manufacturer',
|
||||
'name',
|
||||
]
|
||||
|
||||
manufacturer = models.ForeignKey(
|
||||
Manufacturer,
|
||||
on_delete=models.CASCADE,
|
||||
default = None,
|
||||
null = True,
|
||||
blank= True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.manufacturer.name + ' ' + self.name
|
@ -6,7 +6,6 @@
|
||||
{% block body %}
|
||||
|
||||
<input type="button" value="New Device" onclick="window.location='{% url 'ITAM:_device_add' %}';">
|
||||
<input type="button" value="New Device Type" onclick="window.location='{% url 'ITAM:_device_type_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@ -20,8 +19,20 @@
|
||||
<tr>
|
||||
<td><a href="{% url 'ITAM:_device_view' pk=device.id %}">{{ device.name }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{% include 'icons/issue_link.html.j2' with issue=12 %}</td>
|
||||
<td>{% include 'icons/issue_link.html.j2' with issue=12 %}</td>
|
||||
<td>
|
||||
{% if device.device_model.manufacturer %}
|
||||
<a href="{% url 'Settings:_manufacturer_view' pk=device.device_model.manufacturer.id %}">{{ device.device_model.manufacturer }}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.device_model.name %}
|
||||
<a href="{% url 'Settings:_device_model_view' pk=device.device_model.id %}">{{ device.device_model.name }}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if software.is_global %}Global{% else %}{{ device.organization }}{% endif %}</td>
|
||||
<td><a href="{% url 'ITAM:_device_delete' pk=device.id %}">Delete</a></td>
|
||||
</tr>
|
||||
|
@ -36,3 +36,15 @@ def test_device_operating_system_version_only_one(user):
|
||||
"""model deviceoperatingsystem must only contain one value per device
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_device_model_same_organization(user):
|
||||
""" Can only add a device model from same organization """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_device_model_global(user):
|
||||
""" Can add a device model that is set is_global=true """
|
||||
pass
|
||||
|
38
app/itam/tests/device_model/test_device_model.py
Normal file
38
app/itam/tests/device_model/test_device_model.py
Normal file
@ -0,0 +1,38 @@
|
||||
# from django.conf import settings
|
||||
# from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
# from django.contrib.auth import get_user_model
|
||||
# from django.core.exceptions import ValidationError
|
||||
# from access.models import Organization
|
||||
|
||||
# class Test_app_structure_auth(unittest.TestCase):
|
||||
# User = get_user_model()
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_software_action(user):
|
||||
"""Ensure only software that is from the same organization or is global can be added to the device
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_not_global(user):
|
||||
"""Devices are not global items.
|
||||
|
||||
Ensure that a device can't be set to be global.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_operating_system_version_only_one(user):
|
||||
"""model deviceoperatingsystem must only contain one value per device
|
||||
"""
|
||||
pass
|
@ -0,0 +1,19 @@
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_create_has_organization():
|
||||
""" Devices must be assigned an organization """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_edit_has_organization():
|
||||
""" Devices must be assigned an organization """
|
||||
pass
|
@ -0,0 +1,87 @@
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_auth_view():
|
||||
""" User requires Permission view_history """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_create():
|
||||
""" History row must be added to history table on create """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_update():
|
||||
""" History row must be added to history table on updatej """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_delete():
|
||||
""" History row must be added to history table on delete """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_operating_system_create():
|
||||
""" History row must be added to history table on create
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_operating_system_update():
|
||||
""" History row must be added to history table on update
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_operating_system_delete():
|
||||
""" History row must be added to history table on delete
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_software_create():
|
||||
""" History row must be added to history table on create
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_software_update():
|
||||
""" History row must be added to history table on update
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_device_model_software_delete():
|
||||
""" History row must be added to history table on delete
|
||||
|
||||
Must also have populated parent_item_pk and parent_item_class columns
|
||||
"""
|
||||
pass
|
32
app/itam/tests/device_model/test_device_model_permission.py
Normal file
32
app/itam/tests/device_model/test_device_model_permission.py
Normal file
@ -0,0 +1,32 @@
|
||||
# from django.conf import settings
|
||||
# from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_auth_view(user):
|
||||
""" Check correct permission for view """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_auth_add(user):
|
||||
""" Check correct permission for add """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_auth_change(user):
|
||||
""" Check correct permission for change """
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_model_auth_delete(user):
|
||||
""" Check correct permission for delete """
|
||||
pass
|
88
app/itam/views/device_model.py
Normal file
88
app/itam/views/device_model.py
Normal file
@ -0,0 +1,88 @@
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
|
||||
from itam.models.device_models import DeviceModel
|
||||
|
||||
|
||||
|
||||
class View(OrganizationPermission, generic.UpdateView):
|
||||
model = DeviceModel
|
||||
permission_required = [
|
||||
'itam.view_device_type'
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
fields = [
|
||||
"name",
|
||||
'slug',
|
||||
'manufacturer',
|
||||
'id',
|
||||
'organization',
|
||||
'is_global',
|
||||
]
|
||||
|
||||
context_object_name = "device_model"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
context['content_title'] = self.object.name
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return f"/settings/device_model/{self.kwargs['pk']}"
|
||||
|
||||
|
||||
|
||||
class Add(PermissionRequiredMixin, OrganizationPermission, generic.CreateView):
|
||||
model = DeviceModel
|
||||
permission_required = [
|
||||
'access.add_device_type',
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
fields = [
|
||||
'name',
|
||||
'manufacturer',
|
||||
'organization',
|
||||
'is_global'
|
||||
]
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return f"/settings/device_models"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Add Device Model'
|
||||
|
||||
return context
|
||||
|
||||
class Delete(PermissionRequiredMixin, OrganizationPermission, generic.DeleteView):
|
||||
model = DeviceModel
|
||||
permission_required = [
|
||||
'access.delete_device_type',
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return f"/settings/device_models"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Delete ' + self.object.name
|
||||
|
||||
return context
|
45
app/settings/templates/settings/device_models.html.j2
Normal file
45
app/settings/templates/settings/device_models.html.j2
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
|
||||
{% block content_header_icon %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<input type="button" value="<< Back to settings" onclick="window.location='{% url 'Settings:Settings' %}';">
|
||||
<input type="button" value="New Device Model" onclick="window.location='{% url 'Settings:_device_model_add' %}';">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Organization</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{% for item in list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Settings:_device_model_view' pk=item.id %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.manufacturer }}</td>
|
||||
<td>{% if item.is_global %}Global{% else %}{{ item.organization }}{% endif %}</td>
|
||||
<td><a href="{% url 'Settings:_device_model_delete' pk=item.id %}">Delete</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1">« first</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endblock %}
|
@ -43,6 +43,7 @@ div#content article h3 {
|
||||
<h3>ITAM</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'Settings:_device_types' %}">Device Types</a></li>
|
||||
<li><a href="{% url 'Settings:_device_models' %}">Device Models</a></li>
|
||||
<li><a href="{% url 'Settings:_software_categories' %}">Software Categories</a></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
@ -1,14 +1,19 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home, device_types, manufacturer, software_categories
|
||||
from .views import home, device_models, device_types, manufacturer, software_categories
|
||||
|
||||
from itam.views import device_type, software_category
|
||||
from itam.views import device_type, device_model, software_category
|
||||
|
||||
app_name = "Settings"
|
||||
urlpatterns = [
|
||||
|
||||
path("", home.View.as_view(), name="Settings"),
|
||||
|
||||
path("device_models", device_models.Index.as_view(), name="_device_models"),
|
||||
path("device_model/<int:pk>", device_model.View.as_view(), name="_device_model_view"),
|
||||
path("device_model/add/", device_model.Add.as_view(), name="_device_model_add"),
|
||||
path("device_model/<int:pk>/delete", device_model.Delete.as_view(), name="_device_model_delete"),
|
||||
|
||||
path("device_type/", device_types.Index.as_view(), name="_device_types"),
|
||||
path("device_type/<int:pk>", device_type.View.as_view(), name="_device_type_view"),
|
||||
path("device_type/add/", device_type.Add.as_view(), name="_device_type_add"),
|
||||
|
39
app/settings/views/device_models.py
Normal file
39
app/settings/views/device_models.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.views import generic
|
||||
|
||||
|
||||
from access.mixin import OrganizationPermission
|
||||
from itam.models.device_models import DeviceModel
|
||||
|
||||
|
||||
|
||||
class Index(PermissionRequiredMixin, OrganizationPermission, generic.ListView):
|
||||
model = DeviceModel
|
||||
|
||||
permission_required = 'itam.view_devicetype'
|
||||
|
||||
template_name = 'settings/device_models.html.j2'
|
||||
|
||||
context_object_name = "list"
|
||||
|
||||
paginate_by = 10
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Device Models'
|
||||
|
||||
return context
|
Reference in New Issue
Block a user