diff --git a/admin/templates/lot_tag_panel.html b/admin/templates/lot_tag_panel.html index 3a70a69..48d9a95 100644 --- a/admin/templates/lot_tag_panel.html +++ b/admin/templates/lot_tag_panel.html @@ -16,8 +16,13 @@
{% if lot_tags_edit %} + + {% for tag in lot_tags_edit %} - + + + @@ -44,7 +57,8 @@ @@ -55,6 +69,11 @@
+ {% trans 'Inbox order CANNOT be changed' %} +
# + {% trans "Lot Group Name" %} {% trans "Actions" %} @@ -26,7 +31,15 @@
+ + {{ tag.name }}
+
+ {% csrf_token %} + + +
{% else %}
@@ -136,16 +158,28 @@ {% endfor %} + + + + + {% endblock %} diff --git a/admin/urls.py b/admin/urls.py index 0a719ca..23c6086 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ path("lot/add", views.AddLotTagView.as_view(), name="add_lot_tag"), path("lot/delete/", views.DeleteLotTagView.as_view(), name='delete_lot_tag'), path("lot/edit//", views.UpdateLotTagView.as_view(), name='edit_lot_tag'), + path("lot/update_order/", views.UpdateLotTagOrderView.as_view(), name='update_lot_tag_order'), ] diff --git a/admin/views.py b/admin/views.py index 3465287..b2ad5cb 100644 --- a/admin/views.py +++ b/admin/views.py @@ -206,6 +206,30 @@ class UpdateLotTagView(AdminView, UpdateView): return response +class UpdateLotTagOrderView(AdminView, TemplateView): + success_url = reverse_lazy('admin:tag_panel') + + def post(self, request, *args, **kwargs): + form = OrderingStateForm(request.POST) + + if form.is_valid(): + ordered_ids = form.cleaned_data["ordering"].split(',') + + with transaction.atomic(): + #TODO: log on institution wide logging - if implemented - + current_order = 2 + for lookup_id in ordered_ids: + lot_tag = LotTag.objects.get(id=lookup_id) + lot_tag.order = current_order + lot_tag.save() + current_order += 1 + + messages.success(self.request, _("Order changed succesfuly.")) + return redirect(self.success_url) + else: + return Http404 + + class InstitutionView(AdminView, UpdateView): template_name = "institution.html" title = _("Edit institution") diff --git a/dashboard/mixins.py b/dashboard/mixins.py index c95e808..417747c 100644 --- a/dashboard/mixins.py +++ b/dashboard/mixins.py @@ -35,7 +35,7 @@ class DashboardView(LoginRequiredMixin): context = super().get_context_data(**kwargs) lot_tags = LotTag.objects.filter( owner=self.request.user.institution, - ) + ).order_by('order') context.update({ "commit_id": settings.COMMIT, 'title': self.title, diff --git a/dashboard/templates/base.html b/dashboard/templates/base.html index abe23a0..c1f3030 100644 --- a/dashboard/templates/base.html +++ b/dashboard/templates/base.html @@ -1,4 +1,4 @@ -{% load i18n static %} +{% load i18n static startswith %} @@ -80,97 +80,122 @@ @@ -214,8 +239,14 @@ - {% block content %} - {% endblock content %} +
+ {% block actions %} + {% endblock %} +
+
+ {% block content %} + {% endblock content %} +
diff --git a/dashboard/templatetags/startswith.py b/dashboard/templatetags/startswith.py new file mode 100644 index 0000000..62d0867 --- /dev/null +++ b/dashboard/templatetags/startswith.py @@ -0,0 +1,9 @@ +#https://medium.com/@malvin.lok/add-a-custom-function-startwith-on-the-django-template-f11e1916f0d1 +from django import template + +register = template.Library() + + +@register.filter('startswith') +def startswith(value, prefixes): + return any(value.startswith(prefix) for prefix in prefixes.split(',')) diff --git a/dashboard/urls.py b/dashboard/urls.py index cf19b31..c0fdc14 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -4,8 +4,6 @@ from dashboard import views app_name = 'dashboard' urlpatterns = [ - path("", views.UnassignedDevicesView.as_view(), name="unassigned"), path("all", views.AllDevicesView.as_view(), name="all_device"), - path("/", views.LotDashboardView.as_view(), name="lot"), path("search", views.SearchView.as_view(), name="search"), ] diff --git a/dashboard/views.py b/dashboard/views.py index b4d51ee..344e3ef 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -12,16 +12,6 @@ from device.models import Device from lot.models import Lot -class UnassignedDevicesView(InventaryMixin): - template_name = "unassigned_devices.html" - section = "Unassigned" - title = _("Unassigned Devices") - breadcrumb = "Devices / Unassigned Devices" - - def get_devices(self, user, offset, limit): - return Device.get_unassigned(self.request.user.institution, offset, limit) - - class AllDevicesView(InventaryMixin): template_name = "unassigned_devices.html" section = "All" @@ -32,40 +22,6 @@ class AllDevicesView(InventaryMixin): return Device.get_all(self.request.user.institution, offset, limit) -class LotDashboardView(InventaryMixin, DetailsMixin): - template_name = "unassigned_devices.html" - section = "dashboard_lot" - title = _("Lot Devices") - breadcrumb = "Lot / Devices" - model = Lot - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - lot = context.get('object') - context.update({ - 'lot': lot, - }) - return context - - def get_devices(self, user, offset, limit): - chids = self.object.devicelot_set.all().values_list( - "device_id", flat=True - ).distinct() - - props = SystemProperty.objects.filter( - owner=self.request.user.institution, - value__in=chids - ).order_by("-created") - - chids_ordered = [] - for x in props: - if x.value not in chids_ordered: - chids_ordered.append(x.value) - - chids_page = chids_ordered[offset:offset+limit] - return [Device(id=x) for x in chids_page], len(chids_ordered) - - class SearchView(InventaryMixin): template_name = "unassigned_devices.html" section = "Search" diff --git a/device/templates/new_device.html b/device/templates/new_device.html index 46b4ce6..8ba5064 100644 --- a/device/templates/new_device.html +++ b/device/templates/new_device.html @@ -78,7 +78,7 @@ {% endfor %} diff --git a/device/templates/tabs/lots.html b/device/templates/tabs/lots.html index 59c44ac..be27912 100644 --- a/device/templates/tabs/lots.html +++ b/device/templates/tabs/lots.html @@ -9,7 +9,7 @@ {% if lot.type == tag %} diff --git a/device/views.py b/device/views.py index b2b3192..fd092d5 100644 --- a/device/views.py +++ b/device/views.py @@ -40,7 +40,7 @@ class NewDeviceView(DashboardView, FormView): template_name = "new_device.html" title = _("New Device") breadcrumb = "Device / New Device" - success_url = reverse_lazy('dashboard:unassigned') + success_url = reverse_lazy('lot:unassigned') form_class = DeviceFormSet def form_valid(self, form): @@ -57,7 +57,7 @@ class EditDeviceView(DashboardView, UpdateView): template_name = "new_device.html" title = _("Update Device") breadcrumb = "Device / Update Device" - success_url = reverse_lazy('dashboard:unassigned_devices') + success_url = reverse_lazy('lot:unassigned_devices') model = SystemProperty def get_form_kwargs(self): @@ -74,7 +74,6 @@ class EditDeviceView(DashboardView, UpdateView): class DetailsView(DashboardView, TemplateView): template_name = "details.html" - title = _("Device") breadcrumb = "Device / Details" model = SystemProperty @@ -113,6 +112,7 @@ class DetailsView(DashboardView, TemplateView): device_notes = Note.objects.filter(snapshot_uuid__in=uuids).order_by('-date') context.update({ 'object': self.object, + 'title': _("Device {}".format(self.object.shortid)), 'snapshot': last_evidence, 'lot_tags': lot_tags, 'dpps': dpps, diff --git a/evidence/templates/upload.html b/evidence/templates/upload.html index e2d7aff..9b61f1c 100644 --- a/evidence/templates/upload.html +++ b/evidence/templates/upload.html @@ -22,7 +22,7 @@ {% bootstrap_form form alert_error_type="none" error_css_class="alert alert-danger alert-icon alert-icon-border" %} diff --git a/login/views.py b/login/views.py index 885cc86..51bafbf 100644 --- a/login/views.py +++ b/login/views.py @@ -17,17 +17,17 @@ class LoginView(auth_views.LoginView): template_name = 'login.html' extra_context = { 'title': _('Login'), - 'success_url': reverse_lazy('dashboard:unassigned'), + 'success_url': reverse_lazy('lot:unassigned'), 'commit_id': settings.COMMIT, } def get(self, request, *args, **kwargs): self.extra_context['success_url'] = request.GET.get( 'next', - reverse_lazy('dashboard:unassigned') + reverse_lazy('lot:unassigned') ) if not self.request.user.is_anonymous: - return redirect(reverse_lazy('dashboard:unassigned')) + return redirect(reverse_lazy('lot:unassigned')) return super().get(request, *args, **kwargs) diff --git a/lot/migrations/0008_lot_unique_institution_and_name.py b/lot/migrations/0008_lot_unique_institution_and_name.py new file mode 100644 index 0000000..e4ba18e --- /dev/null +++ b/lot/migrations/0008_lot_unique_institution_and_name.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.6 on 2025-02-21 20:58 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("lot", "0007_lottag_inbox"), + ("user", "0002_institution_algorithm"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddConstraint( + model_name="lot", + constraint=models.UniqueConstraint( + fields=("owner", "name"), name="unique_institution_and_name" + ), + ), + ] diff --git a/lot/migrations/0009_remove_lot_unique_institution_and_name_lottag_order_and_more.py b/lot/migrations/0009_remove_lot_unique_institution_and_name_lottag_order_and_more.py new file mode 100644 index 0000000..e32412e --- /dev/null +++ b/lot/migrations/0009_remove_lot_unique_institution_and_name_lottag_order_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.6 on 2025-02-28 16:57 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("lot", "0008_lot_unique_institution_and_name"), + ("user", "0002_institution_algorithm"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="lot", + name="unique_institution_and_name", + ), + migrations.AddField( + model_name="lottag", + name="order", + field=models.PositiveIntegerField(default=0), + ), + migrations.AddConstraint( + model_name="lot", + constraint=models.UniqueConstraint( + fields=("owner", "name", "type"), name="unique_institution_and_name" + ), + ), + ] diff --git a/lot/models.py b/lot/models.py index 7ab5ae5..f25f22b 100644 --- a/lot/models.py +++ b/lot/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Max from django.utils.translation import gettext_lazy as _ from utils.constants import ( STR_SM_SIZE, @@ -16,11 +17,28 @@ class LotTag(models.Model): owner = models.ForeignKey(Institution, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) inbox = models.BooleanField(default=False) + order = models.PositiveIntegerField(default=0) def __str__(self): return self.name + def save(self, *args, **kwargs): + if not self.pk: + # set the order to be last + max_order = LotTag.objects.filter(owner=self.owner).aggregate(Max('order'))['order__max'] + self.order = (max_order or 0) + 1 + super().save(*args, **kwargs) + + + def delete(self, *args, **kwargs): + institution = self.owner + order = self.order + super().delete(*args, **kwargs) + # Adjust the order of other instances + LotTag.objects.filter(owner=institution, order__gt=order).update(order=models.F('order') - 1) + + class DeviceLot(models.Model): lot = models.ForeignKey("Lot", on_delete=models.CASCADE) device_id = models.CharField(max_length=STR_EXTEND_SIZE, blank=False, null=False) @@ -37,6 +55,12 @@ class Lot(models.Model): user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) type = models.ForeignKey(LotTag, on_delete=models.CASCADE) + class Meta: + constraints = [ + models.UniqueConstraint(fields=['owner', 'name', 'type'], name='unique_institution_and_name') + ] + + def add(self, v): if DeviceLot.objects.filter(lot=self, device_id=v).exists(): return @@ -46,6 +70,11 @@ class Lot(models.Model): for d in DeviceLot.objects.filter(lot=self, device_id=v): d.delete() + @property + def devices(self): + return DeviceLot.objects.filter(lot=self) + + class LotProperty(Property): lot = models.ForeignKey(Lot, on_delete=models.CASCADE) diff --git a/lot/templates/delete_lot.html b/lot/templates/delete_lot.html index babdb89..cc5800b 100644 --- a/lot/templates/delete_lot.html +++ b/lot/templates/delete_lot.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load django_bootstrap5 %} {% block content %}
@@ -8,13 +9,34 @@
-{% load django_bootstrap5 %}
- Are you sure than want remove the lot {{ object.name }} with {{ object.devices.count }} devices. +
+
+
{% trans "Delete Lot" %}
+
+ {% blocktranslate with name=object.name count devices=object.devices.count %} + Are you sure you want to remove the lot "{{ name }}" with {{ devices }} device? + {% plural %} + Are you sure you want to remove the lot "{{ name }}" with {{ devices }} devices? + {% endblocktranslate %} +
+ + {% if object.devices.count > 0 %} +
+ + {% trans "All associated devices will be deassigned." %} +
+ {% else %} +
+ + {% trans "No devices are associated with this lot." %} +
+ {% endif %} +
+
-
{% csrf_token %} {% if form.errors %} @@ -30,8 +52,8 @@ {% endif %} {% bootstrap_form form %}
diff --git a/lot/templates/lot_actions.html b/lot/templates/lot_actions.html new file mode 100644 index 0000000..2397ef3 --- /dev/null +++ b/lot/templates/lot_actions.html @@ -0,0 +1,10 @@ +{% load i18n %} + + + + {% trans 'Edit' %} + + + + {% trans 'Delete' %} + diff --git a/lot/templates/lots.html b/lot/templates/lots.html index 2aa866d..702c4cb 100644 --- a/lot/templates/lots.html +++ b/lot/templates/lots.html @@ -1,43 +1,72 @@ {% extends "base.html" %} -{% load i18n %} +{% load i18n paginacion %} +{% load render_table from django_tables2 %} + {% block content %} -
-
-

{{ subtitle }}

-
-
- {% if show_closed %} - - {% trans 'Hide closed lots' %} - - {% else %} - - {% trans 'Show closed lots' %} - - {% endif %} - - - {% trans 'Add new lot' %} - + + + {% trans 'Add new lot' %} + + +

{{ subtitle }}

+ +
+ +
+
+ + +
+
+ + -
- - {% for lot in lots %} - - - - - - {% endfor %} -
{{ lot.name }} - - - -
+ +{% render_table table %} +
+ {% if table.page and table.paginator.num_pages > 1 %} + {% include "django_tables2/pagination.html" %} + {% endif %}
{% endblock %} diff --git a/lot/templates/new_lot.html b/lot/templates/new_lot.html index c4c2cc8..ba8abcb 100644 --- a/lot/templates/new_lot.html +++ b/lot/templates/new_lot.html @@ -24,7 +24,8 @@ {% endif %} {% bootstrap_form form %} diff --git a/lot/urls.py b/lot/urls.py index 4ddd452..cace15d 100644 --- a/lot/urls.py +++ b/lot/urls.py @@ -4,6 +4,8 @@ from lot import views app_name = 'lot' urlpatterns = [ + path("unasigned", views.UnassignedDevicesView.as_view(), name="unassigned"), + path("/", views.LotView.as_view(), name="lot"), path("add/", views.NewLotView.as_view(), name="add"), path("delete//", views.DeleteLotView.as_view(), name="delete"), path("edit//", views.EditLotView.as_view(), name="edit"), diff --git a/lot/views.py b/lot/views.py index 341f15b..18d84e2 100644 --- a/lot/views.py +++ b/lot/views.py @@ -2,23 +2,45 @@ from django.db import IntegrityError from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, redirect, Http404 from django.contrib import messages +from dashboard.mixins import InventaryMixin, DetailsMixin from django.utils.translation import gettext_lazy as _ +from django.utils.safestring import mark_safe from django.views.generic.base import TemplateView +from django.db.models import Q from django.views.generic.edit import ( CreateView, DeleteView, UpdateView, FormView, ) +import django_tables2 as tables from dashboard.mixins import DashboardView +from evidence.models import SystemProperty +from device.models import Device from lot.models import Lot, LotTag, LotProperty from lot.forms import LotsForm -class NewLotView(DashboardView, CreateView): + +class LotSuccessUrlMixin(): + success_url = reverse_lazy('lot:unassigned') #default_url + + def get_success_url(self): + lot_group_id = LotTag.objects.only('id').get( + owner=self.object.owner, + name=self.object.type + ).id + + #null checking just in case + if not lot_group_id: + return self.success_url + + return reverse_lazy('lot:tags', args=[lot_group_id]) + + +class NewLotView(LotSuccessUrlMixin ,DashboardView, CreateView): template_name = "new_lot.html" title = _("New lot") breadcrumb = "lot / New lot" - success_url = reverse_lazy('dashboard:unassigned') model = Lot fields = ( "type", @@ -37,17 +59,24 @@ class NewLotView(DashboardView, CreateView): return form def form_valid(self, form): - form.instance.owner = self.request.user.institution - form.instance.user = self.request.user - response = super().form_valid(form) + try: + form.instance.owner = self.request.user.institution + form.instance.user = self.request.user + response = super().form_valid(form) + + messages.success(self.request, _("Lot created successfully.")) + return response + + except IntegrityError: + messages.error(self.request, _("Lot name is already defined.")) + return self.form_invalid(form) + return response - -class DeleteLotView(DashboardView, DeleteView): +class DeleteLotView(LotSuccessUrlMixin, DashboardView, DeleteView): template_name = "delete_lot.html" title = _("Delete lot") breadcrumb = "lot / Delete lot" - success_url = reverse_lazy('dashboard:unassigned') model = Lot fields = ( "type", @@ -59,14 +88,20 @@ class DeleteLotView(DashboardView, DeleteView): def form_valid(self, form): response = super().form_valid(form) + messages.warning(self.request, _("Lot '{}' was successfully deleted.").format(self.object.name)) + return response + + def form_invalid(self, form): + response = super().form_invalid(form) + messages.error(self.request, _("Error deleting the lot.")) return response -class EditLotView(DashboardView, UpdateView): +class EditLotView(LotSuccessUrlMixin, DashboardView, UpdateView): template_name = "new_lot.html" title = _("Edit lot") breadcrumb = "Lot / Edit lot" - success_url = reverse_lazy('dashboard:unassigned') + model = Lot fields = ( "type", @@ -95,12 +130,22 @@ class EditLotView(DashboardView, UpdateView): ) return form + def form_valid(self, form): + response = super().form_valid(form) + messages.warning(self.request, _("Lot '{}' was successfully edited.").format(self.object.name)) + return response + + def form_invalid(self, form): + response = super().form_invalid(form) + messages.error(self.request, _("Error editing the lot.")) + return response + class AddToLotView(DashboardView, FormView): template_name = "list_lots.html" title = _("Add to lots") breadcrumb = "lot / add to lots" - success_url = reverse_lazy('dashboard:unassigned') + success_url = reverse_lazy('lot:unassigned') form_class = LotsForm def get_context_data(self, **kwargs): @@ -137,31 +182,79 @@ class DelToLotView(AddToLotView): return response -class LotsTagsView(DashboardView, TemplateView): +class LotTable(tables.Table): + name = tables.Column(linkify=("lot:lot", {"pk": tables.A("id")}), verbose_name=_("Lot Name"), attrs={"td": {"class": "fw-bold"}}) + description = tables.Column(verbose_name=_("Description"), default=_("No description"),attrs={"td": {"class": "text-muted"}} ) + closed = tables.Column(verbose_name=_("Status")) + created = tables.DateColumn(format="Y-m-d", verbose_name=_("Created On")) + user = tables.Column(verbose_name=("Created By"), default=_("Unknown"), attrs={"td": {"class": "text-muted"}} ) + actions = tables.TemplateColumn( + template_name="lot_actions.html", + verbose_name=_(""), + attrs={"td": {"class": "text-end"}} + ) + + def render_closed(self, value): + if value: + return mark_safe('Closed') + return mark_safe('Open') + + class Meta: + model = Lot + fields = ("closed", "name", "description", "created", "user", "actions") + attrs = { + "class": "table table-hover align-middle", + "thead": {"class": "table-light"} + } + order_by = ("-created",) + + +class LotsTagsView(DashboardView, tables.SingleTableView): template_name = "lots.html" - title = _("lots") + title = _("Lot group") breadcrumb = _("lots") + " /" - success_url = reverse_lazy('dashboard:unassigned') + success_url = reverse_lazy('lot:unassigned') + model = Lot + table_class = LotTable + + def get_queryset(self): + self.pk = self.kwargs.get('pk') + self.tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk) + self.show_open = self.request.GET.get('show_open', 'false') == 'true' + self.show_closed = self.request.GET.get('show_closed', 'false') + self.search_query = self.request.GET.get('q', '').strip() + + queryset = Lot.objects.filter(owner=self.request.user.institution, type=self.tag) + + if self.show_closed == 'true': + queryset = queryset.filter(closed=True) + elif self.show_closed == 'false': + queryset = queryset.filter(closed=False) + + if self.search_query: + queryset = queryset.filter( + Q(name__icontains=self.search_query) | + Q(description__icontains=self.search_query) | + Q(code__icontains=self.search_query) + ) + + sort = self.request.GET.get('sort') + if sort: + queryset = queryset.order_by(sort) + + return queryset + def get_context_data(self, **kwargs): - self.pk = kwargs.get('pk') context = super().get_context_data(**kwargs) - tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk) - self.title += " {}".format(tag.name) - self.breadcrumb += " {}".format(tag.name) - show_closed = self.request.GET.get('show_closed', 'false') == 'true' - lots = Lot.objects.filter(owner=self.request.user.institution).filter( - type=tag, closed=show_closed - ) context.update({ - 'lots': lots, - 'title': self.title, - 'breadcrumb': self.breadcrumb, - 'show_closed': show_closed + 'title': self.title + " " + self.tag.name, + 'breadcrumb': self.breadcrumb + " " + self.tag.name, + 'show_closed': self.show_closed, + 'search_query': self.search_query, }) return context - class LotPropertiesView(DashboardView, TemplateView): template_name = "properties.html" title = _("New Lot Property") @@ -189,7 +282,7 @@ class AddLotPropertyView(DashboardView, CreateView): template_name = "new_property.html" title = _("New Lot Property") breadcrumb = "Device / New property" - success_url = reverse_lazy('dashboard:unassigned_devices') + success_url = reverse_lazy('lot:unassigned_devices') model = LotProperty fields = ("key", "value") @@ -274,3 +367,46 @@ class DeleteLotPropertyView(DashboardView, DeleteView): # Redirect back to the original URL return redirect(self.success_url) + + +class UnassignedDevicesView(InventaryMixin): + template_name = "unassigned_devices.html" + section = "Unassigned" + title = _("Unassigned Devices") + breadcrumb = "Devices / Unassigned Devices" + + def get_devices(self, user, offset, limit): + return Device.get_unassigned(self.request.user.institution, offset, limit) + +class LotView(InventaryMixin, DetailsMixin): + template_name = "unassigned_devices.html" + section = "dashboard_lot" + breadcrumb = "Lot / Devices" + model = Lot + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + lot = context.get('object') + context.update({ + 'lot': lot, + 'title': _("Lot {}".format(lot.name)) + }) + return context + + def get_devices(self, user, offset, limit): + chids = self.object.devicelot_set.all().values_list( + "device_id", flat=True + ).distinct() + + props = SystemProperty.objects.filter( + owner=self.request.user.institution, + value__in=chids + ).order_by("-created") + + chids_ordered = [] + for x in props: + if x.value not in chids_ordered: + chids_ordered.append(x.value) + + chids_page = chids_ordered[offset:offset+limit] + return [Device(id=x) for x in chids_page], len(chids_ordered)