Compare commits

...

11 Commits

Author SHA1 Message Date
cayop dd6c58267f Merge pull request '96-add-serial-number-in-public-web' (#24) from 96-add-serial-number-in-public-web into main
Reviewed-on: https://gitea.pangea.org/ereuse/devicehub-django/pulls/24
2024-11-06 16:30:19 +00:00
Cayo Puigdefabregas 6212075852 fix names 2024-11-06 17:21:48 +01:00
sergio_gimenez 2f68eff954 Merge branch 'main' into 96-add-serial-number-in-public-web 2024-11-06 08:29:24 +01:00
sergio_gimenez 26c3401f4d Do not show serial numbers if user not authenticated 2024-11-06 08:24:10 +01:00
sergio_gimenez a68403b05a better organise code 2024-11-05 10:26:21 +01:00
sergio_gimenez 51a2e74f7f [WIP] unit tests 2024-11-05 10:19:35 +01:00
sergio_gimenez e6c42a908f Show components and serials only if user is authenticated 2024-11-05 10:19:24 +01:00
sergio_gimenez 033e4df297 Render serial number in public website 2024-11-04 08:24:23 +01:00
sergio_gimenez 915d95379c Render serial number in details view 2024-11-04 08:21:19 +01:00
sergio_gimenez 8cb66104ca Add get_serial_number() method 2024-11-04 08:21:01 +01:00
sergio_gimenez 7db879e189 Add get_serial_number method 2024-11-04 08:20:37 +01:00
7 changed files with 171 additions and 114 deletions

View File

@ -29,7 +29,7 @@ class Device:
self.shortid = self.pk[:6].upper() self.shortid = self.pk[:6].upper()
self.algorithm = None self.algorithm = None
self.owner = None self.owner = None
self.annotations = [] self.annotations = []
self.hids = [] self.hids = []
self.uuids = [] self.uuids = []
self.evidences = [] self.evidences = []
@ -129,7 +129,8 @@ class Device:
return self.uuids[0] return self.uuids[0]
def get_lots(self): def get_lots(self):
self.lots = [x.lot for x in DeviceLot.objects.filter(device_id=self.id)] self.lots = [
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
@classmethod @classmethod
def get_unassigned(cls, institution, offset=0, limit=None): def get_unassigned(cls, institution, offset=0, limit=None):
@ -179,7 +180,6 @@ class Device:
count = cls.get_unassigned_count(institution) count = cls.get_unassigned_count(institution)
return devices, count return devices, count
@classmethod @classmethod
def get_unassigned_count(cls, institution): def get_unassigned_count(cls, institution):
@ -279,6 +279,12 @@ class Device:
self.get_last_evidence() self.get_last_evidence()
return self.last_evidence.get_manufacturer() return self.last_evidence.get_manufacturer()
@property
def serial_number(self):
if not self.last_evidence:
self.get_last_evidence()
return self.last_evidence.get_serial_number()
@property @property
def type(self): def type(self):
if self.last_evidence.doc['type'] == "WebSnapshot": if self.last_evidence.doc['type'] == "WebSnapshot":

View File

@ -84,7 +84,7 @@
<div class="col-lg-3 col-md-4 label"> <div class="col-lg-3 col-md-4 label">
{% trans 'Serial Number' %} {% trans 'Serial Number' %}
</div> </div>
<div class="col-lg-9 col-md-8">{{ object.last_evidence.doc.device.serialNumber|default:'' }}</div> <div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
</div> </div>
{% endif %} {% endif %}

View File

@ -120,10 +120,12 @@
<div class="col-md-4 info-label">Model</div> <div class="col-md-4 info-label">Model</div>
<div class="col-md-8 info-value">{{ object.model|default:'' }}</div> <div class="col-md-8 info-value">{{ object.model|default:'' }}</div>
</div> </div>
<div class="info-row row"> {% if user.is_authenticated %}
<div class="col-md-4 info-label">Serial Number</div> <div class="info-row row">
<div class="col-md-8 info-value">{{ object.last_evidence.doc.device.serialNumber|default:'' }}</div> <div class="col-md-4 info-label">Serial Number</div>
</div> <div class="col-md-8 info-value">{{ object.serial_number|default:'' }}</div>
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
@ -136,7 +138,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<h2 class="section-title mt-5">Components</h2> <h2 class="section-title mt-5">Components</h2>
<div class="row"> <div class="row">
{% for component in object.components %} {% for component in object.components %}
@ -147,7 +148,9 @@
<p class="card-text"> <p class="card-text">
{% for component_key, component_value in component.items %} {% for component_key, component_value in component.items %}
{% if component_key not in 'actions,type' %} {% if component_key not in 'actions,type' %}
<strong>{{ component_key }}:</strong> {{ component_value }}<br /> {% if component_key != 'serialNumber' or user.is_authenticated %}
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>
@ -157,10 +160,9 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<footer> <footer>
<p> <p>
&copy;{% now 'Y' %} eReuse. All rights reserved. &copy;{% now 'Y' %}eReuse. All rights reserved.
</p> </p>
</footer> </footer>

View File

@ -3,74 +3,47 @@ from unittest.mock import MagicMock
class TestDevice(Device): class TestDevice(Device):
"""A test subclass of Device that overrides the database-dependent methods""" def __init__(self, id):
# TODO Leaving commented bc not used, but might be useful at some point super().__init__(id=id)
# def get_annotations(self): self.shortid = id[:6].upper()
# """Return empty list instead of querying database""" self.uuids = []
# return [] self.hids = ['hid1', 'hid2']
self._setup_evidence()
# def get_uuids(self): def _setup_evidence(self):
# """Set uuids directly instead of querying""" self._evidence = MagicMock()
# self.uuids = ['uuid1', 'uuid2'] self._evidence.doc = {
'type': 'Computer',
# def get_hids(self): 'manufacturer': 'Test Manufacturer',
# """Set hids directly instead of querying""" 'model': 'Test Model',
# self.hids = ['hid1', 'hid2'] 'device': {
'serialNumber': 'SN123456',
# def get_evidences(self): 'type': 'Computer'
# """Set evidences directly instead of querying"""
# self.evidences = []
# def get_lots(self):
# """Set lots directly instead of querying"""
# self.lots = []
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'Computer',
'manufacturer': 'Test Manufacturer',
'model': 'Test Model',
'device': {
'serialNumber': 'SN123456',
'type': 'Computer'
}
} }
self._evidence.get_manufacturer = lambda: 'Test Manufacturer' }
self._evidence.get_model = lambda: 'Test Model' self._evidence.get_manufacturer = lambda: 'Test Manufacturer'
self._evidence.get_chassis = lambda: 'Computer' self._evidence.get_model = lambda: 'Test Model'
self._evidence.get_components = lambda: [ self._evidence.get_chassis = lambda: 'Computer'
{ self._evidence.get_components = lambda: [
'type': 'CPU', {
'model': 'Intel i7', 'type': 'CPU',
'manufacturer': 'Intel' 'model': 'Intel i7',
}, 'manufacturer': 'Intel',
{ 'serialNumber': 'SN12345678'
'type': 'RAM', },
'size': '8GB', {
'manufacturer': 'Kingston' 'type': 'RAM',
} 'size': '8GB',
] 'manufacturer': 'Kingston',
'serialNumber': 'SN87654321'
}
]
self.last_evidence = self._evidence self.last_evidence = self._evidence
@property
def components(self):
return self.last_evidence.get_components()
class TestWebSnapshotDevice(TestDevice): @property
"""A test subclass of Device that simulates a WebSnapshot device""" def serial_number(self):
return self.last_evidence.doc['device']['serialNumber']
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'WebSnapshot',
'kv': {
'URL': 'http://example.com',
'Title': 'Test Page',
'Timestamp': '2024-01-01'
},
'device': {
'type': 'Laptop'
}
}
self.last_evidence = self._evidence
return self._evidence

View File

@ -2,7 +2,8 @@ from django.test import TestCase, Client
from django.urls import reverse from django.urls import reverse
from unittest.mock import patch from unittest.mock import patch
from device.views import PublicDeviceWebView from device.views import PublicDeviceWebView
from device.tests.test_mock_device import TestDevice, TestWebSnapshotDevice from device.tests.test_mock_device import TestDevice
from user.models import User, Institution
class PublicDeviceWebViewTests(TestCase): class PublicDeviceWebViewTests(TestCase):
@ -11,57 +12,99 @@ class PublicDeviceWebViewTests(TestCase):
self.test_id = "test123" self.test_id = "test123"
self.test_url = reverse('device:device_web', self.test_url = reverse('device:device_web',
kwargs={'pk': self.test_id}) kwargs={'pk': self.test_id})
self.institution = Institution.objects.create(
name="Test Institution"
)
self.user = User.objects.create_user(
email='test@example.com',
institution=self.institution,
password='testpass123'
)
def test_url_resolves_correctly(self): def test_url_resolves_correctly(self):
"""Test that the URL is constructed correctly"""
url = reverse('device:device_web', kwargs={'pk': self.test_id}) url = reverse('device:device_web', kwargs={'pk': self.test_id})
self.assertEqual(url, f'/device/{self.test_id}/public/') self.assertEqual(url, f'/device/{self.test_id}/public/')
@patch('device.views.Device') @patch('device.views.Device')
def test_html_response(self, MockDevice): def test_html_response_anonymous(self, MockDevice):
test_device = TestDevice(id=self.test_id) test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device MockDevice.return_value = test_device
response = self.client.get(self.test_url) response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html') self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'Test Manufacturer') self.assertContains(response, 'Test Manufacturer')
self.assertContains(response, 'Test Model') self.assertContains(response, 'Test Model')
self.assertContains(response, 'Computer') self.assertContains(response, 'Computer')
self.assertContains(response, self.test_id) self.assertContains(response, self.test_id)
self.assertNotContains(response, 'Serial Number')
self.assertNotContains(response, 'serialNumber')
@patch('device.views.Device')
def test_html_response_authenticated(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
self.client.login(username='test@example.com', password='testpass123')
response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'Test Manufacturer')
self.assertContains(response, 'Test Model')
self.assertContains(response, 'Computer')
self.assertContains(response, self.test_id)
self.assertContains(response, 'Serial Number')
self.assertContains(response, 'Components')
self.assertContains(response, 'CPU') self.assertContains(response, 'CPU')
self.assertContains(response, 'Intel') self.assertContains(response, 'Intel')
self.assertContains(response, 'RAM') self.assertContains(response, 'RAM')
self.assertContains(response, 'Kingston') self.assertContains(response, 'Kingston')
@patch('device.views.Device') @patch('device.views.Device')
def test_json_response(self, MockDevice): def test_json_response_anonymous(self, MockDevice):
test_device = TestDevice(id=self.test_id) test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device MockDevice.return_value = test_device
response = self.client.get( response = self.client.get(
self.test_url, self.test_url,
HTTP_ACCEPT='application/json' HTTP_ACCEPT='application/json'
) )
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
json_data = response.json()
self.assertEqual(json_data['id'], self.test_id)
self.assertEqual(json_data['shortid'], self.test_id[:6].upper())
self.assertEqual(json_data['uuids'], [])
self.assertEqual(json_data['hids'], ['hid1', 'hid2'])
self.assertNotIn('serial_number', json_data)
self.assertNotIn('serialNumber', json_data)
@patch('device.views.Device')
def test_json_response_authenticated(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
self.client.login(username='test@example.com', password='testpass123')
response = self.client.get(
self.test_url,
HTTP_ACCEPT='application/json'
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json') self.assertEqual(response['Content-Type'], 'application/json')
json_data = response.json() json_data = response.json()
self.assertEqual(json_data['id'], self.test_id) self.assertEqual(json_data['id'], self.test_id)
self.assertEqual(json_data['shortid'], self.test_id[:6].upper()) self.assertEqual(json_data['shortid'], self.test_id[:6].upper())
self.assertEqual(json_data['components'], test_device.components) self.assertEqual(json_data['components'], [
{
@patch('device.views.Device') 'type': 'CPU',
def test_websnapshot_device(self, MockDevice): 'model': 'Intel i7',
test_device = TestWebSnapshotDevice(id=self.test_id) 'manufacturer': 'Intel',
MockDevice.return_value = test_device 'serialNumber': 'SN12345678'
response = self.client.get(self.test_url) },
{
self.assertEqual(response.status_code, 200) 'type': 'RAM',
self.assertTemplateUsed(response, 'device_web.html') 'size': '8GB',
'manufacturer': 'Kingston',
self.assertContains(response, 'http://example.com') 'serialNumber': 'SN87654321'
self.assertContains(response, 'Test Page') }
])
self.assertEqual(json_data['serial_number'], 'SN123456')
self.assertEqual(json_data['uuids'], [])
self.assertEqual(json_data['hids'], ['hid1', 'hid2'])

View File

@ -133,15 +133,38 @@ class PublicDeviceWebView(TemplateView):
}) })
return context return context
def get_json_response(self): @property
data = { def public_fields(self):
return {
'id': self.object.id, 'id': self.object.id,
'shortid': self.object.shortid, 'shortid': self.object.shortid,
'uuids': self.object.uuids, 'uuids': self.object.uuids,
'hids': self.object.hids, 'hids': self.object.hids,
'components': self.object.components 'components': self.remove_serial_number_from(self.object.components),
} }
return JsonResponse(data)
@property
def authenticated_fields(self):
return {
'serial_number': self.object.serial_number,
'components': self.object.components,
}
def remove_serial_number_from(self, components):
for component in components:
if 'serial_number' in component:
del component['SerialNumber']
return components
def get_device_data(self):
data = self.public_fields
if self.request.user.is_authenticated:
data.update(self.authenticated_fields)
return data
def get_json_response(self):
device_data = self.get_device_data()
return JsonResponse(device_data)
class AddAnnotationView(DashboardView, CreateView): class AddAnnotationView(DashboardView, CreateView):

View File

@ -11,7 +11,7 @@ from user.models import User, Institution
class Annotation(models.Model): class Annotation(models.Model):
class Type(models.IntegerChoices): class Type(models.IntegerChoices):
SYSTEM= 0, "System" SYSTEM = 0, "System"
USER = 1, "User" USER = 1, "User"
DOCUMENT = 2, "Document" DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer" ERASE_SERVER = 3, "EraseServer"
@ -19,14 +19,16 @@ class Annotation(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField() uuid = models.UUIDField()
owner = models.ForeignKey(Institution, on_delete=models.CASCADE) owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) user = models.ForeignKey(
type = models.SmallIntegerField(choices=Type) User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.SmallIntegerField(choices=Type)
key = models.CharField(max_length=STR_EXTEND_SIZE) key = models.CharField(max_length=STR_EXTEND_SIZE)
value = models.CharField(max_length=STR_EXTEND_SIZE) value = models.CharField(max_length=STR_EXTEND_SIZE)
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=["type", "key", "uuid"], name="unique_type_key_uuid") models.UniqueConstraint(
fields=["type", "key", "uuid"], name="unique_type_key_uuid")
] ]
@ -37,8 +39,8 @@ class Evidence:
self.doc = None self.doc = None
self.created = None self.created = None
self.dmi = None self.dmi = None
self.annotations = [] self.annotations = []
self.components = [] self.components = []
self.default = "n/a" self.default = "n/a"
self.get_owner() self.get_owner()
@ -87,7 +89,7 @@ class Evidence:
return self.components return self.components
def get_manufacturer(self): def get_manufacturer(self):
if self.doc.get("type") == "WebSnapshot": if self.is_web_snapshot():
kv = self.doc.get('kv', {}) kv = self.doc.get('kv', {})
if len(kv) < 1: if len(kv) < 1:
return "" return ""
@ -99,7 +101,7 @@ class Evidence:
return self.dmi.manufacturer().strip() return self.dmi.manufacturer().strip()
def get_model(self): def get_model(self):
if self.doc.get("type") == "WebSnapshot": if self.is_web_snapshot():
kv = self.doc.get('kv', {}) kv = self.doc.get('kv', {})
if len(kv) < 2: if len(kv) < 2:
return "" return ""
@ -122,6 +124,11 @@ class Evidence:
return k return k
return "" return ""
def get_serial_number(self):
if self.is_legacy():
return self.doc['device']['serialNumber']
return self.dmi.serial_number().strip()
@classmethod @classmethod
def get_all(cls, user): def get_all(cls, user):
return Annotation.objects.filter( return Annotation.objects.filter(
@ -136,3 +143,6 @@ class Evidence:
def is_legacy(self): def is_legacy(self):
return self.doc.get("software") != "workbench-script" return self.doc.get("software") != "workbench-script"
def is_web_snapshot(self):
return self.doc.get("type") == "WebSnapshot"