Skip to content

Commit

Permalink
Merge pull request #156 from husqvarnagroup/mm/version-sorting
Browse files Browse the repository at this point in the history
Fix sorting software by version
  • Loading branch information
tsagadar authored Oct 6, 2024
2 parents 94060ae + 19ccb22 commit 75948e9
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 53 deletions.
34 changes: 22 additions & 12 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@ async def test_data(db):

# Create a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
compatibility = await Hardware.create(model="default", revision="default")

device_rollout = await Device.create(
uuid="device1",
last_state=UpdateStateEnum.REGISTERED,
update_mode=UpdateModeEnum.ROLLOUT,
hardware=compatibility,
)
hardware = await Hardware.create(model="default", revision="default")

temp_file_path = os.path.join(temp_dir, "software")
with open(temp_file_path, "w") as temp_file:
Expand All @@ -90,30 +83,47 @@ async def test_data(db):
size=800,
uri=uri,
)
await software_beta.compatibility.add(compatibility)
await software_beta.compatibility.add(hardware)

software_release = await Software.create(
version="1",
hash="dummy",
size=1200,
uri=uri,
)
await software_release.compatibility.add(compatibility)
await software_release.compatibility.add(hardware)

software_rc = await Software.create(
version="1.0.0-rc2+build77",
hash="dummy2",
size=800,
uri=uri,
)
await software_rc.compatibility.add(compatibility)
await software_rc.compatibility.add(hardware)

rollout_default = await Rollout.create(software_id=software_release.id)

device_rollout = await Device.create(
uuid="device1",
last_state=UpdateStateEnum.REGISTERED,
update_mode=UpdateModeEnum.ROLLOUT,
hardware=hardware,
)

device_assigned = await Device.create(
uuid="device2",
last_state=UpdateStateEnum.REGISTERED,
update_mode=UpdateModeEnum.ASSIGNED,
assigned_software=software_release,
hardware=hardware,
)

yield dict(
device_rollout=device_rollout,
hardware=hardware,
software_release=software_release,
software_rc=software_rc,
software_beta=software_beta,
rollout_default=rollout_default,
device_rollout=device_rollout,
device_assigned=device_assigned,
)
2 changes: 1 addition & 1 deletion goosebit/api/v1/devices/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
dependencies=[Security(validate_user_permissions, scopes=["device.read"])],
)
async def devices_get(_: Request) -> DevicesResponse:
devices = await Device.all().prefetch_related("assigned_software", "hardware")
devices = await Device.all().prefetch_related("hardware", "assigned_software", "assigned_software__compatibility")
response = DevicesResponse(devices=devices)

async def set_assigned_sw(d: DeviceSchema):
Expand Down
5 changes: 5 additions & 0 deletions goosebit/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import semver
from anyio import Path
from semver import Version
from tortoise import Model, fields
from tortoise.exceptions import ValidationError

Expand Down Expand Up @@ -156,3 +157,7 @@ def path_user(self) -> str:
return self.path.name
else:
return self.uri

@property
def parsed_version(self) -> Version:
return semver.Version.parse(self.version, optional_minor_and_patch=True)
18 changes: 3 additions & 15 deletions goosebit/ui/bff/common/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ class DataTableSearchSchema(BaseModel):
regex: bool | None = False


class DataTableColumnSchema(BaseModel):
data: str | None
name: str | None = None
searchable: bool | None = None
orderable: bool | None = None
search: DataTableSearchSchema = DataTableSearchSchema()


class DataTableOrderDirection(StrEnum):
ASCENDING = "asc"
DESCENDING = "desc"
Expand All @@ -36,21 +28,17 @@ def direction(self) -> str:

class DataTableRequest(BaseModel):
draw: int = 1
columns: list[DataTableColumnSchema] = list()
order: list[DataTableOrderSchema] = list()
start: int = 0
length: int = 0
length: int | None = None
search: DataTableSearchSchema = DataTableSearchSchema()

@computed_field # type: ignore[misc]
@property
def order_query(self) -> str | None:
try:
column = self.order[0].column
if column is None:
return None
if self.columns[column].name is None:
if len(self.order) == 0 or self.order[0].direction is None or self.order[0].name is None:
return None
return f"{self.order[0].direction}{self.columns[column].data}"
return f"{self.order[0].direction}{self.order[0].name}"
except LookupError:
return None
8 changes: 6 additions & 2 deletions goosebit/ui/bff/devices/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filte
if dt_query.search.value:
query = query.filter(search_filter(dt_query.search.value))

filtered_records = await query.count()

if dt_query.order_query:
query = query.order_by(dt_query.order_query)

filtered_records = await query.count()
devices = await query.offset(dt_query.start).limit(dt_query.length).all()
if dt_query.length is not None:
query = query.limit(dt_query.length)

devices = await query.offset(dt_query.start).all()
data = [DeviceSchema.model_validate(d) for d in devices]

return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
8 changes: 6 additions & 2 deletions goosebit/ui/bff/rollouts/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filte
if dt_query.search.value:
query = query.filter(search_filter(dt_query.search.value))

filtered_records = await query.count()

if dt_query.order_query:
query = query.order_by(dt_query.order_query)

filtered_records = await query.count()
rollouts = await query.offset(dt_query.start).limit(dt_query.length).all()
if dt_query.length is not None:
query = query.limit(dt_query.length)

rollouts = await query.offset(dt_query.start).all()
data = [RolloutSchema.model_validate(r) for r in rollouts]

return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
28 changes: 19 additions & 9 deletions goosebit/ui/bff/software/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from tortoise.queryset import QuerySet

from goosebit.schema.software import SoftwareSchema
from goosebit.ui.bff.common.requests import DataTableRequest
from goosebit.ui.bff.common.requests import DataTableOrderDirection, DataTableRequest


class BFFSoftwareResponse(BaseModel):
Expand All @@ -21,17 +21,27 @@ async def convert(cls, dt_query: DataTableRequest, query: QuerySet, search_filte
if dt_query.search.value:
query = query.filter(search_filter(dt_query.search.value))

if dt_query.order_query:
query = query.order_by(dt_query.order_query)

filtered_records = await query.count()

query = query.offset(dt_query.start)
if len(dt_query.order) > 0 and dt_query.order[0].name == "version":
# ordering cannot be delegated to database as semantic versioning sorting is not supported
software = await query.all()
reverse = dt_query.order[0].dir == DataTableOrderDirection.DESCENDING
software.sort(key=lambda s: s.parsed_version, reverse=reverse)

# in-memory paging
if dt_query.length is None:
software = software[dt_query.start :]
else:
software = software[dt_query.start : dt_query.start + dt_query.length]

else:
# if no ordering is specified, database-side paging can be used
if dt_query.length is not None:
query = query.limit(dt_query.length)

if not dt_query.length == 0:
query = query.limit(dt_query.length)
software = await query.offset(dt_query.start).all()

devices = await query.all()
data = [SoftwareSchema.model_validate(d) for d in devices]
data = [SoftwareSchema.model_validate(s) for s in software]

return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
16 changes: 10 additions & 6 deletions goosebit/ui/static/js/devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ document.addEventListener("DOMContentLoaded", async () => {
select: true,
rowId: "uuid",
ajax: {
url: "/ui/bff/devices/",
url: "/ui/bff/devices",
data: (data) => {
// biome-ignore lint/performance/noDelete: really has to be deleted
delete data.columns;
},
contentType: "application/json",
},
initComplete: () => {
Expand Down Expand Up @@ -44,14 +48,14 @@ document.addEventListener("DOMContentLoaded", async () => {
},
},
{ data: "uuid", name: "uuid", searchable: true, orderable: true },
{ data: "name", searchable: true, orderable: true },
{ data: "name", name: "name", searchable: true, orderable: true },
{ data: "hw_model" },
{ data: "hw_revision" },
{ data: "feed", searchable: true, orderable: true },
{ data: "sw_version", searchable: true, orderable: true },
{ data: "feed", name: "feed", searchable: true, orderable: true },
{ data: "sw_version", name: "sw_version", searchable: true, orderable: true },
{ data: "sw_target_version" },
{ data: "update_mode", searchable: true, orderable: true },
{ data: "last_state", searchable: true, orderable: true },
{ data: "update_mode", name: "update_mode", searchable: true, orderable: true },
{ data: "last_state", name: "last_state", searchable: true, orderable: true },
{
data: "force_update",
render: (data, type) => {
Expand Down
8 changes: 6 additions & 2 deletions goosebit/ui/static/js/rollouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ document.addEventListener("DOMContentLoaded", async () => {
rowId: "id",
ajax: {
url: "/ui/bff/rollouts",
data: (data) => {
// biome-ignore lint/performance/noDelete: really has to be deleted
delete data.columns;
},
contentType: "application/json",
},
initComplete: () => {
Expand All @@ -38,8 +42,8 @@ document.addEventListener("DOMContentLoaded", async () => {
orderable: true,
render: (data) => new Date(data).toLocaleString(),
},
{ data: "name", searchable: true, orderable: true },
{ data: "feed", searchable: true, orderable: true },
{ data: "name", name: "name", searchable: true, orderable: true },
{ data: "feed", name: "feed", searchable: true, orderable: true },
{ data: "sw_file" },
{ data: "sw_version" },
{
Expand Down
8 changes: 4 additions & 4 deletions goosebit/ui/static/js/software.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,16 @@ document.addEventListener("DOMContentLoaded", () => {
paging: true,
processing: false,
serverSide: true,
order: {
name: "version",
dir: "desc",
},
scrollCollapse: true,
scroller: true,
scrollY: "60vh",
stateSave: true,
ajax: {
url: "/ui/bff/software",
data: (data) => {
// biome-ignore lint/performance/noDelete: really has to be deleted
delete data.columns;
},
contentType: "application/json",
},
initComplete: () => {
Expand Down
Empty file.
23 changes: 23 additions & 0 deletions tests/ui/bff/devices/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest


@pytest.mark.asyncio
async def test_list_devices_uuid_asc(async_client, test_data):
response = await async_client.get(f"/ui/bff/devices?order[0][dir]=asc&order[0][name]=uuid")

assert response.status_code == 200
devices = response.json()["data"]
assert len(devices) == 2
assert devices[0]["uuid"] == test_data["device_rollout"].uuid
assert devices[1]["uuid"] == test_data["device_assigned"].uuid


@pytest.mark.asyncio
async def test_list_devices_uuid_desc(async_client, test_data):
response = await async_client.get(f"/ui/bff/devices?order[0][dir]=desc&order[0][name]=uuid")

assert response.status_code == 200
devices = response.json()["data"]
assert len(devices) == 2
assert devices[0]["uuid"] == test_data["device_assigned"].uuid
assert devices[1]["uuid"] == test_data["device_rollout"].uuid
Empty file.
25 changes: 25 additions & 0 deletions tests/ui/bff/software/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest


@pytest.mark.asyncio
async def test_list_software_version_asc(async_client, test_data):
response = await async_client.get(f"/ui/bff/software?order[0][dir]=asc&order[0][name]=version")

assert response.status_code == 200
software = response.json()["data"]
assert len(software) == 3
assert software[0]["version"] == test_data["software_beta"].version
assert software[1]["version"] == test_data["software_rc"].version
assert software[2]["version"] == test_data["software_release"].version


@pytest.mark.asyncio
async def test_list_software_version_desc(async_client, test_data):
response = await async_client.get(f"/ui/bff/software?order[0][dir]=desc&order[0][name]=version")

assert response.status_code == 200
software = response.json()["data"]
assert len(software) == 3
assert software[0]["version"] == test_data["software_release"].version
assert software[1]["version"] == test_data["software_rc"].version
assert software[2]["version"] == test_data["software_beta"].version

0 comments on commit 75948e9

Please sign in to comment.