Delete poll permanently (#823)
* add logic to irrevocably delete polls Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@uni-tuebingen.de> * add onDelete fk constraint to tables depending on polls Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@uni-tuebingen.de> * rename finally/irrevocably to permanently Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@uni-tuebingen.de> * fix poll still visible after delete and show notification after delete Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@uni-tuebingen.de> * update migration to delete orphaned entries before adding fk constraint Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@uni-tuebingen.de> * fix sql syntax for non-pgsql in migration Signed-off-by: Vinzenz Rosenkranz <vinzenz.rosenkranz@posteo.de> Co-authored-by: René Gieling <github@dartcafe.de>
This commit is contained in:
Родитель
12f081cec7
Коммит
5817454079
|
@ -56,6 +56,7 @@ return [
|
|||
['name' => 'poll#list', 'url' => '/polls/list/', 'verb' => 'GET'],
|
||||
['name' => 'poll#get', 'url' => '/polls/get/{pollId}', 'verb' => 'GET'],
|
||||
['name' => 'poll#delete', 'url' => '/polls/delete/{pollId}', 'verb' => 'GET'],
|
||||
['name' => 'poll#deletePermanently', 'url' => '/polls/delete/permanent/{pollId}', 'verb' => 'GET'],
|
||||
['name' => 'poll#write', 'url' => '/polls/write/', 'verb' => 'POST'],
|
||||
['name' => 'poll#clone', 'url' => '/polls/clone/{pollId}', 'verb' => 'get'],
|
||||
['name' => 'poll#getByToken', 'url' => '/polls/get/s/{token}', 'verb' => 'GET'],
|
||||
|
|
|
@ -223,6 +223,38 @@ class PollController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* deletePermanently
|
||||
* @NoAdminRequired
|
||||
* @param Array $poll
|
||||
* @return DataResponse
|
||||
*/
|
||||
|
||||
public function deletePermanently($pollId) {
|
||||
|
||||
try {
|
||||
// Find existing poll
|
||||
$this->poll = $this->pollMapper->find($pollId);
|
||||
$this->acl->setPollId($this->poll->getId());
|
||||
|
||||
if (!$this->acl->getAllowEdit()) {
|
||||
$this->logger->alert('Unauthorized delete attempt from user ' . $this->userId);
|
||||
return new DataResponse(['message' => 'Unauthorized write attempt.'], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!$this->poll->getDeleted()) {
|
||||
$this->logger->alert('user ' . $this->userId . ' trying to permanently delete active poll');
|
||||
return new DataResponse(['message' => 'Permanent deletion of active poll.'], Http::STATUS_CONFLICT);
|
||||
}
|
||||
|
||||
$this->pollMapper->delete($this->poll);
|
||||
return new DataResponse([], Http::STATUS_OK);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return new DataResponse($e, Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* write
|
||||
* @NoAdminRequired
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2017 René Gieling <github@dartcafe.de>
|
||||
*
|
||||
* @author René Gieling <github@dartcafe.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Polls\Migration;
|
||||
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
use OCP\Migration\IOutput;
|
||||
|
||||
/**
|
||||
* Installation class for the polls app.
|
||||
* Initial db creation
|
||||
*/
|
||||
class Version0104Date20200205104800 extends SimpleMigrationStep {
|
||||
|
||||
/** @var IDBConnection */
|
||||
protected $connection;
|
||||
|
||||
/** @var IConfig */
|
||||
protected $config;
|
||||
|
||||
/** @var array */
|
||||
protected $childTables = [
|
||||
'polls_comments',
|
||||
'polls_log',
|
||||
'polls_notif',
|
||||
'polls_options',
|
||||
'polls_share',
|
||||
'polls_votes',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param IDBConnection $connection
|
||||
* @param IConfig $config
|
||||
*/
|
||||
public function __construct(IDBConnection $connection, IConfig $config) {
|
||||
$this->connection = $connection;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null
|
||||
* @since 13.0.0
|
||||
*/
|
||||
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
|
||||
// delete all orphaned entries by selecting all rows
|
||||
// those poll_ids are not present in the polls table
|
||||
//
|
||||
// we have to use a raw query, because NOT EXISTS is not
|
||||
// part of doctrine's expression builder
|
||||
//
|
||||
// get table prefix, as we are running a raw query
|
||||
$prefix = $this->config->getSystemValue('dbtableprefix', 'oc_');
|
||||
// check for orphaned entries in all tables referencing
|
||||
// the main polls table
|
||||
foreach($this->childTables as $tbl) {
|
||||
$child = "$prefix$tbl";
|
||||
$query = "DELETE
|
||||
FROM $child
|
||||
WHERE NOT EXISTS (
|
||||
SELECT NULL
|
||||
FROM {$prefix}polls_polls polls
|
||||
WHERE polls.id = {$child}.poll_id
|
||||
)";
|
||||
$stmt = $this->connection->prepare($query);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
* @since 13.0.0
|
||||
*/
|
||||
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
|
||||
// add an on delete fk contraint to all tables referencing the main polls table
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$eventTable = $schema->getTable('polls_polls');
|
||||
foreach($this->childTables as $tbl) {
|
||||
$table = $schema->getTable($tbl);
|
||||
|
||||
$table->addForeignKeyConstraint($eventTable, ['poll_id'], ['id'], ['onDelete' => 'CASCADE']);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,8 @@
|
|||
icon="icon-folder" :to="{ name: 'list', params: {type: 'all'}}" :open="true">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in allPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
|
||||
|
@ -37,7 +38,8 @@
|
|||
icon="icon-user" :to="{ name: 'list', params: {type: 'my'}}" :open="false">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in myPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
|
||||
|
@ -45,7 +47,8 @@
|
|||
icon="icon-user" :to="{ name: 'list', params: {type: 'participated'}}" :open="false">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in participatedPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
|
||||
|
@ -53,7 +56,8 @@
|
|||
icon="icon-link" :to="{ name: 'list', params: {type: 'public'}}" :open="false">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in publicPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
|
||||
|
@ -61,7 +65,8 @@
|
|||
icon="icon-password" :to="{ name: 'list', params: {type: 'hidden'}}" :open="false">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in hiddenPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
|
||||
|
@ -69,7 +74,8 @@
|
|||
icon="icon-delete" :to="{ name: 'list', params: {type: 'deleted'}}" :open="false">
|
||||
<ul>
|
||||
<PollNavigationItems v-for="(poll) in deletedPolls" :key="poll.id" :poll="poll"
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" />
|
||||
@switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)"
|
||||
@deletePermanently="deletePermanently(poll.id)" />
|
||||
</ul>
|
||||
</AppNavigationItem>
|
||||
</ul>
|
||||
|
@ -164,6 +170,20 @@ export default {
|
|||
|
||||
},
|
||||
|
||||
deletePermanently(pollId) {
|
||||
this.$store
|
||||
.dispatch('deletePermanently', { pollId: pollId })
|
||||
.then((response) => {
|
||||
// if we permanently delete current selected poll,
|
||||
// reload deleted polls route
|
||||
if(this.$route.params.id && this.$route.params.id == pollId) {
|
||||
this.$router.push({name: 'list', params: {type: 'deleted'}})
|
||||
}
|
||||
this.refreshPolls()
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
refreshPolls() {
|
||||
if (this.$route.name !== 'publicVote') {
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
<ActionButton v-if="poll.allowEdit && poll.deleted" icon="icon-history" @click="$emit('switchDeleted')">
|
||||
{{ (poll.isAdmin) ? t('polls', 'Restore poll as admin') : t('polls', 'Restore poll') }}
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton v-if="poll.allowEdit && poll.deleted" icon="icon-delete" class="danger" @click="$emit('deletePermanently')">
|
||||
{{ (poll.isAdmin) ? t('polls', 'Delete poll permanently as admin') : t('polls', 'Delete poll permanently') }}
|
||||
</ActionButton>
|
||||
</template>
|
||||
</AppNavigationItem>
|
||||
</template>
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
<ActionButton v-if="poll.allowEdit && poll.deleted" icon="icon-history" @click="switchDeleted()">
|
||||
{{ (poll.isAdmin) ? t('polls', 'Restore poll as admin') : t('polls', 'Restore poll') }}
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton v-if="poll.allowEdit && poll.deleted" icon="icon-delete" class="danger" @click="deletePermanently()">
|
||||
{{ (poll.isAdmin) ? t('polls', 'Delete poll permanently as admin') : t('polls', 'Delete poll permanently') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
|
||||
<div v-tooltip.auto="accessType" class="thumbnail access" :class="poll.access">
|
||||
|
@ -183,6 +187,14 @@ export default {
|
|||
this.hideMenu()
|
||||
},
|
||||
|
||||
deletePermanently() {
|
||||
this.$store.dispatch('deletePermanently', { pollId: this.poll.id })
|
||||
.then((response) => {
|
||||
this.refreshPolls()
|
||||
})
|
||||
this.hideMenu()
|
||||
},
|
||||
|
||||
clonePoll() {
|
||||
this.$store.dispatch('clonePoll', { pollId: this.poll.id })
|
||||
.then((response) => {
|
||||
|
|
|
@ -71,8 +71,10 @@
|
|||
<label for="public" class="title">{{ t('polls', 'Visible to other users') }} </label>
|
||||
</div>
|
||||
|
||||
<ButtonDiv icon="icon-delete" :title="poll.deleted ? t('polls', 'Restore poll') : t('polls', 'Delete poll')"
|
||||
<ButtonDiv :icon="poll.deleted ? 'icon-history' : 'icon-delete'" :title="poll.deleted ? t('polls', 'Restore poll') : t('polls', 'Delete poll')"
|
||||
@click="switchDeleted()" />
|
||||
<ButtonDiv v-if="poll.deleted" icon="icon-delete" class="error" :title="t('polls', 'Delete poll permanently')"
|
||||
@click="deletePermanently()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -255,6 +257,16 @@ export default {
|
|||
|
||||
},
|
||||
|
||||
deletePermanently() {
|
||||
if(!this.poll.deleted) return;
|
||||
|
||||
this.$store
|
||||
.dispatch('deletePermanently', { pollId: this.poll.id })
|
||||
.then((response) => {
|
||||
this.$router.push({name: 'list', params: {type: 'deleted'}})
|
||||
})
|
||||
},
|
||||
|
||||
writePoll() {
|
||||
if (this.titleEmpty) {
|
||||
OC.Notification.showTemporary(t('polls', 'Title must not be empty!'), { type: 'success' })
|
||||
|
|
|
@ -81,6 +81,18 @@ const actions = {
|
|||
})
|
||||
},
|
||||
|
||||
deletePermanently(context, payload) {
|
||||
const endPoint = 'apps/polls/polls/delete/permanent/'
|
||||
return axios.get(OC.generateUrl(endPoint + payload.pollId))
|
||||
.then((response) => {
|
||||
OC.Notification.showTemporary(t('polls', 'Deleted poll permanently.'), { type: 'success' })
|
||||
return response
|
||||
}, (error) => {
|
||||
OC.Notification.showTemporary(t('polls', 'Error deleting poll.'), { type: 'error' })
|
||||
console.error('Error deleting poll', { error: error.response }, { payload: payload })
|
||||
})
|
||||
},
|
||||
|
||||
clonePoll(context, payload) {
|
||||
const endPoint = 'apps/polls/polls/clone/'
|
||||
return axios.get(OC.generateUrl(endPoint + payload.pollId))
|
||||
|
|
Загрузка…
Ссылка в новой задаче