Clean Expires

Signed-off-by: Jonas Rittershofer <jotoeri@users.noreply.github.com>
This commit is contained in:
Jonas Rittershofer 2020-04-20 13:51:48 +02:00
parent df69a7a4a3
commit 2cd445d201
12 changed files with 106 additions and 121 deletions

View file

@ -131,7 +131,7 @@ class ApiController extends Controller {
'id' => $form->getId(),
'hash' => $form->getHash(),
'title' => $form->getTitle(),
'expired' => $form->getExpired(),
'expires' => $form->getExpires(),
];
}

View file

@ -173,13 +173,8 @@ class PageController extends Controller {
return new TemplateResponse('forms', 'no.acc.tmpl', []);
}
if ($form->getExpiresTimestamp() === 0) {
$expired = false;
} else {
$expired = time() > $form->getExpiresTimestamp();
}
if ($expired) {
// If form expired, return Expired-Template
if ( ($form->getExpires() !== 0) && (time() > $form->getExpires()) ) {
return new TemplateResponse('forms', 'expired.tmpl');
}

View file

@ -41,8 +41,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setAccess(array $value)
* @method integer getCreated()
* @method void setCreated(integer $value)
* @method integer getExpiresTimestamp()
* @method void setExpiresTimestamp(integer $value)
* @method integer getExpires()
* @method void setExpires(integer $value)
* @method integer getIsAnonymous()
* @method void setIsAnonymous(bool $value)
* @method integer getSubmitOnce()
@ -56,7 +56,7 @@ class Form extends Entity {
protected $ownerId;
protected $accessJson;
protected $created;
protected $expiresTimestamp;
protected $expires;
protected $isAnonymous;
protected $submitOnce;
@ -65,7 +65,7 @@ class Form extends Entity {
*/
public function __construct() {
$this->addType('created', 'integer');
$this->addType('expiresTimestamp', 'integer');
$this->addType('expires', 'integer');
$this->addType('isAnonymous', 'bool');
$this->addType('submitOnce', 'bool');
}
@ -80,20 +80,6 @@ class Form extends Entity {
$this->setAccessJson(json_encode($access));
}
// Get virtual column expires. Set should only be done by setExpiresTimestamp().
public function getExpires(): bool {
return (bool) $this->getExpiresTimestamp();
}
// Get virtual column expired. Set should only be done by setExpiresTimestamp().
public function getExpired(): bool {
if ($this->getExpires()) {
return time() > $this->getExpiresTimestamp();
}
// else - does not expire
return false;
}
// Read full form
public function read() {
return [
@ -102,12 +88,9 @@ class Form extends Entity {
'title' => $this->getTitle(),
'description' => $this->getDescription(),
'ownerId' => $this->getOwnerId(),
'ownerDisplayName' => \OC_User::getDisplayName($this->getOwnerId()),
'created' => $this->getCreated(),
'access' => $this->getAccess(),
'expires' => $this->getExpires(),
'expired' => $this->getExpired(),
'expiresTimestamp' => $this->getExpiresTimestamp(),
'isAnonymous' => $this->getIsAnonymous(),
'submitOnce' => $this->getSubmitOnce()
];

View file

@ -105,7 +105,7 @@ class Version010200Date20200323141300 extends SimpleMigrationStep {
'notnull' => false,
'comment' => 'unix-timestamp',
]);
$table->addColumn('expires_timestamp', Type::INTEGER, [
$table->addColumn('expires', Type::INTEGER, [
'notnull' => false,
'default' => 0,
'comment' => 'unix-timestamp',
@ -237,7 +237,7 @@ class Version010200Date20200323141300 extends SimpleMigrationStep {
'owner_id' => $qb_restore->createNamedParameter($event['owner'], IQueryBuilder::PARAM_STR),
'access_json' => $qb_restore->createNamedParameter($newAccessJSON, IQueryBuilder::PARAM_STR),
'created' => $qb_restore->createNamedParameter($this->convertDateTime($event['created']), IQueryBuilder::PARAM_INT),
'expires_timestamp' => $qb_restore->createNamedParameter($this->convertDateTime($event['expire']), IQueryBuilder::PARAM_INT),
'expires' => $qb_restore->createNamedParameter($this->convertDateTime($event['expire']), IQueryBuilder::PARAM_INT),
'is_anonymous' => $qb_restore->createNamedParameter($event['is_anonymous'], IQueryBuilder::PARAM_BOOL),
'submit_once' => $qb_restore->createNamedParameter($event['unique'], IQueryBuilder::PARAM_BOOL)
]);

View file

@ -66,6 +66,7 @@ import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
import AppNavigationIconBullet from '@nextcloud/vue/dist/Components/AppNavigationIconBullet'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import axios from '@nextcloud/axios'
import moment from '@nextcloud/moment'
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
@ -106,7 +107,7 @@ export default {
*/
bulletColor() {
const style = getComputedStyle(document.body)
if (this.form.expired) {
if (this.form.expires && moment().unix() > this.form.expires) {
return style.getPropertyValue('--color-error').slice(-6)
}
return style.getPropertyValue('--color-success').slice(-6)

View file

@ -36,14 +36,14 @@
<input v-if="edit"
:placeholder="t('forms', 'Enter a title for this question')"
:aria-label="t('forms', 'The title of the question number {index}', {index})"
:value="title"
:value="text"
class="question__header-title"
type="text"
minlength="1"
maxlength="256"
required
@input="onInput">
<h3 v-else class="question__header-title" v-text="title" />
<h3 v-else class="question__header-title" v-text="text" />
<Actions class="question__header-menu" :force-menu="true">
<ActionButton icon="icon-delete" @click="onDelete">
{{ t('forms', 'Delete question') }}
@ -82,7 +82,7 @@ export default {
type: Number,
required: true,
},
title: {
text: {
type: String,
required: true,
},
@ -94,7 +94,7 @@ export default {
methods: {
onInput({ target }) {
this.$emit('update:title', target.value)
this.$emit('update:text', target.value)
},
/**
@ -115,7 +115,7 @@ export default {
* Delete this question
*/
onDelete() {
this.$emit('delete', this.id)
this.$emit('delete')
},
},
}

View file

@ -23,13 +23,14 @@
<template>
<Question
v-bind.sync="$attrs"
:title="title"
:text="text"
:edit.sync="edit"
@update:title="onTitleChange">
@delete="onDelete"
@update:text="onTitleChange">
<div class="question__content">
<!-- TODO: properly choose max length -->
<textarea ref="textarea"
:aria-label="t('forms', 'A long answer for the question “{title}”', { title })"
:aria-label="t('forms', 'A long answer for the question “{text}”', { text })"
:placeholder="t('forms', 'Long answer text')"
:readonly="edit"
:value="values[0]"

View file

@ -23,9 +23,10 @@
<template>
<Question
v-bind.sync="$attrs"
:title="title"
:text="text"
:edit.sync="edit"
@update:title="onTitleChange">
@delete="onDelete"
@update:text="onTitleChange">
<ul class="question__content" :role="isUnique ? 'radiogroup' : ''">
<template v-for="(answer, index) in options">
<li :key="index" class="question__item">
@ -45,7 +46,7 @@
<label v-if="!edit"
ref="label"
:for="`${id}-answer-${index}`"
class="question__label">{{ answer }}</label>
class="question__label">{{ answer.text }}</label>
<!-- Answer text input edit -->
<!-- TODO: properly choose max length -->
@ -53,7 +54,7 @@
ref="input"
:aria-label="t('forms', 'An answer for the {index} option', { index: index + 1 })"
:placeholder="t('forms', 'Answer number {index}', { index: index + 1 })"
:value="answer"
:value="answer.text"
class="question__input"
maxlength="256"
minlength="1"

View file

@ -23,13 +23,14 @@
<template>
<Question
v-bind.sync="$attrs"
:title="title"
:text="text"
:edit.sync="edit"
@update:title="onTitleChange">
@delete="onDelete"
@update:text="onTitleChange">
<div class="question__content">
<!-- TODO: properly choose max length -->
<input ref="input"
:aria-label="t('forms', 'A short answer for the question “{title}”', { title })"
:aria-label="t('forms', 'A short answer for the question “{text}”', { text })"
:placeholder="t('forms', 'Short answer text')"
:readonly="edit"
:value="values[0]"

View file

@ -26,7 +26,7 @@ export default {
/**
* The question title
*/
title: {
text: {
type: String,
required: true,
},
@ -73,10 +73,10 @@ export default {
/**
* Forward the title change to the parent
*
* @param {string} title the title
* @param {string} text the title
*/
onTitleChange(title) {
this.$emit('update:title', title)
onTitleChange(text) {
this.$emit('update:text', text)
},
/**
@ -87,5 +87,12 @@ export default {
onValuesChange(values) {
this.$emit('update:values', values)
},
/**
* Delete this question
*/
onDelete() {
this.$emit('delete')
},
},
}

View file

@ -100,34 +100,19 @@
</EmptyContent>
<!-- Questions list -->
<!-- <transitionGroup
v-else
id="form-list"
name="list"
tag="ul"
class="form-table">
<QuizFormItem
v-for="(question, index) in form.questions"
:key="question.id"
:question="question"
:type="question.type"
@addOption="addOption"
@deleteOption="deleteOption"
@deleteQuestion="deleteQuestion(question, index)" />
</transitionGroup> -->
<form @submit.prevent="onSubmit">
<Draggable v-model="questions"
<Draggable v-model="form.questions"
:animation="200"
tag="ul"
@start="dragging = true"
@end="dragging = false">
<Questions :is="answerTypes[question.type].component"
v-for="(question, index) in questions"
v-for="(question, index) in form.questions"
:key="question.id"
:model="answerTypes[question.type]"
:index="index + 1"
v-bind.sync="question"
@delete="deleteQuestion" />
@delete="deleteQuestion(question)" />
</Draggable>
</form>
</section>
@ -183,30 +168,6 @@ export default {
loadingForm: true,
loadingQuestions: false,
errorForm: false,
questions: [
{
id: 1,
type: 'short',
title: 'How old are you ?',
},
{
id: 2,
type: 'long',
title: 'Your latest best memory ?',
},
{
id: 3,
type: 'multiple',
title: 'Choose an answer ?',
options: ['Answer 1', 'Answer 2', 'Answer 3', 'Answer 4'],
},
{
id: 4,
type: 'multiple_unique',
title: 'Choose an answer ?',
options: ['Answer 1', 'Answer 2', 'Answer 3', 'Answer 4'],
},
],
dragging: false,
}
},
@ -227,8 +188,12 @@ export default {
watch: {
form: {
deep: true,
handler: function() {
this.debounceWriteForm()
handler: function(newForm, oldForm) {
if (newForm.hash === oldForm.hash) {
this.debounceSaveForm()
} else {
this.fetchFullForm(newForm.id)
}
},
},
},
@ -262,7 +227,7 @@ export default {
}
},
onSubmit() {
onSubmit(e) {
this.saveForm()
},
@ -298,10 +263,26 @@ export default {
}
},
async deleteQuestion(question, index) {
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id: question.id }))
// TODO catch Error
this.form.questions.splice(index, 1)
/**
* Delete a question
* @param {Object} question the question to delete
* @param {number} question.id the question id to delete
*/
async deleteQuestion(question) {
console.info(question)
const id = question.id
this.loadingQuestions = true
try {
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id }))
const index = this.form.questions.findIndex(search => search.id === id)
this.form.questions.splice(index, 1)
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error while removing the question'))
} finally {
this.loadingQuestions = false
}
},
async addOption(item, question) {
@ -320,6 +301,9 @@ export default {
question.options.splice(index, 1)
},
/**
* Auto adjust the description height based on lines number
*/
autoSizeDescription() {
const textarea = this.$refs.description
if (textarea) {
@ -328,14 +312,17 @@ export default {
}
},
/**
* Forms saving handlers
*/
debounceSaveForm: debounce(function() {
this.saveForm()
}, 200),
async saveForm() {
try {
await axios.post(OC.generateUrl('apps/forms/write/form'), this.form)
showSuccess(t('forms', '%n successfully saved', 1, this.form.title), { duration: 3000 })
// TODO: add loading status feedback ?
await axios.post(OC.generateUrl('/apps/forms/write/form'), this.form)
} catch (error) {
showError(t('forms', 'Error on saving form, see console'))
console.error(error)
@ -365,7 +352,10 @@ export default {
// conflicting with the click outside directive
setTimeout(() => {
this.questionMenuOpened = true
}, 100)
this.$nextTick(() => {
this.$refs.questionMenu.focusFirstAction()
})
}, 10)
},
/**
@ -437,7 +427,8 @@ export default {
.question-toolbar {
position: sticky;
z-index: 50;
// Above other menus
z-index: 55;
top: var(--header-height);
display: flex;
align-items: center;
@ -446,6 +437,7 @@ export default {
height: var(--top-bar-height);
// make sure this doesn't take any space and appear floating
margin-top: -44px;
.icon-add-white {
opacity: 1;
border-radius: 50%;

View file

@ -46,7 +46,7 @@
</label>
<input id="expires"
v-model="form.expires"
v-model="formExpires"
type="checkbox"
class="checkbox">
@ -54,9 +54,9 @@
{{ t('forms', 'Expires') }}
</label>
<DatetimePicker v-show="form.expires"
<DatetimePicker v-show="formExpires"
id="expiresDatetimePicker"
v-model="form.expiresTimestamp"
v-model="form.expires"
v-bind="expirationDatePicker" />
</div>
@ -131,6 +131,7 @@ export default {
locale: '',
longDateFormat: '',
dateTimeFormat: '',
formExpires: false,
}
},
@ -151,21 +152,17 @@ export default {
},
}
},
},
optionDatePicker() {
return {
editable: false,
minuteStep: 1,
type: 'datetime',
format: moment.localeData().longDateFormat('L') + ' ' + moment.localeData().longDateFormat('LT'),
lang: this.lang.split('-')[0],
placeholder: t('forms', 'Click to add a date'),
timePickerOptions: {
start: '00:00',
step: '00:30',
end: '23:30',
},
}
watch: {
formExpires: {
handler: function() {
if (!this.formExpires) {
this.form.expires = 0
} else {
this.form.expires = moment().unix() + 3600 // Expires in one hour.
}
},
},
},
@ -185,6 +182,13 @@ export default {
this.longDateFormat = moment.localeData().longDateFormat('L')
this.dateTimeFormat = moment.localeData().longDateFormat('L') + ' ' + moment.localeData().longDateFormat('LT')
// Compute current formExpires for checkbox
if (this.form.expires) {
this.formExpires = true
} else {
this.formExpires = false
}
// Watch for Sidebar toggle
subscribe('toggleSidebar', this.onToggle)
},