Skip to content

Commit c7bb4c0

Browse files
authored
Merge pull request #168 from spendingjp/feature/issue-137-add-transition-to-mapping
Feature/issue 137 add transition to mapping
2 parents 592bb18 + dcd8e9c commit c7bb4c0

File tree

9 files changed

+390
-9
lines changed

9 files changed

+390
-9
lines changed

Diff for: backend/budgetmapper/tests/test_request.py

+154-4
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ def test_destroy_returns_method_not_allowed_error_for_member(self) -> None:
111111
self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
112112

113113
def test_get(self) -> None:
114-
self.maxDiff = None
115114
gov = factories.GovernmentFactory(name="まほろ市", slug="mahoro-city")
116115
cs = factories.ClassificationSystemFactory(name="まほろ市2101年一般会計", slug="mahoro-city-2101-ippan-kaikei")
117116
icon0 = factories.IconImageFactory()
@@ -281,7 +280,6 @@ def test_get(self) -> None:
281280
self.assertEqual(actual, expected)
282281

283282
def test_get_mapped_budget(self) -> None:
284-
self.maxDiff = None
285283
gov = factories.GovernmentFactory()
286284
cs0 = factories.ClassificationSystemFactory()
287285
bud0 = factories.BasicBudgetFactory(classification_system=cs0, government_value=gov)
@@ -777,6 +775,84 @@ def test_update_blank_slug_by_slug(self, jp_slugify):
777775
self.assertEqual(actual, expected)
778776

779777

778+
class MappedBudgetCandidateTestCase(BudgetMapperTestUserAPITestCase):
779+
def test_mapped_budget_candidates(self):
780+
cs = [factories.ClassificationSystemFactory() for _ in range(4)]
781+
gov = factories.GovernmentFactory()
782+
bb0_2100 = factories.BasicBudgetFactory(classification_system=cs[0], government_value=gov, year_value=2100)
783+
bb0_2101 = factories.BasicBudgetFactory(classification_system=cs[0], government_value=gov, year_value=2101)
784+
bb0_2102 = factories.BasicBudgetFactory(classification_system=cs[0], government_value=gov, year_value=2102)
785+
factories.BasicBudgetFactory(classification_system=cs[1], government_value=gov, year_value=2100)
786+
factories.MappedBudgetFactory(classification_system=cs[2], source_budget=bb0_2100)
787+
factories.MappedBudgetFactory(classification_system=cs[2], source_budget=bb0_2101)
788+
expected = sorted(
789+
[
790+
{
791+
"id": cs[2].id,
792+
"name": cs[2].name,
793+
"slug": cs[2].slug,
794+
"levelNames": cs[2].level_names,
795+
"createdAt": cs[2].created_at.strftime(datetime_format),
796+
"updatedAt": cs[2].updated_at.strftime(datetime_format),
797+
},
798+
{
799+
"id": cs[3].id,
800+
"name": cs[3].name,
801+
"slug": cs[3].slug,
802+
"levelNames": cs[3].level_names,
803+
"createdAt": cs[3].created_at.strftime(datetime_format),
804+
"updatedAt": cs[3].updated_at.strftime(datetime_format),
805+
},
806+
],
807+
key=lambda d: d["createdAt"],
808+
)
809+
res = self.client.get(f"/api/v1/budgets/{bb0_2100.id}/mapped-budget-candidates/", format="json")
810+
actual = sorted(res.json()["results"], key=lambda d: d["createdAt"])
811+
self.assertEqual(actual, expected)
812+
813+
expected = sorted(
814+
[
815+
{
816+
"id": cs[1].id,
817+
"name": cs[1].name,
818+
"slug": cs[1].slug,
819+
"levelNames": cs[1].level_names,
820+
"createdAt": cs[1].created_at.strftime(datetime_format),
821+
"updatedAt": cs[1].updated_at.strftime(datetime_format),
822+
},
823+
{
824+
"id": cs[2].id,
825+
"name": cs[2].name,
826+
"slug": cs[2].slug,
827+
"levelNames": cs[2].level_names,
828+
"createdAt": cs[2].created_at.strftime(datetime_format),
829+
"updatedAt": cs[2].updated_at.strftime(datetime_format),
830+
},
831+
{
832+
"id": cs[3].id,
833+
"name": cs[3].name,
834+
"slug": cs[3].slug,
835+
"levelNames": cs[3].level_names,
836+
"createdAt": cs[3].created_at.strftime(datetime_format),
837+
"updatedAt": cs[3].updated_at.strftime(datetime_format),
838+
},
839+
],
840+
key=lambda d: d["createdAt"],
841+
)
842+
res = self.client.get(f"/api/v1/budgets/{bb0_2102.id}/mapped-budget-candidates/", format="json")
843+
actual = sorted(res.json()["results"], key=lambda d: d["createdAt"])
844+
self.assertEqual(actual, expected)
845+
846+
def test_404_if_no_budget(self):
847+
res = self.client.get("/api/v1/budgets/nonsuch/mapped-budget-candidates/")
848+
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
849+
850+
def test_404_if_mapped_budget(self):
851+
mb = factories.MappedBudgetFactory()
852+
res = self.client.get(f"/api/v1/budgets/{mb.id}/mapped-budget-candidates/")
853+
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
854+
855+
780856
class ClassificationCrudTestCase(BudgetMapperTestUserAPITestCase):
781857
def test_list(self):
782858
ordering = CreatedAtPagination.ordering
@@ -815,7 +891,6 @@ def test_list(self):
815891
self.assertEqual(actual, expected)
816892

817893
def test_get(self):
818-
self.maxDiff = None
819894
cs = factories.ClassificationSystemFactory()
820895
classification_parent_a = factories.ClassificationFactory(classification_system=cs)
821896
c = factories.ClassificationFactory(classification_system=cs, parent=classification_parent_a)
@@ -1175,6 +1250,82 @@ def test_list_can_filter_by_government_and_year(self):
11751250
actual = res_json["results"]
11761251
self.assertEqual(actual, expected)
11771252

1253+
def test_list_can_filter_by_source_budget(self) -> None:
1254+
ordering = CreatedAtPagination.ordering
1255+
page_size = CreatedAtPagination.page_size
1256+
gov = factories.GovernmentFactory()
1257+
bud0 = factories.BasicBudgetFactory(government_value=gov)
1258+
bud1 = factories.BasicBudgetFactory(government_value=gov)
1259+
cs0 = factories.ClassificationSystemFactory()
1260+
cs1 = factories.ClassificationSystemFactory()
1261+
bud000 = factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs0)
1262+
bud001 = factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs0)
1263+
bud100 = factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs1)
1264+
factories.MappedBudgetFactory(source_budget=bud1, classification_system=cs0)
1265+
1266+
res = self.client.get(f"/api/v1/budgets/?sourceBudget={bud0.id}", format="json")
1267+
expected = [
1268+
{
1269+
"id": b.id,
1270+
"name": b.name,
1271+
"slug": b.slug,
1272+
"year": b.year,
1273+
"subtitle": b.subtitle,
1274+
"classificationSystem": b.classification_system.id,
1275+
"government": b.government.id,
1276+
"createdAt": b.created_at.strftime(datetime_format),
1277+
"updatedAt": b.updated_at.strftime(datetime_format),
1278+
"sourceBudget": b.source_budget.id,
1279+
}
1280+
for b in sorted(
1281+
[bud000, bud001, bud100],
1282+
key=lambda b: getattr(b, ordering.strip("-")),
1283+
reverse=ordering.startswith("-"),
1284+
)
1285+
][:page_size]
1286+
1287+
res_json = res.json()
1288+
self.assertIn("results", res_json)
1289+
actual = res_json["results"]
1290+
self.assertEqual(actual, expected)
1291+
1292+
def test_list_can_filter_by_source_budget_and_classification_system(self) -> None:
1293+
ordering = CreatedAtPagination.ordering
1294+
page_size = CreatedAtPagination.page_size
1295+
gov = factories.GovernmentFactory()
1296+
bud0 = factories.BasicBudgetFactory(government_value=gov)
1297+
bud1 = factories.BasicBudgetFactory(government_value=gov)
1298+
cs0 = factories.ClassificationSystemFactory()
1299+
cs1 = factories.ClassificationSystemFactory()
1300+
bud000 = factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs0)
1301+
bud001 = factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs0)
1302+
factories.MappedBudgetFactory(source_budget=bud0, classification_system=cs1)
1303+
factories.MappedBudgetFactory(source_budget=bud1, classification_system=cs0)
1304+
1305+
res = self.client.get(f"/api/v1/budgets/?sourceBudget={bud0.id}&classificationSystem={cs0.id}", format="json")
1306+
expected = [
1307+
{
1308+
"id": b.id,
1309+
"name": b.name,
1310+
"slug": b.slug,
1311+
"year": b.year,
1312+
"subtitle": b.subtitle,
1313+
"classificationSystem": b.classification_system.id,
1314+
"government": b.government.id,
1315+
"createdAt": b.created_at.strftime(datetime_format),
1316+
"updatedAt": b.updated_at.strftime(datetime_format),
1317+
"sourceBudget": b.source_budget.id,
1318+
}
1319+
for b in sorted(
1320+
[bud000, bud001], key=lambda b: getattr(b, ordering.strip("-")), reverse=ordering.startswith("-")
1321+
)
1322+
][:page_size]
1323+
1324+
res_json = res.json()
1325+
self.assertIn("results", res_json)
1326+
actual = res_json["results"]
1327+
self.assertEqual(actual, expected)
1328+
11781329
def test_retrieve_basic(self):
11791330
bs = [factories.BasicBudgetFactory() for i in range(100)]
11801331
b = bs[random.randint(0, 99)]
@@ -1211,7 +1362,6 @@ def test_retrieve_basic(self):
12111362
self.assertEqual(actual, expected)
12121363

12131364
def test_retrieve_mapped(self):
1214-
self.maxDiff = None
12151365
bs = [factories.MappedBudgetFactory() for i in range(100)]
12161366
b = random.choice(bs)
12171367
res = self.client.get(f"/api/v1/budgets/{b.id}/", format="json")

Diff for: backend/budgetmapper/urls.py

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
)
2121
budget_router = routers.NestedDefaultRouter(router, r"budgets", lookup="budget")
2222
budget_router.register(r"items", views.BudgetItemViewSet, basename="budget-item")
23+
budget_router.register(
24+
r"mapped-budget-candidates", views.MappedgBudgetCandidateView, basename="budget-mapping-budget-candidate"
25+
)
2326

2427
urlpatterns = [
2528
path("api/v1/", include(router.urls)),

Diff for: backend/budgetmapper/views.py

+26
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ def get_serializer_class(self):
6363
return serializers.ClassificationSystemSerializer
6464

6565

66+
class MappedgBudgetCandidateView(mixins.ListModelMixin, viewsets.GenericViewSet):
67+
serializer_class = serializers.ClassificationSystemSerializer
68+
pagination_class = CreatedAtPagination
69+
70+
def get_queryset(self):
71+
budget = get_object_or_404(models.BasicBudget.objects, pk=self.kwargs["budget_pk"])
72+
return models.ClassificationSystem.objects.exclude(
73+
pk__in=models.BasicBudget.objects.filter(
74+
government_value=budget.government_value, year_value=budget.year_value
75+
).values("classification_system")
76+
)
77+
78+
6679
class ClassificationViewSet(viewsets.ModelViewSet):
6780
def get_queryset(self):
6881
return models.Classification.objects.filter(classification_system=self.kwargs["classification_system_pk"])
@@ -77,6 +90,19 @@ def get_serializer_class(self):
7790

7891
class BudgetFilter(filters.BaseFilterBackend):
7992
def filter_queryset(self, request, queryset, view):
93+
if "sourceBudget" in request.query_params:
94+
return queryset.filter(
95+
Q(
96+
pk__in=models.MappedBudget.objects.filter(
97+
**dict(
98+
{}
99+
if "classificationSystem" not in request.query_params
100+
else {"classification_system": request.query_params["classificationSystem"]},
101+
source_budget=request.query_params.get("sourceBudget"),
102+
)
103+
).values("id")
104+
)
105+
)
80106
basic_qs = models.BasicBudget.objects
81107
if "government" in request.query_params:
82108
basic_qs = basic_qs.filter(government_value_id=request.query_params["government"])

Diff for: frontend/components/MappingBudgetCreationForm.vue

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<template>
2+
<v-dialog v-model="state.dialog" max-width="600px">
3+
<template #activator="{ on, attrs }">
4+
<v-btn v-bind="attrs" v-on="on">
5+
この予算を元にマッピングを編集/作成
6+
</v-btn>
7+
</template>
8+
<v-card>
9+
<v-tabs v-model="state.tab" centered dark icons-and-text>
10+
<v-tabs-slider></v-tabs-slider>
11+
<v-tab>編集<v-icon>mdi-pencil</v-icon></v-tab>
12+
<v-tab>新規作成<v-icon>mdi-plus</v-icon></v-tab>
13+
<v-tabs-items v-model="state.tab">
14+
<v-tab-item>
15+
<v-card-title>編集する予算を選択</v-card-title>
16+
<v-card-text>
17+
<v-container>
18+
<v-row>
19+
<v-autocomplete
20+
v-model="state.selectedMappedBudgetSlug"
21+
:items="relatedBudgets"
22+
item-text="name"
23+
item-value="slug"
24+
label="編集する予算"
25+
></v-autocomplete>
26+
</v-row>
27+
</v-container>
28+
</v-card-text>
29+
<v-card-actions>
30+
<v-spacer></v-spacer>
31+
<v-btn @click="state.dialog = false"> キャンセル </v-btn>
32+
<v-btn
33+
:to="`/budgets/${state.selectedMappedBudgetSlug}/mapping`"
34+
:disabled="state.selectedMappedBudgetSlug === null"
35+
nuxt
36+
>編集</v-btn
37+
>
38+
</v-card-actions>
39+
</v-tab-item>
40+
<v-tab-item>
41+
<v-card-title>マッピング先の予算を選択 </v-card-title>
42+
<v-card-text>
43+
<v-container>
44+
<v-row>
45+
<v-text-field
46+
v-model="state.newBudgetName"
47+
label="予算名"
48+
required
49+
></v-text-field>
50+
</v-row>
51+
<v-row>
52+
<v-autocomplete
53+
v-model="state.selectedClassificationSystemIdForNewBudget"
54+
:items="mappedBudgetCandidate"
55+
item-text="name"
56+
item-value="id"
57+
label="マッピング先の予算体系"
58+
></v-autocomplete>
59+
</v-row>
60+
</v-container>
61+
</v-card-text>
62+
<v-card-actions>
63+
<v-spacer></v-spacer>
64+
<v-btn @click="state.dialog = false"> キャンセル </v-btn>
65+
<v-btn
66+
:disabled="
67+
state.selectedClassificationSystemIdForNewBudget === null ||
68+
state.newBudgetName === ''
69+
"
70+
@click="handleCreateNewBudget"
71+
>新規作成</v-btn
72+
>
73+
</v-card-actions>
74+
</v-tab-item>
75+
<v-tab-item>tab2</v-tab-item>
76+
</v-tabs-items>
77+
</v-tabs>
78+
</v-card>
79+
</v-dialog>
80+
</template>
81+
82+
<script lang="ts">
83+
import { defineComponent, reactive } from '@vue/composition-api'
84+
import { useAsync, SetupContext, useRouter } from '@nuxtjs/composition-api'
85+
import authHeader from '@/services/auth-header'
86+
import { $axios } from '@/utils/api-accessor'
87+
import { ClassificationSystemListItem } from '@/types/classification-system-list-item'
88+
import { Budget } from '@/types/budget'
89+
90+
type State = {
91+
dialog: Boolean
92+
selectedMappedBudgetSlug: string | null
93+
newBudgetName: string
94+
selectedClassificationSystemIdForNewBudget: string | null
95+
}
96+
97+
export default defineComponent({
98+
components: {},
99+
props: {
100+
currentBudgetId: {
101+
type: String,
102+
default: '',
103+
required: true,
104+
},
105+
mappedBudgetCandidate: {
106+
type: Array as () => ClassificationSystemListItem[],
107+
default: () => [],
108+
},
109+
relatedBudgets: {
110+
type: Array as () => Budget[],
111+
default: () => [],
112+
},
113+
},
114+
setup({ currentBudgetId }, ctx: SetupContext) {
115+
const state = reactive<State>({
116+
dialog: false,
117+
selectedMappedBudgetSlug: null,
118+
newBudgetName: '',
119+
selectedClassificationSystemIdForNewBudget: null,
120+
})
121+
const router = useRouter()
122+
const handleCreateNewBudget = (): void => {
123+
useAsync(async () => {
124+
try {
125+
const res = (
126+
await $axios.post<Budget>(
127+
'/api/v1/budgets/',
128+
{
129+
sourceBudget: currentBudgetId,
130+
classificationSystem:
131+
state.selectedClassificationSystemIdForNewBudget,
132+
name: state.newBudgetName,
133+
},
134+
{
135+
headers: authHeader(),
136+
}
137+
)
138+
).data
139+
router.push(`/budgets/${res.slug}/mapping/`)
140+
} catch (error) {
141+
ctx.emit('error', '新規予算作成に失敗しました')
142+
}
143+
})
144+
}
145+
return { state, handleCreateNewBudget }
146+
},
147+
})
148+
</script>

0 commit comments

Comments
 (0)