Skip to content

Commit

Permalink
Merge pull request #1136 from syucream/feature/show-ongiong-status
Browse files Browse the repository at this point in the history
Show 'ongoing' status on entity if it has non-finished jobs
  • Loading branch information
hinashi authored Apr 22, 2024
2 parents 0d7e970 + 07db22a commit 6b94e4b
Show file tree
Hide file tree
Showing 35 changed files with 165 additions and 103 deletions.
2 changes: 1 addition & 1 deletion apiclient/typescript-fetch/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dmm-com/airone-apiclient-typescript-fetch",
"version": "0.0.15",
"version": "0.0.16",
"description": "AirOne APIv2 client in TypeScript",
"main": "src/autogenerated/index.ts",
"scripts": {
Expand Down
117 changes: 77 additions & 40 deletions entity/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,49 +205,20 @@ class Meta:
def _update_or_create(
self,
user: User,
entity_id: Optional[int],
entity: Entity,
is_create_mode: bool,
validated_data: EntityCreateData | EntityUpdateData,
) -> Entity:
is_toplevel_data = validated_data.pop("is_toplevel", None)
attrs_data = validated_data.pop("attrs")
webhooks_data = validated_data.pop("webhooks")

entity: Entity
entity, is_created_entity = Entity.objects.get_or_create(
id=entity_id, defaults={**validated_data}
)
if not is_created_entity:
# record history for specific fields on update
updated_fields: list[str] = []
if "name" in validated_data and entity.name != validated_data.get("name"):
entity.name = validated_data.get("name")
updated_fields.append("name")
if "note" in validated_data and entity.note != validated_data.get("note"):
entity.note = validated_data.get("note")
updated_fields.append("note")
if len(updated_fields) > 0:
entity.save(update_fields=updated_fields)
else:
entity.save_without_historical_record()

if is_toplevel_data is None:
is_toplevel_data = (entity.status & Entity.STATUS_TOP_LEVEL) != 0
attrs_data: list = validated_data.get("attrs", [])
webhooks_data: list = validated_data.get("webhooks", [])

# register history to create, update Entity
history: History
if is_created_entity:
entity.set_status(Entity.STATUS_CREATING)
if is_create_mode:
history = user.seth_entity_add(entity)
else:
entity.set_status(Entity.STATUS_EDITING)
history = user.seth_entity_mod(entity)

# set status parameters
if is_toplevel_data:
entity.set_status(Entity.STATUS_TOP_LEVEL)
else:
entity.del_status(Entity.STATUS_TOP_LEVEL)

# create EntityAttr instances in associated with specifying data
for attr_data in attrs_data:
# This is necessary not to pass invalid parameter to DRF DB-register
Expand Down Expand Up @@ -323,7 +294,7 @@ def _update_or_create(
webhook.save(update_fields=["is_verified", "verification_error_details"])

# unset Editing MODE
if is_created_entity:
if is_create_mode:
entity.del_status(Entity.STATUS_CREATING)
if custom_view.is_custom("after_create_entity_v2"):
custom_view.call_custom("after_create_entity_v2", None, user, entity)
Expand Down Expand Up @@ -368,13 +339,12 @@ def validate_webhooks(self, webhooks: list[WebhookCreateUpdateSerializer]):

return webhooks

def create(self, validated_data: EntityCreateData):
def create(self, validated_data: EntityCreateData) -> Entity:
user: User | None = None
if "request" in self.context:
user = self.context["request"].user
if "_user" in self.context:
user = self.context["_user"]

if user is None:
raise RequiredParameterError("user is required")

Expand All @@ -384,7 +354,29 @@ def create(self, validated_data: EntityCreateData):
"before_create_entity_v2", None, user, validated_data
)

return self._update_or_create(user, None, validated_data)
entity = Entity.objects.create(
name=validated_data.get("name"),
note=validated_data.get("note", ""),
created_user=validated_data.get("created_user"),
)

# set status parameters
if validated_data.get("is_toplevel", False):
entity.set_status(Entity.STATUS_TOP_LEVEL)
entity.set_status(Entity.STATUS_CREATING)

return entity

def create_remaining(self, entity: Entity, validated_data: EntityCreateData) -> Entity:
user: User | None = None
if "request" in self.context:
user = self.context["request"].user
if "_user" in self.context:
user = self.context["_user"]
if user is None:
raise RequiredParameterError("user is required")

return self._update_or_create(user, entity, True, validated_data)


class EntityUpdateSerializer(EntitySerializer):
Expand Down Expand Up @@ -453,7 +445,38 @@ def update(self, entity: Entity, validated_data: EntityUpdateData):
"before_update_entity_v2", None, user, validated_data, entity
)

return self._update_or_create(user, entity.id, validated_data)
# record history for specific fields on update
updated_fields: list[str] = []
if "name" in validated_data and entity.name != validated_data.get("name"):
entity.name = validated_data.get("name")
updated_fields.append("name")
if "note" in validated_data and entity.note != validated_data.get("note"):
entity.note = validated_data.get("note")
updated_fields.append("note")
if len(updated_fields) > 0:
entity.save(update_fields=updated_fields)
else:
entity.save_without_historical_record()

# set status parameters
if validated_data.pop("is_toplevel", (entity.status & Entity.STATUS_TOP_LEVEL) != 0):
entity.set_status(Entity.STATUS_TOP_LEVEL)
else:
entity.del_status(Entity.STATUS_TOP_LEVEL)
entity.set_status(Entity.STATUS_EDITING)

return entity

def update_remaining(self, entity: Entity, validated_data: EntityUpdateData) -> Entity:
user: User | None = None
if "request" in self.context:
user = self.context["request"].user
if "_user" in self.context:
user = self.context["_user"]
if user is None:
raise RequiredParameterError("user is required")

return self._update_or_create(user, entity, False, validated_data)


class EntityListSerializer(EntitySerializer):
Expand Down Expand Up @@ -493,10 +516,21 @@ class EntityDetailAttribute(TypedDict):
class EntityDetailSerializer(EntityListSerializer):
attrs = serializers.SerializerMethodField(method_name="get_attrs")
webhooks = WebhookSerializer(many=True)
has_ongoing_changes = serializers.SerializerMethodField()

class Meta:
model = Entity
fields = ["id", "name", "note", "status", "is_toplevel", "attrs", "webhooks", "is_public"]
fields = [
"id",
"name",
"note",
"status",
"is_toplevel",
"attrs",
"webhooks",
"is_public",
"has_ongoing_changes",
]

@extend_schema_field(serializers.ListField(child=EntityDetailAttributeSerializer()))
def get_attrs(self, obj: Entity) -> list[EntityDetailAttributeSerializer.EntityDetailAttribute]:
Expand Down Expand Up @@ -529,6 +563,9 @@ def get_attrs(self, obj: Entity) -> list[EntityDetailAttributeSerializer.EntityD

return attrinfo

def get_has_ongoing_changes(self, obj: Entity) -> bool:
return (obj.status & Entity.STATUS_CREATING) > 0 or (obj.status & Entity.STATUS_EDITING) > 0


class EntityHistorySerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
Expand Down
4 changes: 3 additions & 1 deletion entity/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ def create(self, request: Request, *args, **kwargs) -> Response:

serializer = EntityCreateSerializer(data=request.data, context={"_user": user})
serializer.is_valid(raise_exception=True)
entity = serializer.create(serializer.validated_data)

job = Job.new_create_entity_v2(user, None, params=request.data)
job = Job.new_create_entity_v2(user, entity, params=request.data)
job.run()

return Response(status=status.HTTP_202_ACCEPTED)
Expand All @@ -150,6 +151,7 @@ def update(self, request: Request, *args, **kwargs) -> Response:
instance=entity, data=request.data, context={"_user": user}
)
serializer.is_valid(raise_exception=True)
serializer.update(entity, serializer.validated_data)

job = Job.new_edit_entity_v2(user, entity, params=request.data)
job.run()
Expand Down
10 changes: 6 additions & 4 deletions entity/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,13 @@ def delete_entity(self, job: Job) -> JobStatus:
@app.task(bind=True)
@may_schedule_until_job_is_ready
def create_entity_v2(self, job: Job) -> JobStatus:
serializer = EntityCreateSerializer(data=json.loads(job.params), context={"_user": job.user})
if not serializer.is_valid():
entity: Entity | None = Entity.objects.filter(id=job.target.id, is_active=True).first()
if not entity:
return JobStatus.ERROR

serializer.create(serializer.validated_data)
# pass to validate the params because the entity should be already created
serializer = EntityCreateSerializer(data=json.loads(job.params), context={"_user": job.user})
serializer.create_remaining(entity, serializer.initial_data)

# update job status and save it
return JobStatus.DONE
Expand All @@ -250,7 +252,7 @@ def edit_entity_v2(self, job: Job) -> JobStatus:
if not serializer.is_valid():
return JobStatus.ERROR

serializer.update(entity, serializer.validated_data)
serializer.update_remaining(entity, serializer.validated_data)

return JobStatus.DONE

Expand Down
5 changes: 3 additions & 2 deletions entity/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_retrieve_entity(self):
"attrs": [],
"webhooks": [],
"is_public": True,
"has_ongoing_changes": False,
},
)

Expand Down Expand Up @@ -1303,7 +1304,7 @@ def side_effect(handler_name, entity_name, user, *args):

mock_call_custom.side_effect = side_effect
resp = self.client.post("/entity/api/v2/", json.dumps(params), "application/json")
self.assertEqual(resp.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(mock_call_custom.called)

def side_effect(handler_name, entity_name, user, *args):
Expand Down Expand Up @@ -2488,7 +2489,7 @@ def side_effect(handler_name, entity_name, user, *args):
resp = self.client.put(
"/entity/api/v2/%d/" % self.entity.id, json.dumps(params), "application/json"
)
self.assertEqual(resp.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(mock_call_custom.called)

def side_effect(handler_name, entity_name, user, *args):
Expand Down
18 changes: 15 additions & 3 deletions frontend/src/components/common/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box, Divider, Typography } from "@mui/material";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import { Box, Divider, Tooltip, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import React, { FC } from "react";

Expand All @@ -20,7 +21,7 @@ const Fixed = styled(Box)({
const Header = styled(Box)(({ theme }) => ({
width: theme.breakpoints.values.lg,
display: "flex",
alignItems: "baseline",
alignItems: "center",
marginBottom: "16px",
}));

Expand All @@ -40,10 +41,16 @@ const ChildrenBox = styled(Box)({
interface Props {
title: string;
description?: string;
hasOngoingProcess?: boolean;
children?: React.ReactNode;
}

export const PageHeader: FC<Props> = ({ title, description, children }) => {
export const PageHeader: FC<Props> = ({
title,
description,
hasOngoingProcess,
children,
}) => {
return (
<Frame>
<Fixed>
Expand All @@ -54,6 +61,11 @@ export const PageHeader: FC<Props> = ({ title, description, children }) => {
<Typography id="description" variant="subtitle1">
{description}
</Typography>
{hasOngoingProcess && (
<Tooltip title="未処理の変更があります。現在表示されているデータは最新でない可能性があります。">
<AutorenewIcon />
</Tooltip>
)}
<ChildrenBox>{children}</ChildrenBox>
</Header>
<Divider flexItem />
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/entry/EntryForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test("should render a component with essential props", function () {
name: "bbb",
note: "",
isToplevel: false,
hasOngoingChanges: false,
attrs: [],
webhooks: [],
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/EntityEditPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const entity: EntityDetail = {
name: "test entity",
note: "",
isToplevel: false,
hasOngoingChanges: false,
attrs: [],
webhooks: [],
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/EntityEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export const EntityEditPage: FC = () => {
entity?.value != null ? entity.value.name : "新規エンティティの作成"
}
description={entity?.value && "エンティテイティ詳細 / 編集"}
hasOngoingProcess={entity?.value?.hasOngoingChanges}
>
<SubmitButton
name="保存"
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/pages/EntryListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const EntryListPage: FC<Props> = ({ canCreateEntry = true }) => {
<Box>
<EntityBreadcrumbs entity={entity.value} />

<PageHeader title={entity.value?.name ?? ""} description="エントリ一覧">
<PageHeader
title={entity.value?.name ?? ""}
description="エントリ一覧"
hasOngoingProcess={entity.value?.hasOngoingChanges}
>
<Box width="50px">
<IconButton
id="entity_menu"
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/__snapshots__/ACLEditPage.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down Expand Up @@ -291,7 +291,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/__snapshots__/ACLHistoryPage.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down Expand Up @@ -152,7 +152,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down Expand Up @@ -439,7 +439,7 @@ exports[`should match snapshot 1`] = `
class="MuiBox-root css-t1gim7"
>
<div
class="MuiBox-root css-1pj6r2l"
class="MuiBox-root css-9rtbay"
>
<h6
class="MuiTypography-root MuiTypography-h6 css-178toxd-MuiTypography-root"
Expand Down
Loading

0 comments on commit 6b94e4b

Please sign in to comment.