* 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:
Vinzenz Rosenkranz 2020-03-02 20:48:05 +01:00 коммит произвёл GitHub
Родитель 12f081cec7
Коммит 5817454079
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 218 добавлений и 7 удалений

Просмотреть файл

@ -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))