Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate material list to be empty before deletion #2765

Merged
merged 2 commits into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/fixtures/materialLists.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ App\Entity\MaterialList:
materialList1:
camp: '@camp1'
name: Baumarkt
materialList2:
materialList2WithNoItems:
camp: '@camp1'
name: Packliste
materialList3Manager:
Expand Down
5 changes: 5 additions & 0 deletions api/src/Entity/MaterialList.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class MaterialList extends BaseEntity implements BelongsToCampInterface, CopyFro
/**
* The items that are part of this list.
*/
#[Assert\Count(
exactly: 0,
exactMessage: 'It\'s not possible to delete a material list as long as it has items linked to it.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we already have a concept for api error messaging?

Copy link
Member

@carlobeltrame carlobeltrame May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have #2365 which works towards displaying the validation messages from the backend.
We also have the more general epic issue #1277, which should include translation of error messages.

groups: ['delete']
)]
#[ApiProperty(writable: false, example: '["/material_items/1a2b3c4d"]')]
#[Groups(['read'])]
#[ORM\OneToMany(targetEntity: MaterialItem::class, mappedBy: 'materialList')]
Expand Down
16 changes: 8 additions & 8 deletions api/tests/Api/MaterialItems/UpdateMaterialItemTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class UpdateMaterialItemTest extends ECampApiTestCase {
public function testPatchMaterialItemIsDeniedForAnonymousUser() {
$materialItem = static::$fixtures['materialItem1'];
static::createBasicClient()->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -31,7 +31,7 @@ public function testPatchMaterialItemIsDeniedForUnrelatedUser() {
$materialItem = static::$fixtures['materialItem1'];
static::createClientWithCredentials(['username' => static::$fixtures['user4unrelated']->getUsername()])
->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -50,7 +50,7 @@ public function testPatchMaterialItemIsDeniedForInactiveCollaborator() {
$materialItem = static::$fixtures['materialItem1'];
static::createClientWithCredentials(['username' => static::$fixtures['user5inactive']->getUsername()])
->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -69,7 +69,7 @@ public function testPatchMaterialItemIsDeniedForGuest() {
$materialItem = static::$fixtures['materialItem1'];
static::createClientWithCredentials(['username' => static::$fixtures['user3guest']->getUsername()])
->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -88,7 +88,7 @@ public function testPatchMaterialItemIsAllowedForMember() {
$materialItem = static::$fixtures['materialItem1'];
static::createClientWithCredentials(['username' => static::$fixtures['user2member']->getUsername()])
->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -102,7 +102,7 @@ public function testPatchMaterialItemIsAllowedForMember() {
'quantity' => 1500,
'unit' => 'g',
'_links' => [
'materialList' => ['href' => $this->getIriFor('materialList2')],
'materialList' => ['href' => $this->getIriFor('materialList2WithNoItems')],
'period' => ['href' => $this->getIriFor('period1')],
// 'materialNode' => null,
],
Expand All @@ -112,7 +112,7 @@ public function testPatchMaterialItemIsAllowedForMember() {
public function testPatchMaterialItemIsAllowedForManager() {
$materialItem = static::$fixtures['materialItem1'];
static::createClientWithCredentials()->request('PATCH', '/material_items/'.$materialItem->getId(), ['json' => [
'materialList' => $this->getIriFor('materialList2'),
'materialList' => $this->getIriFor('materialList2WithNoItems'),
'period' => $this->getIriFor('period1'),
'materialNode' => null,
'article' => 'Mehl',
Expand All @@ -125,7 +125,7 @@ public function testPatchMaterialItemIsAllowedForManager() {
'quantity' => 1500,
'unit' => 'g',
'_links' => [
'materialList' => ['href' => $this->getIriFor('materialList2')],
'materialList' => ['href' => $this->getIriFor('materialList2WithNoItems')],
'period' => ['href' => $this->getIriFor('period1')],
// 'materialNode' => null,
],
Expand Down
23 changes: 17 additions & 6 deletions api/tests/Api/MaterialLists/DeleteMaterialListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
class DeleteMaterialListTest extends ECampApiTestCase {
public function testDeleteMaterialListIsDeniedForAnonymousUser() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createBasicClient()->request('DELETE', '/material_lists/'.$materialList->getId());
$this->assertResponseStatusCodeSame(401);
$this->assertJsonContains([
Expand All @@ -20,7 +20,7 @@ public function testDeleteMaterialListIsDeniedForAnonymousUser() {
}

public function testDeleteMaterialListIsDeniedForUnrelatedUser() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createClientWithCredentials(['username' => static::$fixtures['user4unrelated']->getUsername()])
->request('DELETE', '/material_lists/'.$materialList->getId())
;
Expand All @@ -33,7 +33,7 @@ public function testDeleteMaterialListIsDeniedForUnrelatedUser() {
}

public function testDeleteMaterialListIsDeniedForInactiveCollaborator() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createClientWithCredentials(['username' => static::$fixtures['user5inactive']->getUsername()])
->request('DELETE', '/material_lists/'.$materialList->getId())
;
Expand All @@ -46,7 +46,7 @@ public function testDeleteMaterialListIsDeniedForInactiveCollaborator() {
}

public function testDeleteMaterialListIsDeniedForGuest() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createClientWithCredentials(['username' => static::$fixtures['user3guest']->getUsername()])
->request('DELETE', '/material_lists/'.$materialList->getId())
;
Expand All @@ -59,7 +59,7 @@ public function testDeleteMaterialListIsDeniedForGuest() {
}

public function testDeleteMaterialListIsAllowedForMember() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createClientWithCredentials(['username' => static::$fixtures['user2member']->getUsername()])
->request('DELETE', '/material_lists/'.$materialList->getId())
;
Expand All @@ -68,7 +68,7 @@ public function testDeleteMaterialListIsAllowedForMember() {
}

public function testDeleteMaterialListIsAllowedForManager() {
$materialList = static::$fixtures['materialList1'];
$materialList = static::$fixtures['materialList2WithNoItems'];
static::createClientWithCredentials()->request('DELETE', '/material_lists/'.$materialList->getId());
$this->assertResponseStatusCodeSame(204);
$this->assertNull($this->getEntityManager()->getRepository(MaterialList::class)->find($materialList->getId()));
Expand All @@ -84,4 +84,15 @@ public function testDeleteMaterialListFromCampPrototypeIsDeniedForUnrelatedUser(
'detail' => 'Access Denied.',
]);
}

public function testDeleteMaterialListValidatesThatListHasNoItems() {
$materialList = static::$fixtures['materialList1'];
static::createClientWithCredentials()->request('DELETE', '/material_lists/'.$materialList->getId());

$this->assertResponseStatusCodeSame(422);
$this->assertJsonContains([
'title' => 'An error occurred',
'detail' => 'materialItems: It\'s not possible to delete a material list as long as it has items linked to it.',
]);
}
}
4 changes: 2 additions & 2 deletions api/tests/Api/MaterialLists/ListMaterialListsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function testListMaterialListsIsAllowedForLoggedInUserButFiltered() {
]);
$this->assertEqualsCanonicalizing([
['href' => $this->getIriFor('materialList1')],
['href' => $this->getIriFor('materialList2')],
['href' => $this->getIriFor('materialList2WithNoItems')],
['href' => $this->getIriFor('materialList3Manager')],
['href' => $this->getIriFor('materialList1camp2')],
['href' => $this->getIriFor('materialList1campPrototype')],
Expand All @@ -56,7 +56,7 @@ public function testListMaterialListsFilteredByCampIsAllowedForCollaborator() {
]);
$this->assertEqualsCanonicalizing([
['href' => $this->getIriFor('materialList1')],
['href' => $this->getIriFor('materialList2')],
['href' => $this->getIriFor('materialList2WithNoItems')],
['href' => $this->getIriFor('materialList3Manager')],
], $response->toArray()['_links']['items']);
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/campAdmin/CampCategories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Displays all periods of a single camp and allows to edit them & create new ones
<v-item-group>
<v-list-item-action>
<dialog-entity-delete :entity="category">
{{ $tc('components.camp.CampCategories.deleteCategoryQuestion') }}
{{ $tc('components.camp.campCategories.deleteCategoryQuestion') }}
<ul>
<li>
{{ category.short }}: {{ category.name }}
Expand All @@ -70,7 +70,7 @@ Displays all periods of a single camp and allows to edit them & create new ones
<button-delete v-on="on" />
</template>
<template v-if="findActivities(category).length > 0" #error>
{{ $tc('components.camp.CampCategories.deleteCategoryNotPossibleInUse') }}
{{ $tc('components.camp.campCategories.deleteCategoryNotPossibleInUse') }}
<ul>
<li v-for="activity in findActivities(category)" :key="activity._meta.self">
{{ activity.title }}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/campAdmin/CampMaterialLists.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<v-skeleton-loader v-if="camp().materialLists()._meta.loading" type="article" />
<v-list>
<camp-material-lists-item
v-for="materialList in materialLists.items"
v-for="materialList in materialLists.allItems"
:key="materialList._meta.self"
class="px-0"
:material-list="materialList"
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/campAdmin/CampMaterialListsItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<v-card>
<v-item-group>
<v-list-item-action>
<dialog-entity-delete :entity="materialList">
<dialog-entity-delete :entity="materialList" :error-handler="deleteErrorHandler">
<template #activator="{ on }">
<button-delete v-on="on" />
</template>
Expand Down Expand Up @@ -54,6 +54,15 @@ export default {
props: {
materialList: { type: Object, required: true },
disabled: { type: Boolean, default: false }
},
methods: {
deleteErrorHandler (e) {
if (e?.response?.status === 422 /* Validation Error */) {
return this.$tc('components.camp.campMaterialListsItem.deleteError')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we say, what the reason is, instead of just a generic message?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it's because it currently doesn't work, see #2764 which he opened around the same time as this PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main motivation was, because the backend error comes without translation. So I kind of assume that all 422 errors are due to fact that the list was not empty (there's no other deletion validation at the moment).

Would certainly be cleaner to implement proper translation codes.

}

return null
}
}
}
</script>
Expand Down
21 changes: 18 additions & 3 deletions frontend/src/components/dialog/DialogBase.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script>
export default {
name: 'DialogBase',
props: {
// manual error handler to generate error message
// should return an error message as string (or null, in which case the default error message is displayed)
errorHandler: { type: Function, required: false, default: null }
},
data () {
return {
// specifies entity properties available in the form
Expand All @@ -20,6 +25,7 @@ export default {
showDialog: false,
loading: true,
error: null

}
},
watch: {
Expand Down Expand Up @@ -108,15 +114,24 @@ export default {
this.$emit('error', e)
this._events = eventHandlers

this.error = e.message
// default error message
let defaultMessage = e.message
if (e.response) {
if (e.response.status === 409 /* Conflict */) {
this.error = this.$tc('global.serverError.409')
defaultMessage = this.$tc('global.serverError.409')
}
if (e.response.status === 422 /* Validation Error */) {
this.error = e
defaultMessage = e
}
}

// overwrite with manual error message from errorHandler
let errorHandlerMessage = null
if (this.errorHandler) {
errorHandlerMessage = this.errorHandler(e)
}

this.error = errorHandlerMessage || defaultMessage
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/dialog/DialogEntityDelete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
v-model="showDialog"
:icon="icon"
:title="$tc('components.dialog.dialogEntityDelete.title')"
:error="error"
max-width="600px"
:submit-action="del"
:submit-enabled="submitEnabled && !$slots.error"
Expand Down Expand Up @@ -39,6 +40,7 @@ export default {
created () {
this.entityUri = this.entity._meta.self
}

}
</script>

Expand Down
10 changes: 9 additions & 1 deletion frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@
"campInvitations": {
"title": "Einladen"
},
"campMaterialLists": {
"createMaterialList": "Materialliste erstellen",
"title": "Materiallisten"
},
"campMaterialListsItem": {
"deleteWarning": "Möchtest du diese Materialliste wirklich löschen?",
"deleteError": "Materialliste konnte nicht gelöscht werden. Überprüfe vor dem Löschen, dass die Liste leer ist."
},
"campMembers": {
"title": "Mitglieder"
},
Expand Down Expand Up @@ -361,4 +369,4 @@
"profile": "Profil"
}
}
}
}
5 changes: 3 additions & 2 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@
"title": "Material lists"
},
"campMaterialListsItem": {
"deleteWarning": "Do you really want to delete this material list?"
"deleteWarning": "Do you really want to delete this material list?",
"deleteError": "Could not delete the material list. Check if the list is empty before deleting it."
},
"campMembers": {
"title": "Members"
Expand Down Expand Up @@ -463,4 +464,4 @@
"profile": "Profile"
}
}
}
}