Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
dc4f6ca9e1
312 changed files with 62376 additions and 23704 deletions
|
@ -4,7 +4,6 @@
|
|||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
|
|
@ -5,5 +5,11 @@
|
|||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false
|
||||
"disableAnalytics": false,
|
||||
|
||||
/**
|
||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||
*/
|
||||
"isTypeScriptProject": true
|
||||
}
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/api
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/package.json.ember-try
|
||||
|
|
75
.eslintrc.js
75
.eslintrc.js
|
@ -1,55 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: 'babel-eslint',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true
|
||||
}
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
plugins: [
|
||||
'ember'
|
||||
],
|
||||
plugins: ['ember', '@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:ember/recommended'
|
||||
'plugin:ember/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
rules: {
|
||||
// Croodle is not compliant with some of the recommended rules yet.
|
||||
// We should refactor the code step by step and enable them as soon
|
||||
// as the code is compliant.
|
||||
'ember/classic-decorator-no-classic-methods': 'warn',
|
||||
'ember/no-controller-access-in-routes': 'warn',
|
||||
'ember/no-observers': 'warn',
|
||||
'ember/no-jquery': 'error',
|
||||
'no-prototype-builtins': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
// ts files
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {},
|
||||
},
|
||||
// node files
|
||||
{
|
||||
files: [
|
||||
'.eslintrc.js',
|
||||
'.template-lintrc.js',
|
||||
'ember-cli-build.js',
|
||||
'testem.js',
|
||||
'blueprints/*/index.js',
|
||||
'config/**/*.js',
|
||||
'lib/*/index.js',
|
||||
'server/**/*.js'
|
||||
'./.eslintrc.js',
|
||||
'./.prettierrc.js',
|
||||
'./.stylelintrc.js',
|
||||
'./.template-lintrc.js',
|
||||
'./ember-cli-build.js',
|
||||
'./testem.js',
|
||||
'./blueprints/*/index.js',
|
||||
'./config/**/*.js',
|
||||
'./lib/*/index.js',
|
||||
'./server/**/*.js',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'script'
|
||||
},
|
||||
env: {
|
||||
browser: false,
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
plugins: ['node'],
|
||||
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
|
||||
// add your custom rules and overrides for node files here
|
||||
|
||||
// this can be removed once the following is fixed
|
||||
// https://github.com/mysticatea/eslint-plugin-node/issues/77
|
||||
'node/no-unpublished-require': 'off'
|
||||
})
|
||||
}
|
||||
]
|
||||
extends: ['plugin:n/recommended'],
|
||||
},
|
||||
{
|
||||
// test files
|
||||
files: ['tests/**/*-test.{js,ts}'],
|
||||
extends: ['plugin:qunit/recommended'],
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
156
.github/workflows/ci.yml
vendored
Normal file
156
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,156 @@
|
|||
name: Test Frontend and backend
|
||||
# This workflow is triggered on pushes to the repository.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- renovate/*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
test-bundlesize:
|
||||
name: test bundlesize
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install php
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
extensions: mbstring, zip
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install node modules
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
run: npm run test:bundlesize
|
||||
|
||||
test-csp-header:
|
||||
name: test CSP in .htaccess
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install node modules
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
run: npm run test:csp-header
|
||||
|
||||
test-chrome:
|
||||
name: test against Chrome
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install node modules
|
||||
run: npm ci
|
||||
- name: Install chrome browser
|
||||
run: |
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
sudo apt install ./google-chrome-stable_current_amd64.deb
|
||||
- name: Build with test environment
|
||||
env:
|
||||
CI: true
|
||||
run: npm run build --environment test
|
||||
- name: run tests in chrome
|
||||
run: npm run test:ember --launch Chrome --path dist
|
||||
|
||||
test-firefox:
|
||||
name: test against Firefox
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install node modules
|
||||
run: npm ci
|
||||
- name: Setup firefox
|
||||
uses: browser-actions/setup-firefox@latest
|
||||
with:
|
||||
firefox-version: 102.0.1
|
||||
- name: Build with test environment
|
||||
env:
|
||||
CI: true
|
||||
run: npm run build --environment test
|
||||
- name: run tests in firefox
|
||||
run: npm run test:ember --launch Firefox --path dist
|
||||
|
||||
test-browserstack:
|
||||
name: test against additional browser in BrowserStack
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install node modules
|
||||
run: npm ci
|
||||
- name: Build with test environment
|
||||
env:
|
||||
CI: true
|
||||
run: npm run build --environment test
|
||||
- name: 'BrowserStack Env Setup'
|
||||
uses: 'browserstack/github-actions/setup-env@master'
|
||||
with:
|
||||
username: 'jeldrikhanschke1'
|
||||
access-key: 'xaM9Uxurv2GyxFLKQXgj'
|
||||
- name: 'Start BrowserStackLocal Tunnel'
|
||||
uses: 'browserstack/github-actions/setup-local@master'
|
||||
with:
|
||||
local-testing: 'start'
|
||||
local-logging-level: 'all-logs'
|
||||
local-identifier: 'random'
|
||||
- name: 'Running test on BrowserStack'
|
||||
run: npm run test:ember --config-file testem.browserstack.js --path dist
|
||||
- name: 'BrowserStackLocal Stop'
|
||||
uses: browserstack/github-actions/setup-local@master
|
||||
with:
|
||||
local-testing: stop
|
||||
|
||||
test-backend:
|
||||
name: Test php backend
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
steps:
|
||||
- name: Checkout repository files
|
||||
uses: actions/checkout@v4
|
||||
- name: Install php
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: mbstring, zip
|
||||
- name: Install php dependencies
|
||||
run: composer install
|
||||
working-directory: ./api
|
||||
- name: Run backend tests
|
||||
run: ./vendor/bin/codecept run
|
||||
working-directory: ./api
|
120
.github/workflows/test-workflow.yml
vendored
120
.github/workflows/test-workflow.yml
vendored
|
@ -1,120 +0,0 @@
|
|||
name: Test Frontend and backend
|
||||
# This workflow is triggered on pushes to the repository.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-javascript:
|
||||
name: lint javascript
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Run lint
|
||||
run: yarn lint:js
|
||||
lint-templates:
|
||||
name: lint ember templates
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Run lint
|
||||
run: yarn lint:hbs
|
||||
test-bundlesize:
|
||||
name: test bundlesize
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Install php
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: '7.4'
|
||||
extensions: mbstring, zip
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Run tests
|
||||
run: yarn test:bundlesize
|
||||
test-csp-header:
|
||||
name: test CSP in .htaccess
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Run tests
|
||||
run: yarn test:csp-header
|
||||
test-chrome:
|
||||
name: test in chromium
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Install chrome browser
|
||||
run: |
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
sudo apt install ./google-chrome-stable_current_amd64.deb
|
||||
- name: Build with test environment
|
||||
env:
|
||||
CI: true
|
||||
run: yarn build --environment test
|
||||
- name: run tests in chrome
|
||||
run: yarn test --launch Chrome --path dist
|
||||
test-firefox:
|
||||
name: test in firefox
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Install node modules
|
||||
run: yarn install
|
||||
- name: Install Firefox
|
||||
run: sudo apt-get install firefox
|
||||
- name: Build with test environment
|
||||
env:
|
||||
CI: true
|
||||
run: yarn build --environment test
|
||||
- name: run tests in firefox
|
||||
run: yarn test --launch Firefox --path dist
|
||||
test-backend:
|
||||
name: Test php backend
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.2', '7.3', '7.4']
|
||||
steps:
|
||||
- name: Checkout repository files
|
||||
uses: actions/checkout@v2
|
||||
- name: Install php
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: mbstring, zip
|
||||
- name: Install php dependencies
|
||||
run: composer install
|
||||
working-directory: ./api
|
||||
- name: Run backend tests
|
||||
run: ./vendor/bin/codecept run
|
||||
working-directory: ./api
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,33 +1,34 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
/declarations/
|
||||
/api/tests/_output/
|
||||
/api/tests/_tmp/
|
||||
/api/tests/_support/_generated/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
/api/vendor/
|
||||
|
||||
# misc
|
||||
/.env*
|
||||
/.pnp*
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/.eslintcache
|
||||
/coverage/
|
||||
/libpeerconnection.log
|
||||
/npm-debug.log*
|
||||
/testem.log
|
||||
/yarn-error.log
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
|
||||
# BrowserStack
|
||||
/*.pid
|
||||
/*.log
|
||||
|
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
12
.prettierrc.js
Normal file
12
.prettierrc.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: '*.{js,ts}',
|
||||
options: {
|
||||
singleQuote: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -14,5 +14,8 @@
|
|||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it-plugins/lerna-changelog": {}
|
||||
}
|
||||
}
|
||||
|
|
37
.renovaterc
37
.renovaterc
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"@ember/jquery",
|
||||
"@ember/optional-features",
|
||||
"broccoli-asset-rev",
|
||||
"ember-ajax",
|
||||
"ember-cli",
|
||||
"ember-cli-app-version",
|
||||
"ember-cli-babel",
|
||||
"ember-cli-dependency-checker",
|
||||
"ember-cli-eslint",
|
||||
"ember-cli-htmlbars",
|
||||
"ember-cli-htmlbars-inline-precompile",
|
||||
"ember-cli-inject-live-reload",
|
||||
"ember-cli-sri",
|
||||
"ember-cli-template-lint",
|
||||
"ember-cli-uglify",
|
||||
"ember-data",
|
||||
"ember-export-application-global",
|
||||
"ember-load-initializers",
|
||||
"ember-maybe-import-regenerator",
|
||||
"ember-qunit",
|
||||
"ember-resolver",
|
||||
"ember-source",
|
||||
"ember-welcome-page",
|
||||
"eslint-plugin-ember",
|
||||
"loader.js",
|
||||
"qunit-dom"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"lib/**"
|
||||
],
|
||||
"rangeStrategy": "update-lockfile"
|
||||
}
|
8
.stylelintignore
Normal file
8
.stylelintignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
# unconventional files
|
||||
/blueprints/*/files/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
|
||||
# addons
|
||||
/.node_modules.ember-try/
|
15
.stylelintrc.js
Normal file
15
.stylelintrc.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-standard-scss',
|
||||
'stylelint-prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'declaration-block-no-duplicate-properties': null,
|
||||
'no-descending-specificity': null,
|
||||
'scss/no-global-function-names': null,
|
||||
'selector-class-pattern': null,
|
||||
},
|
||||
};
|
|
@ -1,10 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: 'recommended',
|
||||
plugins: ['ember-template-lint-plugin-prettier'],
|
||||
extends: ['recommended', 'ember-template-lint-plugin-prettier:recommended'],
|
||||
rules: {
|
||||
'no-implicit-this': {
|
||||
allow: ['scroll-first-invalid-element-into-view-port'],
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['tests/integration/modifiers/*.js'],
|
||||
rules: {
|
||||
'require-input-label': false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
70
.travis.yml
70
.travis.yml
|
@ -1,70 +0,0 @@
|
|||
---
|
||||
language: php
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: TEST="API"
|
||||
php: 7.3
|
||||
- env: TEST="API"
|
||||
php: 7.2
|
||||
- env: TEST="API"
|
||||
php: 7.1
|
||||
- env: TEST="EMBER"
|
||||
- env: TEST="BROWSER"
|
||||
- env: TEST="BUNDLESIZE"
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
firefox: latest-esr
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
env:
|
||||
global:
|
||||
- "BROWSERSTACK_USERNAME=jeldrikhanschke1"
|
||||
- "BROWSERSTACK_ACCESS_KEY=xaM9Uxurv2GyxFLKQXgj"
|
||||
|
||||
before_install:
|
||||
# use a recent node version if ember build is tested
|
||||
- if [ $TEST = "EMBER" ] || [ $TEST = "BROWSER" ] || [ $TEST = "BUNDLESIZE" ]; then nvm install --lts; fi
|
||||
# provide yarn if ember build is tested
|
||||
- if [ $TEST = "EMBER" ] || [ $TEST = "BROWSER" ] || [ $TEST = "BUNDLESIZE" ]; then curl -o- -L https://yarnpkg.com/install.sh | bash; fi
|
||||
- if [ $TEST = "EMBER" ] || [ $TEST = "BROWSER" ] || [ $TEST = "BUNDLESIZE" ]; then export PATH=$HOME/.yarn/bin:$PATH; fi
|
||||
|
||||
install:
|
||||
# install dependencies for client
|
||||
- if [ $TEST = "EMBER" ] || [ $TEST = "BROWSER" ] || [ $TEST = "BUNDLESIZE" ]; then yarn install --no-interactive; fi
|
||||
# install dependencies for api
|
||||
- if [ $TEST = "API" ]; then cd api/ && composer install && cd ..; fi
|
||||
|
||||
before_script:
|
||||
# http://php.net/manual/de/ini.core.php#ini.always-populate-raw-post-data
|
||||
- if [ $TEST = "API" ]; then echo 'always_populate_raw_post_data = -1' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi
|
||||
# create a browser stack tunnel for cross-browser testing
|
||||
- if [ $TEST = "BROWSER" ]; then node_modules/ember-cli/bin/ember browserstack:connect; fi
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
script:
|
||||
# run frontend and integration tests
|
||||
- if [ $TEST = "EMBER" ]; then yarn run lint:hbs; fi
|
||||
- if [ $TEST = "EMBER" ]; then yarn run lint:js; fi
|
||||
- if [ $TEST = "EMBER" ]; then yarn test; fi
|
||||
# test that CSP headers in public/.htaccess are matching the ones configured in config/environment.js
|
||||
- if [ $TEST = "EMBER" ]; then grep "`node_modules/ember-cli/bin/ember csp-headers --environment production --silent 2>&1 | sed 's/ $//'`" public/.htaccess || (echo "CSP headers in public/.htaccess does not match configuration" && exit 1); fi
|
||||
# test against different browsers using sauce lab
|
||||
- if [ $TEST = "BROWSER" ]; then yarn test --config-file testem.browserstack.js; fi
|
||||
# test bundle size
|
||||
- if [ $TEST = "BUNDLESIZE" ]; then yarn test:bundlesize; fi
|
||||
# run api tests with composer
|
||||
- if [ $TEST = "API" ]; then cd api/ && ./vendor/bin/codecept run && cd ..; fi
|
||||
|
||||
after_script:
|
||||
# destroy the sauce tunnel
|
||||
- if [ $TEST = "BROWSER" ]; then node_modules/ember-cli/bin/ember browserstack:disconnect; fi
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
"ignore_dirs": ["dist"]
|
||||
}
|
||||
|
|
33
README.md
33
README.md
|
@ -1,8 +1,7 @@
|
|||
# Croodle
|
||||
|
||||
[![Build Status](https://travis-ci.org/jelhan/croodle.svg?branch=master)](https://travis-ci.org/jelhan/croodle)
|
||||
[![Build Status](https://github.com/jelhan/croodle/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jelhan/croodle/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||
[![Code Climate](https://codeclimate.com/github/jelhan/croodle/badges/gpa.svg)](https://codeclimate.com/github/jelhan/croodle)
|
||||
[![devDependency Status](https://david-dm.org/jelhan/croodle/dev-status.svg)](https://david-dm.org/jelhan/croodle?type=dev)
|
||||
|
||||
Croodle is an end-to-end encrypted web application to schedule a date or to do a poll on a any topic. All data like title, description, number and labels of options, available answers and names of users and their selections are encrypted/decrypted in the browser using strong 256-bit AES encryption.
|
||||
|
||||
|
@ -23,7 +22,7 @@ Theoretically you could also check for an attack like this by analysing the sour
|
|||
|
||||
## Requirements
|
||||
|
||||
Croodle is designed to have as few as possible requirements on the server it is running on. Croodle runs on almost every web space with PHP >= 5.6. Croodle stores the data in textfiles, so there is no need for a database server like MySQL.
|
||||
Croodle is designed to have as few as possible requirements on the server it is running on. Croodle runs on almost every web space with PHP >= 7.2. Croodle stores the data in textfiles, so there is no need for a database server like MySQL.
|
||||
|
||||
Due to security reasons you should have TLS encryption enabled and provide a valid certificate. (see the [security notice](#security-notice))
|
||||
|
||||
|
@ -31,14 +30,14 @@ Due to security reasons you should have TLS encryption enabled and provide a val
|
|||
|
||||
Production builds are provided as github [release assets](https://github.com/jelhan/croodle/releases).
|
||||
|
||||
If you like to build yourself you have to install [yarn](https://yarnpkg.com/), [ember-cli](http://www.ember-cli.com/) and [composer](https://getcomposer.org/) before.
|
||||
If you like to build yourself you have to install [node](https://nodejs.org/), [ember-cli](http://www.ember-cli.com/) and [composer](https://getcomposer.org/) before. It's recommended using [volta](https://volta.sh/) to ensure a compatible and tested node version is used.
|
||||
|
||||
```shell
|
||||
git clone git@github.com:jelhan/croodle.git
|
||||
cd croodle
|
||||
yarn install
|
||||
npm install
|
||||
cd api/ && composer install --no-dev && cd ..
|
||||
yarn build --prod
|
||||
npm run build
|
||||
```
|
||||
|
||||
Afterwards copy all files in `/dist` folder to your werbserver.
|
||||
|
@ -65,15 +64,26 @@ If source files are changing, a rebuild and reload is triggered.
|
|||
By default Croodle uses an api mock in development. Since that one
|
||||
does not persist records all polls are gone after a reload.
|
||||
|
||||
If you like to test against the real API, run api via php built-in web
|
||||
server: `php -S 127.0.0.1:8080 -t dist/`
|
||||
If you like to test against the real API, you should run the API
|
||||
using php built-in web server locally:
|
||||
|
||||
```sh
|
||||
php -S 127.0.0.1:8080 -t dist/
|
||||
```
|
||||
|
||||
Afterwards start ember-cli development server using `--proxy` option:
|
||||
`ember server --proxy http://127.0.0.1:8080`.
|
||||
|
||||
```sh
|
||||
ember server --proxy http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
Ember-cli clears dist folder on each rebuild. If you like to keep
|
||||
created polls over rebuild, configure api to use a non default folder
|
||||
to save your polls:
|
||||
`CROODLE__DATA_DIR=/tmp/croodle_data php -S 127.0.0.1:8080 -t dist/`
|
||||
|
||||
```sh
|
||||
CROODLE__DATA_DIR=/tmp/croodle_data php -S 127.0.0.1:8080 -t dist/
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
|
@ -96,9 +106,6 @@ without `--no-dev` option).
|
|||
|
||||
## Credits
|
||||
|
||||
Continous Integration powered by<br>
|
||||
<a href="https://travis-ci.com/"><img src="https://travis-ci.com/images/logos/TravisCI-Full-Color.png" height="50"></a>
|
||||
|
||||
Cross-browser testing provided by<br>
|
||||
<a href="https://www.browserstack.com"><img src="docs/Browserstack-logo.svg" height="50"></a>
|
||||
|
||||
|
|
|
@ -32,14 +32,14 @@ Once the prep work is completed, the actual release is straight forward:
|
|||
* First ensure that you have installed the project dependencies:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
npm ci
|
||||
```
|
||||
|
||||
* Second, do your release:
|
||||
|
||||
```sh
|
||||
export GITHUB_AUTH="github-personal-access-token"
|
||||
yarn release
|
||||
npm run release
|
||||
```
|
||||
|
||||
[release-it](https://github.com/release-it/release-it/) manages the actual
|
||||
|
|
|
@ -177,11 +177,6 @@ class Model {
|
|||
}
|
||||
|
||||
$data = self::convertFromStorage($storageObject);
|
||||
|
||||
if(method_exists($model, 'restoreLegacySupportHook')) {
|
||||
$model->restoreLegacySupportHook($data);
|
||||
}
|
||||
|
||||
$properties = array_merge(
|
||||
static::ENCRYPTED_PROPERTIES,
|
||||
static::PLAIN_PROPERTIES,
|
||||
|
|
|
@ -6,7 +6,6 @@ require_once 'user.php';
|
|||
class Poll extends model {
|
||||
const ENCRYPTED_PROPERTIES = [
|
||||
'anonymousUser',
|
||||
'answers',
|
||||
'answerType',
|
||||
'creationDate',
|
||||
'description',
|
||||
|
@ -126,20 +125,4 @@ class Poll extends model {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function restoreLegacySupportHook(&$data) {
|
||||
if (!isset($data->version) || $data->version === 'v0.3-0') {
|
||||
if (isset($data->poll) && is_object($data->poll)) {
|
||||
$data = $data->poll;
|
||||
}
|
||||
|
||||
foreach($data as $key => $value) {
|
||||
if (strpos($key, 'encrypted') === 0) {
|
||||
$newKey = lcfirst(substr($key, 9));
|
||||
$data->$newKey = $data->$key;
|
||||
unset($data->$key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ class User extends Model {
|
|||
}
|
||||
|
||||
if (!Poll::isValidId($pollId)) {
|
||||
throw new Exception('cound not get a valid id when getPollDir was called');
|
||||
throw new Exception('Could not get a valid id when getPollDir was called');
|
||||
}
|
||||
|
||||
return DATA_FOLDER . $pollId . '/';
|
||||
|
@ -74,20 +74,4 @@ class User extends Model {
|
|||
Poll::isValidId($parts[0]) &&
|
||||
intval($parts[1]) == $parts[1];
|
||||
}
|
||||
|
||||
protected function restoreLegacySupportHook(&$data) {
|
||||
if (!isset($data->version) || $data->version === 'v0.3-0') {
|
||||
if (isset($data->user) && is_object($data->user)) {
|
||||
$data = $data->user;
|
||||
}
|
||||
|
||||
foreach($data as $key => $value) {
|
||||
if (strpos($key, 'encrypted') === 0) {
|
||||
$newKey = lcfirst(substr($key, 9));
|
||||
$data->$newKey = $data->$key;
|
||||
unset($data->$key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ settings:
|
|||
bootstrap: _bootstrap.php
|
||||
colors: true
|
||||
memory_limit: 1024M
|
||||
error_level: E_ALL & ~E_DEPRECATED
|
||||
extensions:
|
||||
enabled:
|
||||
- Codeception\Extension\RunFailed
|
||||
|
@ -20,6 +21,5 @@ extensions:
|
|||
port: 8000
|
||||
documentRoot: .
|
||||
startDelay: 1
|
||||
phpIni: /etc/php5/apache2/php.ini
|
||||
params:
|
||||
- .env.testing
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
{
|
||||
"require": {
|
||||
"php": "^7.1",
|
||||
"slim/slim": "^3.5"
|
||||
"php": "^7.2",
|
||||
"slim/slim": "3.12.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"flow/jsonpath": "^0.5.0",
|
||||
"vlucas/phpdotenv": "^3.6.0",
|
||||
"codeception/codeception": "^3.1",
|
||||
"codeception/phpbuiltinserver": "^1.5"
|
||||
"flow/jsonpath": "0.5.0",
|
||||
"vlucas/phpdotenv": "3.6.10",
|
||||
"codeception/codeception": "3.1.3",
|
||||
"codeception/phpbuiltinserver": "@dev"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1"
|
||||
"php": "7.2"
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/jelhan/PhpBuiltinServer.git",
|
||||
"reference":"support-codeception-error-level-and-memory-limit-settings"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
2601
api/composer.lock
generated
2601
api/composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,34 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"anonymousUser":"{\"iv\":\"gVHZSXyMm10Fn+kDooa7uw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"GJsQQYA7TdAa+v3Rvg==\"}","answers":"{\"iv\":\"aK1JcI3viLPIlOO45K+ePA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"Bx4SRcww+hJ46NIiVcWBUZHADADX\/XPsxXMx4XzMQZWqu6M0690D4oTflSRJoqxe0egxdfMOUxuWhmACG\/UYXSYJQjcSg+QTq6KJbaXG+SvsCMZ7iz12a\/uf9lXyiag4IbLldgL4vE3LfZO6oih\/o\/yG4hechjNdSkqUa2IvsRbXWB2aHen6a5Ch5WjqWrr4xRRrukPvf7aumilT2Cf0LswHJ2fwYNilylV0h9oegKYp+qWphm4SL8x2ogRemSCt7u7ByEOwZV0w6D9bz9RvGLTRRLJaLIm\/VlE3k7R6Hz1vyps=\"}","answerType":"{\"iv\":\"ILkAzgUfAGNUtLr7CbEJEQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"nMOp+QApQGgP9dwefNpi\"}","creationDate":"{\"iv\":\"6tWbieK03uXUR+E0AMbs0A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"YkkLVBkFyx4xFldZ7qnDESG0teHJmXaPMUB05p9L0xUIMg==\"}","description":"{\"iv\":\"fWvHh47So4WBNfEHXrwLiA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"5W7nauOakSoFD52V\"}","expirationDate":"{\"iv\":\"HRsMvEQaoCp8QdqBGHevnA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"LXYamNRDyhIY5xY+CLqI4GHbocc9NoHQtePKU9fHpJn9zg==\"}","forceAnswer":"{\"iv\":\"bh4iZ4pKe0GnXcM764702g==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"q5VBynWGotXRrc2P\"}","isDateTime":"{\"iv\":\"mlDCtvsJZaDlZD9kqfJHuA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"53g42C6Am+0s25\/DsA==\"}","options":"{\"iv\":\"ZneP\/x45NGh\/DC26GI4kvg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"4MvV9SNQq2dB6b\/MdX47R0KaRSfyZOZMEVUFDv7G3\/EcDBv7Z0pgSU9JXoF8BoSOz40rYrRtTw==\"}","pollType":"{\"iv\":\"j3P6eN0ZmNMMxLTAVD6gjQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"opwiZHAQi+I8R5HDxLfLK59DcQ==\"}","timezone":"{\"iv\":\"HKkSqcJONggGT9QQ+jZdUg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"BANN8sJlk8JK9A==\"}","title":"{\"iv\":\"4DX7dAJt7JIBHaR1V0Ct8A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"f1VUZf69nB94TF3\/HA==\"}","version":"v0.3.0+0ae62f31","serverExpirationDate":"2015-11-22T20:35:03.764Z"}';
|
||||
$userJson = '{"user":{"name":"{\"iv\":\"kizIqK7FPNmRuQB7VHsMOw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UsYMzrww3HKR8vl2TKVE\"}","selections":"{\"iv\":\"hRmiZagEhQVhw2cg6UJNrg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"2zIPGpiSC6wJHRoAMYBFPXx3qmlZg0Z/Jt/15mY+sHPLCqoAn97TKGN6KIvl/5gmgCFqLQFNo6uppCTUhljoV5y2kMtGvm0g3+NdpcejWGOeMACDPcp1mpXII87ZTfC6WrtxcWCB6UGYN8EynOdndFTGp+WVZnXCCya7YPThk/QRwoHoPWS6+TJFT9WeHV4i4kUIg2K3kdz3Op7S/c7l7KbOc8GsyjZzv0bRDnAm68/+FlJyZnvfMfU8vTxExsIsd0pBy4JBV4hg9SlCPectb5BAvBCULLDPA08prf262RUmVKJ+M3P1+5KkBQcnQwnUW/fzAQ7lqA==\"}","creationDate":"{\"iv\":\"xqdDY/A7MHLeAsoU9S/j+A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"TQOhbjveZbvdiyYpxfwNyu5pi1PLia9FApJJRmr3QoyrWA==\"}","version":"v0.3.0+0ae62f31","poll":"' . $pollId . '"}}';
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"anonymousUser": "{\"iv\":\"gVHZSXyMm10Fn+kDooa7uw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"GJsQQYA7TdAa+v3Rvg==\"}",
|
||||
"answerType": "{\"iv\":\"ILkAzgUfAGNUtLr7CbEJEQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"nMOp+QApQGgP9dwefNpi\"}",
|
||||
"creationDate": "{\"iv\":\"6tWbieK03uXUR+E0AMbs0A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"YkkLVBkFyx4xFldZ7qnDESG0teHJmXaPMUB05p9L0xUIMg==\"}",
|
||||
"description": "{\"iv\":\"fWvHh47So4WBNfEHXrwLiA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"5W7nauOakSoFD52V\"}",
|
||||
"expirationDate": "{\"iv\":\"HRsMvEQaoCp8QdqBGHevnA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"LXYamNRDyhIY5xY+CLqI4GHbocc9NoHQtePKU9fHpJn9zg==\"}",
|
||||
"forceAnswer": "{\"iv\":\"bh4iZ4pKe0GnXcM764702g==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"q5VBynWGotXRrc2P\"}",
|
||||
"isDateTime": "{\"iv\":\"mlDCtvsJZaDlZD9kqfJHuA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"53g42C6Am+0s25/DsA==\"}",
|
||||
"options": "{\"iv\":\"ZneP/x45NGh/DC26GI4kvg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"4MvV9SNQq2dB6b/MdX47R0KaRSfyZOZMEVUFDv7G3/EcDBv7Z0pgSU9JXoF8BoSOz40rYrRtTw==\"}",
|
||||
"pollType": "{\"iv\":\"j3P6eN0ZmNMMxLTAVD6gjQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"opwiZHAQi+I8R5HDxLfLK59DcQ==\"}",
|
||||
"timezone": "{\"iv\":\"HKkSqcJONggGT9QQ+jZdUg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"BANN8sJlk8JK9A==\"}",
|
||||
"title": "{\"iv\":\"4DX7dAJt7JIBHaR1V0Ct8A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"f1VUZf69nB94TF3/HA==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"serverExpirationDate": "2015-11-22T20:35:03.764Z"
|
||||
}
|
||||
EOD;
|
||||
$userJson = <<<EOD
|
||||
{
|
||||
"user": {
|
||||
"name": "{\"iv\":\"kizIqK7FPNmRuQB7VHsMOw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UsYMzrww3HKR8vl2TKVE\"}",
|
||||
"selections": "{\"iv\":\"hRmiZagEhQVhw2cg6UJNrg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"2zIPGpiSC6wJHRoAMYBFPXx3qmlZg0Z/Jt/15mY+sHPLCqoAn97TKGN6KIvl/5gmgCFqLQFNo6uppCTUhljoV5y2kMtGvm0g3+NdpcejWGOeMACDPcp1mpXII87ZTfC6WrtxcWCB6UGYN8EynOdndFTGp+WVZnXCCya7YPThk/QRwoHoPWS6+TJFT9WeHV4i4kUIg2K3kdz3Op7S/c7l7KbOc8GsyjZzv0bRDnAm68/+FlJyZnvfMfU8vTxExsIsd0pBy4JBV4hg9SlCPectb5BAvBCULLDPA08prf262RUmVKJ+M3P1+5KkBQcnQwnUW/fzAQ7lqA==\"}",
|
||||
"creationDate": "{\"iv\":\"xqdDY/A7MHLeAsoU9S/j+A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"TQOhbjveZbvdiyYpxfwNyu5pi1PLia9FApJJRmr3QoyrWA==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"poll": "$pollId"
|
||||
}
|
||||
}
|
||||
EOD;
|
||||
|
||||
$pollDir = 'tests/_tmp/data/' . $pollId . '/';
|
||||
$userDir = $pollDir . 'user/';
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
<?php
|
||||
|
||||
$pollJson = '{"poll":{"title":"{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}","description":"{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}","pollType":"{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}","answerType":"{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO/yFmk\"}","answers":"{\"iv\":\"WRdAwEa0DF+E83ginLYtPw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"Oaer31ct2PXkmXkzJ1EXRPM3LMf6vGfzMZqjODwey4f7EhqSCUhYov+N7AZKCAAXYVS4WR84kKizxXBK2PQBSFrlB3Bll74ED9ZzRJSJD00otMG9BbgUR90aFws+1jMBP5vpti9+POsii85zLbDPkNg/Th/C4Ufv5YWwg/4ZV0bFMyOgfdjtOWaG5YAMTGUIkz9U9+VCesYJQaTb497qTD/Wmtz8J/2pUxdL5/b5xkdh2DJ4/N5q0Kz/CEbaoKwbexnQDlSr3ldlIhs7UmBjC9gkpgG2l9fu6a0VZFBE8hvzYrw=\"}","options":"{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs/Jm5XK/thQW0phxKd0OxKt9NZ3FE/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}","creationDate":"{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p/ANinfanE/51DbcDNw==\"}","forceAnswer":"{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}","anonymousUser":"{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}","timezone":"{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}","expirationDate":"{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}","serverExpirationDate":"2015-11-22T22:05:15.065Z","version":"v0.3.0+0ae62f31"}}';
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"poll": {
|
||||
"title": "{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}",
|
||||
"description": "{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}",
|
||||
"pollType": "{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}",
|
||||
"answerType": "{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO/yFmk\"}",
|
||||
"options": "{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs/Jm5XK/thQW0phxKd0OxKt9NZ3FE/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}",
|
||||
"creationDate": "{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p/ANinfanE/51DbcDNw==\"}",
|
||||
"forceAnswer": "{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}",
|
||||
"anonymousUser": "{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}",
|
||||
"timezone": "{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}",
|
||||
"expirationDate": "{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}",
|
||||
"serverExpirationDate": "2015-11-22T22:05:15.065Z",
|
||||
"version": "v0.3.0+0ae62f31"
|
||||
}
|
||||
}
|
||||
EOD;
|
||||
|
||||
$I = new ApiTester($scenario);
|
||||
$I->wantTo('create a poll');
|
||||
|
|
|
@ -1,8 +1,34 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"anonymousUser":"{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}","answers":"{\"iv\":\"WRdAwEa0DF+E83ginLYtPw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"Oaer31ct2PXkmXkzJ1EXRPM3LMf6vGfzMZqjODwey4f7EhqSCUhYov+N7AZKCAAXYVS4WR84kKizxXBK2PQBSFrlB3Bll74ED9ZzRJSJD00otMG9BbgUR90aFws+1jMBP5vpti9+POsii85zLbDPkNg\/Th\/C4Ufv5YWwg\/4ZV0bFMyOgfdjtOWaG5YAMTGUIkz9U9+VCesYJQaTb497qTD\/Wmtz8J\/2pUxdL5\/b5xkdh2DJ4\/N5q0Kz\/CEbaoKwbexnQDlSr3ldlIhs7UmBjC9gkpgG2l9fu6a0VZFBE8hvzYrw=\"}","answerType":"{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO\/yFmk\"}","creationDate":"{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p\/ANinfanE\/51DbcDNw==\"}","description":"{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}","expirationDate":"{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}","forceAnswer":"{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}","isDateTime":"{\"iv\":\"3y9OmTJDG0mLqU5zLoZwgQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"yyGaGitGrunDSpsRpw==\"}","options":"{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs\/Jm5XK\/thQW0phxKd0OxKt9NZ3FE\/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}","pollType":"{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}","timezone":"{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}","title":"{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}","version":"v0.3.0+0ae62f31","serverExpirationDate":"2015-11-22T22:05:15.065Z"}';
|
||||
$userJson = '{"user":{"name":"{\"iv\":\"kizIqK7FPNmRuQB7VHsMOw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UsYMzrww3HKR8vl2TKVE\"}","selections":"{\"iv\":\"hRmiZagEhQVhw2cg6UJNrg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"2zIPGpiSC6wJHRoAMYBFPXx3qmlZg0Z/Jt/15mY+sHPLCqoAn97TKGN6KIvl/5gmgCFqLQFNo6uppCTUhljoV5y2kMtGvm0g3+NdpcejWGOeMACDPcp1mpXII87ZTfC6WrtxcWCB6UGYN8EynOdndFTGp+WVZnXCCya7YPThk/QRwoHoPWS6+TJFT9WeHV4i4kUIg2K3kdz3Op7S/c7l7KbOc8GsyjZzv0bRDnAm68/+FlJyZnvfMfU8vTxExsIsd0pBy4JBV4hg9SlCPectb5BAvBCULLDPA08prf262RUmVKJ+M3P1+5KkBQcnQwnUW/fzAQ7lqA==\"}","creationDate":"{\"iv\":\"xqdDY/A7MHLeAsoU9S/j+A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"TQOhbjveZbvdiyYpxfwNyu5pi1PLia9FApJJRmr3QoyrWA==\"}","version":"v0.3.0+0ae62f31","poll":"' . $pollId . '"}}';
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"anonymousUser": "{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}",
|
||||
"answerType": "{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO/yFmk\"}",
|
||||
"creationDate": "{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p/ANinfanE/51DbcDNw==\"}",
|
||||
"description": "{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}",
|
||||
"expirationDate": "{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}",
|
||||
"forceAnswer": "{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}",
|
||||
"isDateTime": "{\"iv\":\"3y9OmTJDG0mLqU5zLoZwgQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"yyGaGitGrunDSpsRpw==\"}",
|
||||
"options": "{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs/Jm5XK/thQW0phxKd0OxKt9NZ3FE/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}",
|
||||
"pollType": "{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}",
|
||||
"timezone": "{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}",
|
||||
"title": "{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"serverExpirationDate": "2015-11-22T22:05:15.065Z"
|
||||
}
|
||||
EOD;
|
||||
$userJson = <<<EOD
|
||||
{
|
||||
"user": {
|
||||
"name": "{\"iv\":\"kizIqK7FPNmRuQB7VHsMOw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UsYMzrww3HKR8vl2TKVE\"}",
|
||||
"selections": "{\"iv\":\"hRmiZagEhQVhw2cg6UJNrg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"2zIPGpiSC6wJHRoAMYBFPXx3qmlZg0Z/Jt/15mY+sHPLCqoAn97TKGN6KIvl/5gmgCFqLQFNo6uppCTUhljoV5y2kMtGvm0g3+NdpcejWGOeMACDPcp1mpXII87ZTfC6WrtxcWCB6UGYN8EynOdndFTGp+WVZnXCCya7YPThk/QRwoHoPWS6+TJFT9WeHV4i4kUIg2K3kdz3Op7S/c7l7KbOc8GsyjZzv0bRDnAm68/+FlJyZnvfMfU8vTxExsIsd0pBy4JBV4hg9SlCPectb5BAvBCULLDPA08prf262RUmVKJ+M3P1+5KkBQcnQwnUW/fzAQ7lqA==\"}",
|
||||
"creationDate": "{\"iv\":\"xqdDY/A7MHLeAsoU9S/j+A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"TQOhbjveZbvdiyYpxfwNyu5pi1PLia9FApJJRmr3QoyrWA==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"poll": "$pollId"
|
||||
}
|
||||
}
|
||||
EOD;
|
||||
|
||||
$pollDir = TEST_DATA_DIR . $pollId . '/';
|
||||
$usersDir = $pollDir . 'users/';
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"anonymousUser":"{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}","answers":"{\"iv\":\"WRdAwEa0DF+E83ginLYtPw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"Oaer31ct2PXkmXkzJ1EXRPM3LMf6vGfzMZqjODwey4f7EhqSCUhYov+N7AZKCAAXYVS4WR84kKizxXBK2PQBSFrlB3Bll74ED9ZzRJSJD00otMG9BbgUR90aFws+1jMBP5vpti9+POsii85zLbDPkNg\/Th\/C4Ufv5YWwg\/4ZV0bFMyOgfdjtOWaG5YAMTGUIkz9U9+VCesYJQaTb497qTD\/Wmtz8J\/2pUxdL5\/b5xkdh2DJ4\/N5q0Kz\/CEbaoKwbexnQDlSr3ldlIhs7UmBjC9gkpgG2l9fu6a0VZFBE8hvzYrw=\"}","answerType":"{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO\/yFmk\"}","creationDate":"{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p\/ANinfanE\/51DbcDNw==\"}","description":"{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}","expirationDate":"{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}","forceAnswer":"{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}","isDateTime":"{\"iv\":\"3y9OmTJDG0mLqU5zLoZwgQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"yyGaGitGrunDSpsRpw==\"}","options":"{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs\/Jm5XK\/thQW0phxKd0OxKt9NZ3FE\/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}","pollType":"{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}","timezone":"{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}","title":"{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}","version":"v0.3.0+0ae62f31","serverExpirationDate":"2015-01-01T00:00:00.000Z"}';
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"anonymousUser": "{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}",
|
||||
"answerType": "{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO/yFmk\"}",
|
||||
"creationDate": "{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p/ANinfanE/51DbcDNw==\"}",
|
||||
"description": "{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}",
|
||||
"expirationDate": "{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}",
|
||||
"forceAnswer": "{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}",
|
||||
"isDateTime": "{\"iv\":\"3y9OmTJDG0mLqU5zLoZwgQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"yyGaGitGrunDSpsRpw==\"}",
|
||||
"options": "{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs/Jm5XK/thQW0phxKd0OxKt9NZ3FE/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}",
|
||||
"pollType": "{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}",
|
||||
"timezone": "{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}",
|
||||
"title": "{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"serverExpirationDate": "2015-01-01T00:00:00.000Z"
|
||||
}
|
||||
EOD;
|
||||
|
||||
$pollDir = TEST_DATA_DIR . $pollId . '/';
|
||||
$usersDir = $pollDir . 'users/';
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"poll":{"encryptedTitle":"{\"iv\":\"G1QGS+OHz5Z6Y4Og/3UFRQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"rgMRyJep4e0+Jj+K0ZTqbJS1j/gaouoTCoSHgXFdccn5L9gHBo1JO7Sl\"}","encryptedDescription":"{\"iv\":\"StcBqdGghIip/N3gLFmTMQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"5fgh7XABR7OifXoqHxE+c89mnVwkKUAG+x7D+BOGzoZK8dGT\"}","encryptedPollType":"{\"iv\":\"JYFdfUTb6xLWja302/bAKQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"wW+J3cGGF1tQUNfxt4gENDcZXQ==\"}","encryptedAnswerType":"{\"iv\":\"i0lDlvIVg2Le8pBSb47CIA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"+LNTLmILvxsN1X6E+vWa\"}","encryptedAnswers":"{\"iv\":\"xQV29b/F+gvlLl0zDCB7yw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"n5CwLLhSE5d28NU2/rOB7o6tXXWdDE7/uPr951Rr2ZQsmhsadmVwYE0K3Cxt+Hif4Am1jliS+PFjgVralrsSB00vlIH53wvDqQmNdk1Q/2zIebsVhHueamL4REyXn+18uVrjRarioojwOPYJLxNJHh0kPHATd0TgJxTb87RXgqUvAr1xc6DL7hY+fIbGoa6Otzt+OqIPhRTpaL+My1TYFXWQSlJxpPVSOILe1G/y6wg3Cp1lx4aFdHmGOGmrW+EF5pW9XrIz4A+3kNapSyUsDyuMk8wejrJpRHNcFpIlyRkxgUU=\"}","encryptedOptions":"{\"iv\":\"ruAw1xvAVLh9D19ngrEDgw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"pl1JeMRamWBuScBb1QOT9eqheJG2KD3y8RjtoPhNVid90wmoOQDm6WGtwt+gz6QXQEWUmIIXt8lyAJTH7updSnceW1SihfDi7xMmPTOf/338uSt3RdA2q+F+skiT14gheXHMtSFQaeVGvS8QfDXQfJBY9zJYp+On\"}","encryptedCreationDate":"{\"iv\":\"H/eApLF+Ja7ebX/1tPg7+w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"8BhpHOrgM3L7XftckMdkXSFzXi2evkfheanKfcjMFxzYsg==\"}","encryptedForceAnswer":"{\"iv\":\"DapF8f4GhKPORoIrDPiRXg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"WfPqwu5yBkhrHJWR\"}","encryptedAnonymousUser":"{\"iv\":\"sIclbapCBCxkHi0QrlCqOA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"SXyhfk7DVgfVKfp7Kw==\"}","encryptedIsDateTime":"{\"iv\":\"CbB/QEzDlENL3qRK2ZjWxA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"TD0rnzzHaawAWyUP\"}","encryptedTimezone":"{\"iv\":\"wQQXNefWW5QC9VZ1KkQQmA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"5Zkx8f0WQAgBBG1s0DcxoHeA5Dc/fEI=\"}","version":"v0.3-0"}}';
|
||||
$user1Json = '{"user":{"encryptedName":"{\"iv\":\"wibexCADUTTMP8vjmegTwA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"r6Vu5fzAgvXctXaCx70/Pr1ldaOE\"}","encryptedSelections":"{\"iv\":\"gCMgC5Rie++L3s42RGzQJg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"jTmkMEkFTUiy8vGJinXAUmi7u8RNQ/FnLgnd+CMKL2XtqjeZuXgfMMDAdTEZnzNQTK7p/IhHBZrcrksUTnWkv68+f76msMs3rOqnYi7jhVL7O7NZMVNXysAgzalrQ+78Zz8TqoJ1qIARksTTCOi7Md07XKkYptCr3QUu0r8kfgk3KbGDuIE3tS4gGuB5CLKuPfFcbE0DjWAcr9IIEXpSPgjzJyEAx3bDd89ZfbRE0RaMoAR7Vqx+L3Hs6pXoUSbtnBJOQypNQYqUYycWA/kxCcuQEBlHwIR5qq7c9VsXNBG9SfGSA62scmbg6pxVXd3jEyTaxw3+B5r705mpMgAEY6NiJykob34x8LThdJP7XZKfe/tyczcKAlcQtJ7ocQsac0l1gRLK6eKHcNs8I3Zzi5iBzyqZtg0OVHzI8NiYpjwvg7piTHawsujAZIYkw/S4Pt29wkbb/heWpUsdJOF2xlfYYkTHnrPbX5jwZbNIgA==\"}","encryptedCreationDate":"{\"iv\":\"NGuUKkuLbabetQG5co01ZA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"ratk7+pQ14nax+slSf7ttOk2IIbQx7W2iu3I5VuUIR8RGg==\"}","version":"v0.3-0","poll":"gpwW7uZhbP"}}';
|
||||
$user2Json = '{"user":{"encryptedName":"{\"iv\":\"GsRvWloC3GYi+MoOCUQ1vg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"0upVYhihQhWZcWpXcx5xKGHOTKTcVptz\"}","encryptedSelections":"{\"iv\":\"1FQ0Bf91k3JQbr23AhTszg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"u6qAugU7o/gTUvjaBrSjlNsU1AJ5oIPiSqrXs7iC7117ss2iX0aENcwsGG09XUk+K1llyrGAI7Fp2uBqn6fyujpacJrJG5oO7SR7F8xwc5TpMlWp/CHN2C9VPdOnm8KhdDtt6IUbNV+McjBxa3FtNVttkF4FAtUGYSurrrEscRad7bvSVbYzYkMs+83xS/ui+pJ3NLuNPntfErRIJw3EKacaUfm2eHCftBVvPHTy3AQbJ9mSKy3tMch+qu1nLnyFSMKjRieCFOgkT3LkQcvfpSteV3V/UNfm82ERy7AYOB8KZ0hW1R/vDp2R+EjFS3/0cw+a8luW6HGcyY0fs18uIbsSUaLOiThKTjp9pYhupXEa9gz1DeZMC51M79Ha4YC9uy3AyG5hH29DYF5yhBPD1Z0iYcgosJ8TweiYN0AvlCYsy939VRSzFGeiI/ZFN76DF0YP1LAOK9bTXHN9n8oyDoQbBMKcY48/uWZZpCAvBw==\"}","encryptedCreationDate":"{\"iv\":\"gLvf5OObVV10vRrQbMcrDw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"sWHHRuInJDY=\",\"ct\":\"2S5gjZmZ8QQRPfovPpLTbvQLsurgNIHXhkG0Ze8OpTScCw==\"}","version":"v0.3-0","poll":"gpwW7uZhbP"}}';
|
||||
|
||||
$pollDir = 'tests/_tmp/data/' . $pollId . '/';
|
||||
$userDir = $pollDir . 'user/';
|
||||
mkdir($pollDir);
|
||||
file_put_contents($pollDir . 'poll_data', $pollJson);
|
||||
mkdir($userDir);
|
||||
file_put_contents($userDir . '0', $user1Json);
|
||||
file_put_contents($userDir . '1', $user2Json);
|
||||
|
||||
$I = new ApiTester($scenario);
|
||||
$I->wantTo('get an existing legacy (v0.3.0) poll with users');
|
||||
$I->sendGET('/polls/' . $pollId);
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->seeHttpHeader('Content-Type', 'application/json');
|
||||
$I->seeHttpHeader('Expires', '-1');
|
||||
$I->seeResponseIsJson();
|
||||
|
||||
$pollData = json_decode($pollJson, true)["poll"];
|
||||
unset($pollData["serverExpirationDate"]);
|
||||
unset($pollData["encryptedIsDateTime"]);
|
||||
foreach($pollData as $key => $value) {
|
||||
if (strpos($key, 'encrypted') === 0) {
|
||||
$key = lcfirst(substr($key, 9));
|
||||
}
|
||||
else {
|
||||
$key = $key;
|
||||
}
|
||||
$I->seeResponseContainsJson(
|
||||
array(
|
||||
'poll' => array(
|
||||
$key => $value
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$I->seeResponseContainsJson(["poll" => ["id" => $pollId]]);
|
||||
$I->dontSeeResponseJsonMatchesJsonPath('poll.serverExpirationDate');
|
||||
$I->seeResponseJsonMatchesJsonPath('poll.users');
|
||||
$users = $I->grabDataFromResponseByJsonPath('poll.users')[0];
|
||||
\PHPUnit_Framework_Assert::assertTrue(
|
||||
is_array($users),
|
||||
'user should be an array'
|
||||
);
|
||||
\PHPUnit_Framework_Assert::assertEquals(
|
||||
count($users),
|
||||
2,
|
||||
'user array should contain 2 users'
|
||||
);
|
||||
|
||||
function wellformUser($user) {
|
||||
$return = $user["user"];
|
||||
foreach ($return as $key => $value) {
|
||||
if(strpos($key, 'encrypted') === 0) {
|
||||
$return[lcfirst(substr($key, 9))] = $value;
|
||||
unset($return[$key]);
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
$I->seeResponseContainsJson([
|
||||
"poll" => [
|
||||
"users" => [
|
||||
wellformUser(json_decode($user1Json, true)),
|
||||
wellformUser(json_decode($user2Json, true))
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$I->seeResponseJsonMatchesJsonPath('poll.users.0.id');
|
||||
$I->seeResponseJsonMatchesJsonPath('poll.users.1.id');
|
||||
$user1Id = $I->grabDataFromResponseByJsonPath('poll.users.0.id')[0];
|
||||
$user2Id = $I->grabDataFromResponseByJsonPath('poll.users.1.id')[0];
|
||||
\PHPUnit_Framework_Assert::assertTrue(
|
||||
$user1Id !== $user2Id,
|
||||
'user ids are unique'
|
||||
);
|
||||
\PHPUnit_Framework_Assert::assertEquals(
|
||||
explode('_', $user1Id)[0],
|
||||
$pollId,
|
||||
'user id starts by poll id'
|
||||
);
|
|
@ -1,7 +1,24 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"anonymousUser":"{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}","answers":"{\"iv\":\"WRdAwEa0DF+E83ginLYtPw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"Oaer31ct2PXkmXkzJ1EXRPM3LMf6vGfzMZqjODwey4f7EhqSCUhYov+N7AZKCAAXYVS4WR84kKizxXBK2PQBSFrlB3Bll74ED9ZzRJSJD00otMG9BbgUR90aFws+1jMBP5vpti9+POsii85zLbDPkNg\/Th\/C4Ufv5YWwg\/4ZV0bFMyOgfdjtOWaG5YAMTGUIkz9U9+VCesYJQaTb497qTD\/Wmtz8J\/2pUxdL5\/b5xkdh2DJ4\/N5q0Kz\/CEbaoKwbexnQDlSr3ldlIhs7UmBjC9gkpgG2l9fu6a0VZFBE8hvzYrw=\"}","answerType":"{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO\/yFmk\"}","creationDate":"{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p\/ANinfanE\/51DbcDNw==\"}","description":"{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}","expirationDate":"{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}","forceAnswer":"{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}","options":"{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs\/Jm5XK\/thQW0phxKd0OxKt9NZ3FE\/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}","pollType":"{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}","timezone":"{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}","title":"{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}","version":"v0.3.0+0ae62f31","serverExpirationDate":"' . date("Y-m-dTH:i:s.000Z", strtotime("+3 month")) . '"}';
|
||||
$expirationDate = date("Y-m-dTH:i:s.000Z", strtotime("+3 month"));
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"anonymousUser": "{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}",
|
||||
"answerType": "{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO/yFmk\"}",
|
||||
"creationDate": "{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p/ANinfanE/51DbcDNw==\"}",
|
||||
"description": "{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}",
|
||||
"expirationDate": "{\"iv\":\"Y0O4n9+Tj+4LSmLoFTaNow==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"jCz8DFIS5eLI4tsjfpr+F4lG+F27BItHPdj85o5+gaDayA==\"}",
|
||||
"forceAnswer": "{\"iv\":\"P5Dg5Y9fS7EFxvqzP8u20A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"90G4jQ1PbalZyyzz\"}",
|
||||
"options": "{\"iv\":\"79HYzanMnjtgvBMowUWHaA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"HuFz0AFCpupdmXYdCcAX4OiwpMs/Jm5XK/thQW0phxKd0OxKt9NZ3FE/rMAiYVqRKBqFp+KLhBnbs9ewTFW0Xrvw6paTnvpY9Ftcz1MB\"}",
|
||||
"pollType": "{\"iv\":\"suOomfYe6kKBxjln091tCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"7iDQ2y571OBiJNxdaUY0PjqlgQ==\"}",
|
||||
"timezone": "{\"iv\":\"l0VeY3CPUvMtoDPrw7+iCw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"qBlHlZ0nLd3mqA==\"}",
|
||||
"title": "{\"iv\":\"szAOrvhM+bODnldJJP0pGw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"KwMkE7bneP0MX6hQEnM=\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"serverExpirationDate": "$expirationDate"
|
||||
}
|
||||
EOD;
|
||||
|
||||
mkdir('tests/_tmp/data/' . $pollId);
|
||||
file_put_contents('tests/_tmp/data/' . $pollId . '/poll_data', $pollJson);
|
||||
|
||||
|
|
|
@ -1,9 +1,41 @@
|
|||
<?php
|
||||
|
||||
$pollId = substr(md5(__FILE__), 0, 10);
|
||||
$pollJson = '{"anonymousUser":"{\"iv\":\"gVHZSXyMm10Fn+kDooa7uw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"GJsQQYA7TdAa+v3Rvg==\"}","answers":"{\"iv\":\"aK1JcI3viLPIlOO45K+ePA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"Bx4SRcww+hJ46NIiVcWBUZHADADX\/XPsxXMx4XzMQZWqu6M0690D4oTflSRJoqxe0egxdfMOUxuWhmACG\/UYXSYJQjcSg+QTq6KJbaXG+SvsCMZ7iz12a\/uf9lXyiag4IbLldgL4vE3LfZO6oih\/o\/yG4hechjNdSkqUa2IvsRbXWB2aHen6a5Ch5WjqWrr4xRRrukPvf7aumilT2Cf0LswHJ2fwYNilylV0h9oegKYp+qWphm4SL8x2ogRemSCt7u7ByEOwZV0w6D9bz9RvGLTRRLJaLIm\/VlE3k7R6Hz1vyps=\"}","answerType":"{\"iv\":\"ILkAzgUfAGNUtLr7CbEJEQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"nMOp+QApQGgP9dwefNpi\"}","creationDate":"{\"iv\":\"6tWbieK03uXUR+E0AMbs0A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"YkkLVBkFyx4xFldZ7qnDESG0teHJmXaPMUB05p9L0xUIMg==\"}","description":"{\"iv\":\"fWvHh47So4WBNfEHXrwLiA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"5W7nauOakSoFD52V\"}","expirationDate":"{\"iv\":\"HRsMvEQaoCp8QdqBGHevnA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"LXYamNRDyhIY5xY+CLqI4GHbocc9NoHQtePKU9fHpJn9zg==\"}","forceAnswer":"{\"iv\":\"bh4iZ4pKe0GnXcM764702g==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"q5VBynWGotXRrc2P\"}","options":"{\"iv\":\"ZneP\/x45NGh\/DC26GI4kvg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"4MvV9SNQq2dB6b\/MdX47R0KaRSfyZOZMEVUFDv7G3\/EcDBv7Z0pgSU9JXoF8BoSOz40rYrRtTw==\"}","pollType":"{\"iv\":\"j3P6eN0ZmNMMxLTAVD6gjQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"opwiZHAQi+I8R5HDxLfLK59DcQ==\"}","timezone":"{\"iv\":\"HKkSqcJONggGT9QQ+jZdUg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"BANN8sJlk8JK9A==\"}","title":"{\"iv\":\"4DX7dAJt7JIBHaR1V0Ct8A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"f1VUZf69nB94TF3\/HA==\"}","version":"v0.3.0+0ae62f31","serverExpirationDate":"' . date("Y-m-dTH:i:s.000Z", strtotime("+3 month")) . '"}';
|
||||
$user1Json = '{"name":"{\"iv\":\"GJXPSYYmTVfEsst31BD92w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"smbuxujRLF/xNS1syTWFguE=\"}","selections":"{\"iv\":\"WXlkCM3pGyD+SyIhccmHYg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UHs2ArTVTHfS04J3HNvwrV68xFqra0Q0qtVpLZRLPUbqgdXmn960FjEFiLa9Cnk5OcKEQtbOgWJXehHlAJFAWFpdyDE/gcCOKG62c2/hVauroeycQE16wDCjrEwor/FV9HxNjTbYxoJASjCy9ROLdOUhSlFfQfHLcvVpsTgPpnKPr7aYBgODu5XIdRI8Pf5nYF0K96KE9xn+mkg3ZjyXWSk1LBaBDpIOCrcj+8zl7tLtkgPNfh8aNVgQHC5hRrIbL9kZwD4XXEUPImRFITEy2rUWKp8Q0/jAgHCnqSzOLOFS8KrJOktDX++DjK3cB4oT5ttLvcRxRQ==\"}","creationDate":"{\"iv\":\"3/KUsITWzJNWx5fDzYC9Xg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"llgQE2GpZDg1ZKRRlXliGlF09VsrVZ1R57EIQ21+dej5yg==\"}","version":"v0.3.0+0ae62f31","poll":"l3zyFJUWcQ"}';
|
||||
$user2Json = '{"name":"{\"iv\":\"DVNTCOFfACEOrgtVNVMyww==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"eug7bstOm7T\/CCFs32o=\"}","selections":"{\"iv\":\"ubEuXoXzw4QFuzjAyvXC6w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"PfQ4v4hkBf+S0GX7JmnIp2LO5sh\/jg9nEIPn8NeU2Gn9Rb7cqsjCLgKOQ2xkiIzCyimVBOYg0fjGCyzM\/b6ZPQnY+86teNGogEteD4fjqGHhO832FNOy7Oci0YC8VAM1x9SlQNBI9V+vFc706JbZgwA8JY46UMiGK3HU49pgbYMpdnWEmt4dGzGrLMnNbh4J1Or5JydKmrp4dXaMiiggSXhmUTgBJSRhF7dxQm16oaA1lJpCWoQBvu+WTJv34LnBXHbgg6JcAEEONaQRw1jmMeqo36tQJxSdjiVfcDWzMifWiz\/nhQMqDHkc19iOAmDBo2Rf+yrGWA==\"}","creationDate":"{\"iv\":\"Sj4pVW\/maHa8DUNFHhyUrw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"HaY9MtEzVmEg3dxtI\/pfaIrsivBJSNeC5l5iJHQrvyYQGA==\"}","version":"v0.3.0+0ae62f31","poll":"l3zyFJUWcQ"}';
|
||||
$expirationDate = date("Y-m-dTH:i:s.000Z", strtotime("+3 month"));
|
||||
$pollJson = <<<EOD
|
||||
{
|
||||
"anonymousUser": "{\"iv\":\"gVHZSXyMm10Fn+kDooa7uw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"GJsQQYA7TdAa+v3Rvg==\"}",
|
||||
"answerType": "{\"iv\":\"ILkAzgUfAGNUtLr7CbEJEQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"nMOp+QApQGgP9dwefNpi\"}",
|
||||
"creationDate": "{\"iv\":\"6tWbieK03uXUR+E0AMbs0A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"YkkLVBkFyx4xFldZ7qnDESG0teHJmXaPMUB05p9L0xUIMg==\"}",
|
||||
"description": "{\"iv\":\"fWvHh47So4WBNfEHXrwLiA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"5W7nauOakSoFD52V\"}",
|
||||
"expirationDate": "{\"iv\":\"HRsMvEQaoCp8QdqBGHevnA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"LXYamNRDyhIY5xY+CLqI4GHbocc9NoHQtePKU9fHpJn9zg==\"}",
|
||||
"forceAnswer": "{\"iv\":\"bh4iZ4pKe0GnXcM764702g==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"q5VBynWGotXRrc2P\"}",
|
||||
"options": "{\"iv\":\"ZneP/x45NGh/DC26GI4kvg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"4MvV9SNQq2dB6b/MdX47R0KaRSfyZOZMEVUFDv7G3/EcDBv7Z0pgSU9JXoF8BoSOz40rYrRtTw==\"}",
|
||||
"pollType": "{\"iv\":\"j3P6eN0ZmNMMxLTAVD6gjQ==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"opwiZHAQi+I8R5HDxLfLK59DcQ==\"}",
|
||||
"timezone": "{\"iv\":\"HKkSqcJONggGT9QQ+jZdUg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"BANN8sJlk8JK9A==\"}",
|
||||
"title": "{\"iv\":\"4DX7dAJt7JIBHaR1V0Ct8A==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"f1VUZf69nB94TF3/HA==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"serverExpirationDate": "$expirationDate"
|
||||
}
|
||||
EOD;
|
||||
$user1Json = <<<EOD
|
||||
{
|
||||
"name": "{\"iv\":\"GJXPSYYmTVfEsst31BD92w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"smbuxujRLF/xNS1syTWFguE=\"}",
|
||||
"selections": "{\"iv\":\"WXlkCM3pGyD+SyIhccmHYg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"UHs2ArTVTHfS04J3HNvwrV68xFqra0Q0qtVpLZRLPUbqgdXmn960FjEFiLa9Cnk5OcKEQtbOgWJXehHlAJFAWFpdyDE/gcCOKG62c2/hVauroeycQE16wDCjrEwor/FV9HxNjTbYxoJASjCy9ROLdOUhSlFfQfHLcvVpsTgPpnKPr7aYBgODu5XIdRI8Pf5nYF0K96KE9xn+mkg3ZjyXWSk1LBaBDpIOCrcj+8zl7tLtkgPNfh8aNVgQHC5hRrIbL9kZwD4XXEUPImRFITEy2rUWKp8Q0/jAgHCnqSzOLOFS8KrJOktDX++DjK3cB4oT5ttLvcRxRQ==\"}",
|
||||
"creationDate": "{\"iv\":\"3/KUsITWzJNWx5fDzYC9Xg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"llgQE2GpZDg1ZKRRlXliGlF09VsrVZ1R57EIQ21+dej5yg==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"poll": "l3zyFJUWcQ"
|
||||
}
|
||||
EOD;
|
||||
$user2Json = <<<EOD
|
||||
{
|
||||
"name": "{\"iv\":\"DVNTCOFfACEOrgtVNVMyww==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"eug7bstOm7T/CCFs32o=\"}",
|
||||
"selections": "{\"iv\":\"ubEuXoXzw4QFuzjAyvXC6w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"PfQ4v4hkBf+S0GX7JmnIp2LO5sh/jg9nEIPn8NeU2Gn9Rb7cqsjCLgKOQ2xkiIzCyimVBOYg0fjGCyzM/b6ZPQnY+86teNGogEteD4fjqGHhO832FNOy7Oci0YC8VAM1x9SlQNBI9V+vFc706JbZgwA8JY46UMiGK3HU49pgbYMpdnWEmt4dGzGrLMnNbh4J1Or5JydKmrp4dXaMiiggSXhmUTgBJSRhF7dxQm16oaA1lJpCWoQBvu+WTJv34LnBXHbgg6JcAEEONaQRw1jmMeqo36tQJxSdjiVfcDWzMifWiz/nhQMqDHkc19iOAmDBo2Rf+yrGWA==\"}",
|
||||
"creationDate": "{\"iv\":\"Sj4pVW/maHa8DUNFHhyUrw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"mhO9ROu+dr4=\",\"ct\":\"HaY9MtEzVmEg3dxtI/pfaIrsivBJSNeC5l5iJHQrvyYQGA==\"}",
|
||||
"version": "v0.3.0+0ae62f31",
|
||||
"poll": "l3zyFJUWcQ"
|
||||
}
|
||||
EOD;
|
||||
|
||||
$pollDir = 'tests/_tmp/data/' . $pollId . '/';
|
||||
$userDir = $pollDir . 'user/';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
$pollTemplate = array(
|
||||
"anonymousUser" => "{\"iv\":\"SOqei2Y7QZt1PFR6IXR4qg==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"WAg0oSjCiMAO+JqzIg==\"}",
|
||||
"answers" => "{\"iv\":\"WRdAwEa0DF+E83ginLYtPw==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"Oaer31ct2PXkmXkzJ1EXRPM3LMf6vGfzMZqjODwey4f7EhqSCUhYov+N7AZKCAAXYVS4WR84kKizxXBK2PQBSFrlB3Bll74ED9ZzRJSJD00otMG9BbgUR90aFws+1jMBP5vpti9+POsii85zLbDPkNg\/Th\/C4Ufv5YWwg\/4ZV0bFMyOgfdjtOWaG5YAMTGUIkz9U9+VCesYJQaTb497qTD\/Wmtz8J\/2pUxdL5\/b5xkdh2DJ4\/N5q0Kz\/CEbaoKwbexnQDlSr3ldlIhs7UmBjC9gkpgG2l9fu6a0VZFBE8hvzYrw=\"}",
|
||||
"answerType" => "{\"iv\":\"z1V+GmSWJxSng0bXxnYNRA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ZDf5sBxR6rO+DdO\/yFmk\"}",
|
||||
"creationDate" => "{\"iv\":\"DBKid4Yiyr61GVLigJj20w==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"ORRPzySTa6vt7GQrJOGBvNZXXq4p\/ANinfanE\/51DbcDNw==\"}",
|
||||
"description" => "{\"iv\":\"aohDHKaO7c7Fl5vIueBkcA==\",\"v\":1,\"iter\":1000,\"ks\":128,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"3gtpUTAyVK4=\",\"ct\":\"+ygmsnYAsEBLZRUV\"}",
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import RESTAdapter from '@ember-data/adapter/rest';
|
||||
import { inject as service } from '@ember/service';
|
||||
import AdapterFetch from 'ember-fetch/mixins/adapter-fetch';
|
||||
|
||||
export default class ApplicationAdapter extends RESTAdapter.extend(AdapterFetch) {
|
||||
@service
|
||||
encryption;
|
||||
|
||||
// set namespace to api.php in same subdirectory
|
||||
namespace =
|
||||
window.location.pathname
|
||||
// remove index.html if it's there
|
||||
.replace(/index.html$/, '')
|
||||
// remove tests prefix which is added by testem (starting with a number)
|
||||
.replace(/\/\d+\/tests/, '')
|
||||
// remove tests prefix which is added by tests run in browser
|
||||
.replace(/tests/, '')
|
||||
// remove leading and trailing slash
|
||||
.replace(/\/$/, '')
|
||||
// add api.php
|
||||
.concat('/api/index.php')
|
||||
// remove leading slash
|
||||
.replace(/^\//g, '')
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import Application from '@ember/application';
|
||||
import Resolver from 'ember-resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from './config/environment';
|
||||
import config from 'croodle/config/environment';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
|
@ -1,15 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import Component from '@ember/component';
|
||||
|
||||
@classic
|
||||
export default class AutofocusableElement extends Component {
|
||||
autofocus = true;
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
|
||||
if (this.autofocus) {
|
||||
this.element.focus();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
<BsButton
|
||||
@disabled={{@disabled}}
|
||||
@onClick={{@onClick}}
|
||||
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__prev-button prev"
|
||||
data-test-action="back"
|
||||
...attributes
|
||||
>
|
||||
<span class="cr-steps-bottom-nav__icon oi oi-caret-left" title={{t "action.back"}} aria-hidden="true"></span>
|
||||
<span
|
||||
class="cr-steps-bottom-nav__icon oi oi-caret-left"
|
||||
title={{t "action.back"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="cr-steps-bottom-nav__label">
|
||||
{{t "action.back"}}
|
||||
</span>
|
16
app/components/back-button.ts
Normal file
16
app/components/back-button.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
|
||||
interface BackButtonSignature {
|
||||
Args: { onClick?: () => void };
|
||||
Element: HTMLButtonElement;
|
||||
}
|
||||
|
||||
const BackButton = templateOnlyComponent<BackButtonSignature>();
|
||||
|
||||
export default BackButton;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
BackButton: typeof BackButton;
|
||||
}
|
||||
}
|
28
app/components/bs-form.js
Normal file
28
app/components/bs-form.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import BaseBsForm from 'ember-bootstrap/components/bs-form';
|
||||
import IntlMessage from '../utils/intl-message';
|
||||
|
||||
export default class BsForm extends BaseBsForm {
|
||||
'__ember-bootstrap_subclass' = true;
|
||||
|
||||
get hasValidator() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async validate(model) {
|
||||
const isInvalid = Object.getOwnPropertyNames(
|
||||
Object.getPrototypeOf(model),
|
||||
).some((potentialValidationKey) => {
|
||||
// Validation getters must be named `propertyValidation` by our convention
|
||||
if (!potentialValidationKey.endsWith('Validation')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validation errors must be an instance of IntlMessage by convention
|
||||
return model[potentialValidationKey] instanceof IntlMessage;
|
||||
});
|
||||
|
||||
if (isInvalid) {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
26
app/components/bs-form/element.js
Normal file
26
app/components/bs-form/element.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import BaseBsFormElement from 'ember-bootstrap/components/bs-form/element';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class BsFormElement extends BaseBsFormElement {
|
||||
'__ember-bootstrap_subclass' = true;
|
||||
|
||||
@service intl;
|
||||
|
||||
get errors() {
|
||||
// native validation state doesn't integrate with Ember's autotracking, so we need to invalidate our `errors` getter explicitly when
|
||||
// `this.value` changes by consuming it here.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { model, property } = this.args;
|
||||
const validation = model[`${property}Validation`];
|
||||
|
||||
if (validation === undefined || validation === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [this.intl.t(validation.key, validation.options)];
|
||||
}
|
||||
|
||||
get hasValidator() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import BaseBsInput from 'ember-bootstrap/components/bs-form/element/control/input';
|
||||
|
||||
@classic
|
||||
export default class CustomizedBsInput extends BaseBsInput {
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
|
||||
if (this.autofocus) {
|
||||
this.element.focus();
|
||||
}
|
||||
}
|
||||
}
|
42
app/components/create-index.hbs
Normal file
42
app/components/create-index.hbs
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="cr-form-wrapper box">
|
||||
<BsForm
|
||||
@formLayout="horizontal"
|
||||
@model={{@formData}}
|
||||
@onSubmit={{this.submit}}
|
||||
as |form|
|
||||
>
|
||||
<form.element
|
||||
@label={{t "create.index.input.pollType.label"}}
|
||||
@property="pollType"
|
||||
@showValidationOn={{array "change" "focusOut"}}
|
||||
@useIcons={{false}}
|
||||
class="poll-type"
|
||||
data-test-form-element="poll-type"
|
||||
as |el|
|
||||
>
|
||||
<select
|
||||
id={{el.id}}
|
||||
class="form-control"
|
||||
required
|
||||
{{on "change" (pick "target.value" el.setValue)}}
|
||||
{{autofocus}}
|
||||
>
|
||||
<option value="FindADate" selected={{eq el.value "FindADate"}}>
|
||||
{{t "pollTypes.findADate.label"}}
|
||||
</option>
|
||||
<option value="MakeAPoll" selected={{eq el.value "MakeAPoll"}}>
|
||||
{{t "pollTypes.makeAPoll.label"}}
|
||||
</option>
|
||||
</select>
|
||||
</form.element>
|
||||
|
||||
<div class="row cr-steps-bottom-nav">
|
||||
<div class="col-6 col-md-8 order-12">
|
||||
<NextButton />
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton disabled />
|
||||
</div>
|
||||
</div>
|
||||
</BsForm>
|
||||
</div>
|
38
app/components/create-index.ts
Normal file
38
app/components/create-index.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { registerDestructor } from '@ember/destroyable';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { CreateIndexRouteModel } from '../routes/create/index';
|
||||
|
||||
export interface CreateIndexSignature {
|
||||
Args: {
|
||||
formData: CreateIndexRouteModel['formData'];
|
||||
poll: CreateIndexRouteModel['poll'];
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateIndexComponent extends Component<CreateIndexSignature> {
|
||||
@service declare router: RouterService;
|
||||
|
||||
@action
|
||||
submit() {
|
||||
this.router.transitionTo('create.meta');
|
||||
}
|
||||
|
||||
constructor(owner: unknown, args: CreateIndexSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
registerDestructor(this, () => {
|
||||
const { poll, formData } = this.args;
|
||||
|
||||
poll.pollType = formData.pollType;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateIndex: typeof CreateIndexComponent;
|
||||
}
|
||||
}
|
45
app/components/create-meta.hbs
Normal file
45
app/components/create-meta.hbs
Normal file
|
@ -0,0 +1,45 @@
|
|||
<div class="cr-form-wrapper box">
|
||||
<BsForm
|
||||
@formLayout="horizontal"
|
||||
@model={{@formData}}
|
||||
@onInvalid={{(scroll-first-invalid-element-into-view-port)}}
|
||||
@onSubmit={{this.submit}}
|
||||
novalidate
|
||||
as |form|
|
||||
>
|
||||
<form.element
|
||||
@controlType="text"
|
||||
@label={{t "create.meta.input.title.label"}}
|
||||
@property="title"
|
||||
class="title"
|
||||
data-test-form-element="title"
|
||||
as |el|
|
||||
>
|
||||
<el.control
|
||||
placeholder={{t "create.meta.input.title.placeholder"}}
|
||||
{{autofocus}}
|
||||
/>
|
||||
</form.element>
|
||||
<form.element
|
||||
@controlType="textarea"
|
||||
@label={{t "create.meta.input.description.label"}}
|
||||
@property="description"
|
||||
class="description"
|
||||
data-test-form-element="description"
|
||||
as |el|
|
||||
>
|
||||
<el.control
|
||||
placeholder={{t "create.meta.input.description.placeholder"}}
|
||||
/>
|
||||
</form.element>
|
||||
|
||||
<div class="row cr-steps-bottom-nav">
|
||||
<div class="col-6 col-md-8 order-12">
|
||||
<NextButton />
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton @onClick={{this.previousPage}} />
|
||||
</div>
|
||||
</div>
|
||||
</BsForm>
|
||||
</div>
|
44
app/components/create-meta.ts
Normal file
44
app/components/create-meta.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { registerDestructor } from '@ember/destroyable';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { CreateMetaRouteModel } from '../routes/create/meta';
|
||||
|
||||
export interface CreateMetaSignature {
|
||||
Args: {
|
||||
formData: CreateMetaRouteModel['formData'];
|
||||
poll: CreateMetaRouteModel['poll'];
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateMetaComponent extends Component<CreateMetaSignature> {
|
||||
@service declare router: RouterService;
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.router.transitionTo('create.index');
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
this.router.transitionTo('create.options');
|
||||
}
|
||||
|
||||
constructor(owner: unknown, args: CreateMetaSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
registerDestructor(this, () => {
|
||||
const { poll, formData } = this.args;
|
||||
|
||||
poll.title = formData.title;
|
||||
poll.description = formData.description;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateMeta: typeof CreateMetaComponent;
|
||||
}
|
||||
}
|
|
@ -1,37 +1,35 @@
|
|||
{{#let @form as |form|}}
|
||||
<form.element
|
||||
{{#let @formElement as |FormElement|}}
|
||||
<FormElement
|
||||
@label={{t "create.options.dates.label"}}
|
||||
@property="options"
|
||||
data-test-form-element-for="days"
|
||||
as |el|
|
||||
>
|
||||
<div
|
||||
class="
|
||||
form-control
|
||||
cr-h-auto
|
||||
cr-pr-validation
|
||||
{{if (eq el.validation "error") "is-invalid"}}
|
||||
{{if (eq el.validation "success") "is-valid"}}
|
||||
class="form-control cr-h-auto cr-pr-validation
|
||||
{{if (eq el.validation 'error') 'is-invalid'}}
|
||||
{{if (eq el.validation 'success') 'is-valid'}}
|
||||
"
|
||||
id={{el.id}}
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<InlineDatepicker
|
||||
@center={{this.calendarCenter}}
|
||||
@selectedDays={{this.selectedDays}}
|
||||
@onCenterChange={{action (mut this.calendarCenter) value="moment"}}
|
||||
@onSelect={{action "daysSelected"}}
|
||||
@onCenterChange={{fn this.handleCalenderCenterChange 0}}
|
||||
@onSelect={{this.handleSelectedDaysChange}}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 cr-hide-on-mobile">
|
||||
<InlineDatepicker
|
||||
@center={{this.calendarCenterNext}}
|
||||
@selectedDays={{this.selectedDays}}
|
||||
@onCenterChange={{action (mut this.calendarCenter) value="moment"}}
|
||||
@onSelect={{action "daysSelected"}}
|
||||
@onCenterChange={{fn this.handleCalenderCenterChange -1}}
|
||||
@onSelect={{this.handleSelectedDaysChange}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form.element>
|
||||
</FormElement>
|
||||
{{/let}}
|
|
@ -1,87 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import { isArray } from '@ember/array';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import moment from 'moment';
|
||||
|
||||
@classic
|
||||
export default class CreateOptionsDates extends Component {
|
||||
@service('store')
|
||||
store;
|
||||
|
||||
@computed('options.[]')
|
||||
get selectedDays() {
|
||||
return this.options
|
||||
// should be unique
|
||||
.uniqBy('day')
|
||||
// raw dates
|
||||
.map(({ date }) => date)
|
||||
// filter out invalid
|
||||
.filter(moment.isMoment)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
@computed('calendarCenter')
|
||||
get calendarCenterNext() {
|
||||
return moment(this.calendarCenter).add(1, 'months');
|
||||
}
|
||||
|
||||
@action
|
||||
daysSelected({ moment: newMoments }) {
|
||||
let { options } = this;
|
||||
|
||||
if (!isArray(newMoments)) {
|
||||
// special case: all options are unselected
|
||||
options.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// array of options that represent days missing in updated selection
|
||||
let removedOptions = options.filter((option) => {
|
||||
return !newMoments.find((newMoment) => newMoment.format('YYYY-MM-DD') === option.day);
|
||||
});
|
||||
|
||||
// array of moments that aren't represented yet by an option
|
||||
let addedMoments = newMoments.filter((moment) => {
|
||||
return !options.find((option) => moment.format('YYYY-MM-DD') === option.day);
|
||||
});
|
||||
|
||||
// remove options that represent deselected days
|
||||
options.removeObjects(removedOptions);
|
||||
|
||||
// add options for newly selected days
|
||||
let newOptions = addedMoments.map((moment) => {
|
||||
return this.store.createFragment('option', {
|
||||
title: moment.format('YYYY-MM-DD'),
|
||||
})
|
||||
});
|
||||
newOptions.forEach((newOption) => {
|
||||
// options must be insert into options array at correct position
|
||||
let insertBefore = options.find(({ date }) => {
|
||||
if (!moment.isMoment(date)) {
|
||||
// ignore options that do not represent a valid date
|
||||
return false;
|
||||
}
|
||||
|
||||
return date.isAfter(newOption.date);
|
||||
});
|
||||
let position = isPresent(insertBefore) ? options.indexOf(insertBefore) : options.length;
|
||||
options.insertAt(position, newOption);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
updateCalenderCenter(diff) {
|
||||
this.calendarCenter.add(diff, 'months');
|
||||
this.notifyPropertyChange('calenderCenter');
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(arguments);
|
||||
|
||||
let { selectedDays } = this;
|
||||
this.set('calendarCenter', selectedDays.length >= 1 ? selectedDays[0] : moment());
|
||||
}
|
||||
}
|
63
app/components/create-options-dates.ts
Normal file
63
app/components/create-options-dates.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { DateTime } from 'luxon';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import type { FormDataOption } from './create-options';
|
||||
import type BsFormElementComponent from 'ember-bootstrap/components/bs-form/element';
|
||||
|
||||
export interface CreateOptionsDatesSignature {
|
||||
Args: {
|
||||
formElement: BsFormElementComponent;
|
||||
options: Array<FormDataOption>;
|
||||
updateOptions: (options: string[]) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateOptionsDates extends Component<CreateOptionsDatesSignature> {
|
||||
@tracked calendarCenter =
|
||||
this.selectedDays.length >= 1
|
||||
? (this.selectedDays[0] as DateTime)
|
||||
: DateTime.local();
|
||||
|
||||
get selectedDays(): DateTime[] {
|
||||
return this.args.options.map(
|
||||
({ value }) => DateTime.fromISO(value) as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
get calendarCenterNext() {
|
||||
return this.calendarCenter.plus({ months: 1 });
|
||||
}
|
||||
|
||||
@action
|
||||
handleSelectedDaysChange({
|
||||
datetime: newDatesAsLuxonDateTime,
|
||||
}: {
|
||||
datetime: DateTime[];
|
||||
}) {
|
||||
if (!isArray(newDatesAsLuxonDateTime)) {
|
||||
// special case: all options are unselected
|
||||
this.args.updateOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.args.updateOptions(
|
||||
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate() as string),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
handleCalenderCenterChange(
|
||||
offset: number,
|
||||
{ datetime }: { datetime: DateTime },
|
||||
) {
|
||||
this.calendarCenter = datetime.plus({ months: offset });
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateOptionsDates: typeof CreateOptionsDates;
|
||||
}
|
||||
}
|
127
app/components/create-options-datetime.hbs
Normal file
127
app/components/create-options-datetime.hbs
Normal file
|
@ -0,0 +1,127 @@
|
|||
<div class="cr-form-wrapper box">
|
||||
{{#if this.errorMessage}}
|
||||
<BsAlert @type="warning">
|
||||
{{t this.errorMessage}}
|
||||
</BsAlert>
|
||||
{{/if}}
|
||||
|
||||
<BsForm
|
||||
@onInvalid={{(scroll-first-invalid-element-into-view-port)}}
|
||||
@onSubmit={{this.submit}}
|
||||
@formLayout="horizontal"
|
||||
@model={{this.formData}}
|
||||
novalidate
|
||||
as |form|
|
||||
>
|
||||
<div class="days">
|
||||
{{#each-in this.formData.datetimes as |date timeOptions|}}
|
||||
{{!--
|
||||
@glint-ignore
|
||||
Types for value returned by `{{#each-in}}` are broken if used
|
||||
with a `Map`. https://github.com/typed-ember/glint/issues/645
|
||||
--}}
|
||||
{{#each timeOptions as |timeOption indexInTimeOptions|}}
|
||||
<div data-test-day={{date}}>
|
||||
<form.element
|
||||
@label={{format-date timeOption.jsDate dateStyle="full"}}
|
||||
{{!
|
||||
show label only for the first time of this date
|
||||
}}
|
||||
@invisibleLabel={{gt indexInTimeOptions 0}}
|
||||
@model={{timeOption}}
|
||||
@property="time"
|
||||
class="option"
|
||||
as |el|
|
||||
>
|
||||
<div class="input-group">
|
||||
<el.control
|
||||
@placeholder="00:00"
|
||||
@type="time"
|
||||
@value={{el.value}}
|
||||
{{! focus input if it's the first one }}
|
||||
{{autofocus enabled=timeOption.isFirstTimeOnFirstDate}}
|
||||
{{! run validation for partially filled input on focusout event }}
|
||||
{{on "focusout" (fn this.validateInput timeOption)}}
|
||||
{{on "change" (fn this.validateInput timeOption)}}
|
||||
{{!
|
||||
Validation for partially input field must be reset if input is cleared.
|
||||
But `@onChange` is not called and `focusout` event not triggered in that
|
||||
scenario. Need to listen to additional events to ensure that partially
|
||||
input validation is updated as soon as user fixed a partially input.
|
||||
The `keyup` events captures all scenarios in which the input is cleared
|
||||
using keyboard. `focusin` event is triggered if user clicks the clears
|
||||
button provided by native input. As a fallback validation is rerun on
|
||||
`focusout`.
|
||||
As the time of implementation this was only affecting Chrome cause
|
||||
Firefox does not consider partially time input as invalid, Edge prevents
|
||||
partially filling in first place and Desktop Safari as well as IE 11
|
||||
do not support `<input type="time">`.
|
||||
}}
|
||||
{{on "focusin" (fn this.updateInputValidation timeOption)}}
|
||||
{{on "keyup" (fn this.updateInputValidation timeOption)}}
|
||||
id={{el.id}}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<BsButton
|
||||
@onClick={{fn this.formData.deleteOption timeOption}}
|
||||
@type="link"
|
||||
class="delete"
|
||||
data-test-action="delete"
|
||||
>
|
||||
<span
|
||||
class="oi oi-trash"
|
||||
title={{t "create.options.button.delete.label"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">
|
||||
{{t "create.options.button.delete.label"}}
|
||||
</span>
|
||||
</BsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BsButton
|
||||
@onClick={{fn this.formData.addOption date}}
|
||||
@type="link"
|
||||
@size="sm"
|
||||
class="add cr-option-menu__button cr-option-menu__add-button float-left"
|
||||
data-test-action="add"
|
||||
>
|
||||
<span
|
||||
class="oi oi-plus"
|
||||
title={{t "create.options.button.add.label"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">{{t
|
||||
"create.options.button.add.label"
|
||||
}}</span>
|
||||
</BsButton>
|
||||
</form.element>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
|
||||
{{#if this.formData.hasMultipleDays}}
|
||||
<form.element>
|
||||
<BsButton
|
||||
@onClick={{this.adoptTimesOfFirstDay}}
|
||||
@size="sm"
|
||||
class="adopt-times-of-first-day"
|
||||
data-test-action="adopt-times-of-first-day"
|
||||
>
|
||||
{{t "create.options-datetime.copy-first-line"}}
|
||||
</BsButton>
|
||||
</form.element>
|
||||
{{/if}}
|
||||
|
||||
<div class="row cr-steps-bottom-nav">
|
||||
<div class="col-6 col-md-8 order-12">
|
||||
<NextButton />
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton @onClick={{this.previousPage}} />
|
||||
</div>
|
||||
</div>
|
||||
</BsForm>
|
||||
</div>
|
|
@ -1,190 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
import { action, get } from '@ember/object';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
import { raw } from 'ember-awesome-macros';
|
||||
import { groupBy } from 'ember-awesome-macros/array';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
let modelValidations = buildValidations({
|
||||
dates: [
|
||||
validator('collection', true),
|
||||
validator('length', {
|
||||
dependentKeys: ['model.datetimes.[]'],
|
||||
min: 1
|
||||
}),
|
||||
validator('valid-collection', {
|
||||
dependentKeys: ['model.datetimes.[]', 'model.datetimes.@each.time']
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default class CreateOptionsDatetime extends Component.extend(modelValidations) {
|
||||
@service
|
||||
store;
|
||||
|
||||
errorMesage = null;
|
||||
|
||||
// group dates by day
|
||||
@groupBy('dates', raw('day'))
|
||||
groupedDates;
|
||||
|
||||
get datesForFirstDay() {
|
||||
// dates are sorted
|
||||
let firstDay = this.groupedDates[0];
|
||||
return firstDay.items;
|
||||
}
|
||||
|
||||
get timesForFirstDay() {
|
||||
return this.datesForFirstDay.map((date) => date.time).filter((time) => isPresent(time));
|
||||
}
|
||||
|
||||
@action
|
||||
addOption(afterOption) {
|
||||
let options = this.dates;
|
||||
let dayString = afterOption.get('day');
|
||||
let fragment = this.store.createFragment('option', {
|
||||
title: dayString
|
||||
});
|
||||
let position = options.indexOf(afterOption) + 1;
|
||||
options.insertAt(
|
||||
position,
|
||||
fragment
|
||||
);
|
||||
|
||||
next(() => {
|
||||
this.notifyPropertyChange('_nestedChildViews');
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
const dates = this.dates;
|
||||
const datesForFirstDay = this.datesForFirstDay;
|
||||
const timesForFirstDay = this.timesForFirstDay;
|
||||
const datesWithoutFirstDay = this.groupedDates.slice(1);
|
||||
|
||||
/* validate if times on firstDay are valid */
|
||||
const datesForFirstDayAreValid = datesForFirstDay.every((date) => {
|
||||
// ignore dates where time is null
|
||||
return isEmpty(date.get('time')) || date.get('validations.isValid');
|
||||
});
|
||||
|
||||
if (!datesForFirstDayAreValid) {
|
||||
this.set('errorMessage', 'create.options-datetime.fix-validation-errors-first-day');
|
||||
return;
|
||||
}
|
||||
|
||||
datesWithoutFirstDay.forEach(({ items }) => {
|
||||
if (isEmpty(timesForFirstDay)) {
|
||||
// there aren't any times on first day
|
||||
const remainingOption = items[0];
|
||||
// remove all times but the first one
|
||||
dates.removeObjects(
|
||||
items.slice(1)
|
||||
);
|
||||
// set title as date without time
|
||||
remainingOption.set('title', remainingOption.get('date').format('YYYY-MM-DD'));
|
||||
} else {
|
||||
// adopt times of first day
|
||||
if (timesForFirstDay.get('length') < items.length) {
|
||||
// remove excess options
|
||||
dates.removeObjects(
|
||||
items.slice(timesForFirstDay.get('length'))
|
||||
);
|
||||
}
|
||||
// set times according to first day
|
||||
let targetPosition;
|
||||
timesForFirstDay.forEach((timeOfFirstDate, index) => {
|
||||
const target = items[index];
|
||||
if (target === undefined) {
|
||||
const basisDate = get(items[0], 'date').clone();
|
||||
let [hour, minute] = timeOfFirstDate.split(':');
|
||||
let dateString = basisDate.hour(hour).minute(minute).toISOString();
|
||||
let fragment = this.store.createFragment('option', {
|
||||
title: dateString
|
||||
});
|
||||
dates.insertAt(
|
||||
targetPosition,
|
||||
fragment
|
||||
);
|
||||
targetPosition++;
|
||||
} else {
|
||||
target.set('time', timeOfFirstDate);
|
||||
targetPosition = dates.indexOf(target) + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* removes target option if it's not the only date for this day
|
||||
* otherwise it deletes time for this date
|
||||
*/
|
||||
@action
|
||||
deleteOption(target) {
|
||||
let position = this.dates.indexOf(target);
|
||||
let datesForThisDay = this.groupedDates.find((group) => {
|
||||
return group.value === target.get('day');
|
||||
}).items;
|
||||
if (datesForThisDay.length > 1) {
|
||||
this.dates.removeAt(position);
|
||||
} else {
|
||||
target.set('time', null);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.onPrevPage();
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
if (this.get('validations.isValid')) {
|
||||
this.onNextPage();
|
||||
} else {
|
||||
this.set('shouldShowErrors', true);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
inputChanged(date, value) {
|
||||
// update property, which is normally done by default
|
||||
date.set('time', value);
|
||||
|
||||
// reset partially filled state
|
||||
date.set('isPartiallyFilled', false);
|
||||
|
||||
// reset error message
|
||||
this.set('errorMessage', null);
|
||||
}
|
||||
|
||||
// validate input field for being partially filled
|
||||
@action
|
||||
validateInput(date, event) {
|
||||
let element = event.target;
|
||||
|
||||
// update partially filled time validation error
|
||||
if (!element.checkValidity()) {
|
||||
date.set('isPartiallyFilled', true);
|
||||
} else {
|
||||
date.set('isPartiallyFilled', false);
|
||||
}
|
||||
}
|
||||
|
||||
// remove partially filled validation error if user fixed it
|
||||
@action
|
||||
updateInputValidation(date, event) {
|
||||
let element = event.target;
|
||||
|
||||
if (element.checkValidity() && date.isPartiallyFilled) {
|
||||
date.set('isPartiallyFilled', false);
|
||||
}
|
||||
}
|
||||
}
|
285
app/components/create-options-datetime.ts
Normal file
285
app/components/create-options-datetime.ts
Normal file
|
@ -0,0 +1,285 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { TrackedMap, TrackedSet } from 'tracked-built-ins';
|
||||
import { DateTime } from 'luxon';
|
||||
import IntlMessage from '../utils/intl-message';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { CreateOptionsDatetimeRouteModel } from 'croodle/routes/create/options-datetime';
|
||||
|
||||
class FormDataTimeOption {
|
||||
formData;
|
||||
|
||||
// ISO 8601 date string: YYYY-MM-DD
|
||||
date: string;
|
||||
|
||||
// ISO 8601 time string without seconds: HH:mm
|
||||
@tracked time: string | null;
|
||||
|
||||
// helper property set by modifiers to track if input element is invalid
|
||||
// because user only entered the time partly (e.g. "10:--").
|
||||
@tracked isPartiallyFilled = false;
|
||||
|
||||
get timeValidation() {
|
||||
const { isPartiallyFilled } = this;
|
||||
if (isPartiallyFilled) {
|
||||
return new IntlMessage(
|
||||
'create.options-datetime.error.partiallyFilledTime',
|
||||
);
|
||||
}
|
||||
|
||||
// The same time must not be entered twice for a day.
|
||||
// It should show a validation error if the same time has been entered for
|
||||
// the same day already before. Only the second input field containing the
|
||||
// duplicated time should show the validation error.
|
||||
const { formData, date } = this;
|
||||
const timesForThisDate = Array.from(formData.datetimes.get(date)!);
|
||||
const isDuplicate = timesForThisDate
|
||||
.slice(0, timesForThisDate.indexOf(this))
|
||||
.some((timeOption) => timeOption.time == this.time);
|
||||
if (isDuplicate) {
|
||||
return new IntlMessage('create.options-datetime.error.duplicatedDate');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get datetime() {
|
||||
const { date, time } = this;
|
||||
const isoString = time === null ? date : `${date}T${time}`;
|
||||
return DateTime.fromISO(isoString);
|
||||
}
|
||||
|
||||
get jsDate() {
|
||||
const { datetime } = this;
|
||||
return datetime.toJSDate();
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
const { timeValidation } = this;
|
||||
return timeValidation === null;
|
||||
}
|
||||
|
||||
get isFirstTimeOnFirstDate() {
|
||||
const { formData, date } = this;
|
||||
const { datetimes } = formData;
|
||||
return (
|
||||
Array.from(datetimes.keys())[0] === date &&
|
||||
Array.from(datetimes.get(date)!)[0] === this
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
formData: FormData,
|
||||
{ date, time }: { date: string; time: string | null },
|
||||
) {
|
||||
this.formData = formData;
|
||||
this.date = date;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
class FormData {
|
||||
@tracked datetimes: Map<string, Set<FormDataTimeOption>>;
|
||||
|
||||
get optionsValidation() {
|
||||
const { datetimes } = this;
|
||||
const allTimeOptionsAreValid = Array.from(datetimes.values()).every(
|
||||
(timeOptionsForDate) =>
|
||||
Array.from(timeOptionsForDate).every(
|
||||
(timeOption) => timeOption.isValid,
|
||||
),
|
||||
);
|
||||
if (!allTimeOptionsAreValid) {
|
||||
return new IntlMessage('create.options-datetime.error.invalidTime');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get hasMultipleDays() {
|
||||
return this.datetimes.size > 1;
|
||||
}
|
||||
|
||||
get validationStatePerDate() {
|
||||
const validationState: Map<string, boolean> = new Map();
|
||||
|
||||
for (const [date, timeOptions] of this.datetimes.entries()) {
|
||||
validationState.set(
|
||||
date,
|
||||
Array.from(timeOptions).every((time) => time.isValid),
|
||||
);
|
||||
}
|
||||
|
||||
return validationState;
|
||||
}
|
||||
|
||||
@action
|
||||
addOption(date: string) {
|
||||
this.datetimes
|
||||
.get(date)!
|
||||
.add(new FormDataTimeOption(this, { date, time: null }));
|
||||
}
|
||||
|
||||
/*
|
||||
* removes target option if it's not the only time for this date
|
||||
* otherwise it deletes time for this date
|
||||
*/
|
||||
@action
|
||||
deleteOption(option: FormDataTimeOption) {
|
||||
const timeOptionsForDate = this.datetimes.get(option.date)!;
|
||||
|
||||
if (timeOptionsForDate.size > 1) {
|
||||
timeOptionsForDate.delete(option);
|
||||
} else {
|
||||
option.time = null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
const timeOptionsForFirstDay = Array.from(
|
||||
Array.from(this.datetimes.values())[0]!,
|
||||
) as FormDataTimeOption[];
|
||||
const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
|
||||
(timeOption) => timeOption.isValid,
|
||||
);
|
||||
if (!timesForFirstDayAreValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const date of Array.from(this.datetimes.keys()).slice(1)) {
|
||||
this.datetimes.set(
|
||||
date,
|
||||
new TrackedSet(
|
||||
timeOptionsForFirstDay.map(
|
||||
({ time }) => new FormDataTimeOption(this, { date, time }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
constructor({
|
||||
dates,
|
||||
times,
|
||||
}: {
|
||||
dates: Set<string>;
|
||||
times: Map<string, Set<string>>;
|
||||
}) {
|
||||
const datetimes = new Map();
|
||||
|
||||
for (const date of dates) {
|
||||
const timesForDate = times.has(date)
|
||||
? Array.from(times.get(date) as Set<string>)
|
||||
: [null];
|
||||
datetimes.set(
|
||||
date,
|
||||
new TrackedSet(
|
||||
timesForDate.map(
|
||||
(time) => new FormDataTimeOption(this, { date, time }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.datetimes = new TrackedMap(datetimes);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateOptoinsDatetimeSignature {
|
||||
Args: {
|
||||
poll: CreateOptionsDatetimeRouteModel;
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateOptionsDatetime extends Component<CreateOptoinsDatetimeSignature> {
|
||||
@service declare router: RouterService;
|
||||
|
||||
formData = new FormData({
|
||||
dates: this.args.poll.dateOptions,
|
||||
times: this.args.poll.timesForDateOptions,
|
||||
});
|
||||
|
||||
@tracked errorMessage: string | null = null;
|
||||
|
||||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
const { formData } = this;
|
||||
const successful = formData.adoptTimesOfFirstDay();
|
||||
|
||||
if (!successful) {
|
||||
this.errorMessage =
|
||||
'create.options-datetime.fix-validation-errors-first-day';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.router.transitionTo('create.options');
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
this.router.transitionTo('create.settings');
|
||||
}
|
||||
|
||||
// validate input field for being partially filled
|
||||
@action
|
||||
validateInput(option: FormDataTimeOption, event: Event) {
|
||||
const element = event.target as HTMLInputElement;
|
||||
|
||||
// update partially filled time validation error
|
||||
option.isPartiallyFilled = !element.checkValidity();
|
||||
}
|
||||
|
||||
// remove partially filled validation error if user fixed it
|
||||
@action
|
||||
updateInputValidation(option: FormDataTimeOption, event: Event) {
|
||||
const element = event.target as HTMLInputElement;
|
||||
|
||||
if (element.checkValidity() && option.isPartiallyFilled) {
|
||||
option.isPartiallyFilled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.options-datetime') {
|
||||
this.args.poll.timesForDateOptions = new Map(
|
||||
// FormData.datetimes Map has a Set of FormDataTime object as values
|
||||
// We need to transform it to a Set of plain time strings
|
||||
Array.from(this.formData.datetimes.entries())
|
||||
.map(([key, timeOptions]): [string, Set<string>] => {
|
||||
return [
|
||||
key,
|
||||
new Set(
|
||||
Array.from(timeOptions)
|
||||
.map(({ time }: FormDataTimeOption) => time)
|
||||
// There might be FormDataTime objects without a time, which
|
||||
// we need to filter out
|
||||
.filter((time) => time !== null),
|
||||
) as Set<string>,
|
||||
];
|
||||
})
|
||||
// There might be dates without any time, which we need to filter out
|
||||
.filter(([, times]) => times.size > 0),
|
||||
);
|
||||
this.router.off('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(owner: unknown, args: CreateOptoinsDatetimeSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateOptionsDatetime: typeof CreateOptionsDatetime;
|
||||
}
|
||||
}
|
53
app/components/create-options-text.hbs
Normal file
53
app/components/create-options-text.hbs
Normal file
|
@ -0,0 +1,53 @@
|
|||
{{#let @formElement as |FormElement|}}
|
||||
{{#each @options as |option index|}}
|
||||
<FormElement
|
||||
{{! show label only on first item }}
|
||||
@label={{unless index (t "create.options.options.label")}}
|
||||
@model={{option}}
|
||||
@property="value"
|
||||
class="option"
|
||||
data-test-form-element="option"
|
||||
data-test-option={{index}}
|
||||
as |el|
|
||||
>
|
||||
<div class="input-group">
|
||||
<el.control
|
||||
{{! first control should be focused automatically }}
|
||||
{{autofocus enabled=(eq index 0)}}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<BsButton
|
||||
@onClick={{fn @deleteOption option}}
|
||||
@type="link"
|
||||
class="delete"
|
||||
{{! disable delete button if there is only one option }}
|
||||
disabled={{lte @options.length 1}}
|
||||
>
|
||||
<span
|
||||
class="oi oi-trash"
|
||||
title={{t "create.options.button.delete.label"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">{{t
|
||||
"create.options.button.delete.label"
|
||||
}}</span>
|
||||
</BsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BsButton
|
||||
@onClick={{fn @addOption "" index}}
|
||||
@type="link"
|
||||
@size="sm"
|
||||
class="add float-left"
|
||||
>
|
||||
<span
|
||||
class="oi oi-plus"
|
||||
title={{t "create.options.button.add.label"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
|
||||
</BsButton>
|
||||
</FormElement>
|
||||
{{/each}}
|
||||
{{/let}}
|
|
@ -1,49 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
@classic
|
||||
export default class CreateOptionsText extends Component {
|
||||
@action
|
||||
addOption(element) {
|
||||
let fragment = this.store.createFragment('option');
|
||||
let options = this.options;
|
||||
let position = this.options.indexOf(element) + 1;
|
||||
options.insertAt(
|
||||
position,
|
||||
fragment
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteOption(element) {
|
||||
let position = this.options.indexOf(element);
|
||||
this.options.removeAt(position);
|
||||
}
|
||||
|
||||
enforceMinimalOptionsAmount() {
|
||||
let options = this.options;
|
||||
|
||||
while (options.length < 2) {
|
||||
options.pushObject(
|
||||
this.store.createFragment('option')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@service('store')
|
||||
store;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
// need to delay pushing fragments into options array to prevent
|
||||
// > You modified "disabled" twice on <(unknown):ember330> in a single render.
|
||||
// error.
|
||||
next(() => {
|
||||
this.enforceMinimalOptionsAmount();
|
||||
});
|
||||
}
|
||||
}
|
24
app/components/create-options-text.ts
Normal file
24
app/components/create-options-text.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
import type { FormDataOption } from './create-options';
|
||||
import type BsFormElementComponent from 'ember-bootstrap/components/bs-form/element';
|
||||
|
||||
interface CreateOptionsTextSignature {
|
||||
Args: {
|
||||
Named: {
|
||||
addOption: (value: string, afterPosition: number) => void;
|
||||
deleteOption: (option: FormDataOption) => void;
|
||||
formElement: BsFormElementComponent;
|
||||
options: FormDataOption[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const CreateOptionsText = templateOnlyComponent<CreateOptionsTextSignature>();
|
||||
|
||||
export default CreateOptionsText;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateOptionsText: typeof CreateOptionsText;
|
||||
}
|
||||
}
|
34
app/components/create-options.hbs
Normal file
34
app/components/create-options.hbs
Normal file
|
@ -0,0 +1,34 @@
|
|||
<div class="cr-form-wrapper box">
|
||||
<BsForm
|
||||
@formLayout="horizontal"
|
||||
@model={{this.formData}}
|
||||
@onInvalid={{(scroll-first-invalid-element-into-view-port)}}
|
||||
@onSubmit={{this.submit}}
|
||||
novalidate
|
||||
as |form|
|
||||
>
|
||||
{{#if (eq @poll.pollType "MakeAPoll")}}
|
||||
<CreateOptionsText
|
||||
@options={{this.formData.options}}
|
||||
@addOption={{this.formData.addOption}}
|
||||
@deleteOption={{this.formData.deleteOption}}
|
||||
@formElement={{form.element}}
|
||||
/>
|
||||
{{else}}
|
||||
<CreateOptionsDates
|
||||
@options={{this.formData.options}}
|
||||
@updateOptions={{this.formData.updateOptions}}
|
||||
@formElement={{form.element}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<div class="row cr-steps-bottom-nav">
|
||||
<div class="col-6 col-md-8 order-12">
|
||||
<NextButton />
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton @onClick={{this.previousPage}} />
|
||||
</div>
|
||||
</div>
|
||||
</BsForm>
|
||||
</div>
|
|
@ -1,50 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
|
||||
let Validations = buildValidations({
|
||||
options: [
|
||||
validator('collection', true),
|
||||
validator('length', {
|
||||
dependentKeys: ['model.options.[]', 'model.intl.locale'],
|
||||
min: 1,
|
||||
// it's impossible to delete all text options so this case could be ignored
|
||||
// for validation error message
|
||||
descriptionKey: 'create.options.error.notEnoughDates'
|
||||
}),
|
||||
validator('valid-collection', {
|
||||
dependentKeys: ['model.options.[]', 'model.options.@each.title']
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default class CreateOptionsComponent extends Component.extend(Validations) {
|
||||
shouldShowErrors = false;
|
||||
|
||||
// consumed by validator
|
||||
@service intl;
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.onPrevPage();
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
if (this.get('validations.isValid')) {
|
||||
this.onNextPage();
|
||||
} else {
|
||||
this.set('shouldShowErrors', true);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.intl.locale;
|
||||
}
|
||||
}
|
168
app/components/create-options.ts
Normal file
168
app/components/create-options.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { TrackedArray, TrackedSet } from 'tracked-built-ins';
|
||||
import IntlMessage from '../utils/intl-message';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { CreateOptionsRouteModel } from '../routes/create/options';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
export class FormDataOption {
|
||||
@tracked value;
|
||||
formData;
|
||||
|
||||
get valueValidation() {
|
||||
const { formData, value } = this;
|
||||
|
||||
// Every option must have a label
|
||||
if (!value) {
|
||||
return new IntlMessage('create.options.error.valueMissing');
|
||||
}
|
||||
|
||||
// Options must be unique. There must not be another option having the
|
||||
// same value before
|
||||
const isUnique = !formData.options
|
||||
.slice(0, this.formData.options.indexOf(this))
|
||||
.some((option) => option.value === this.value);
|
||||
if (!isUnique) {
|
||||
return new IntlMessage('create.options.error.duplicatedOption');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return this.valueValidation === null;
|
||||
}
|
||||
|
||||
constructor(formData: FormData, value: string) {
|
||||
this.formData = formData;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class FormData {
|
||||
@tracked options;
|
||||
|
||||
get optionsValidation() {
|
||||
const { options } = this;
|
||||
|
||||
if (options.length < 1) {
|
||||
// UI enforces that there is at least one option if poll type is `MakeAPoll`.
|
||||
// This validation error can only happen if poll type is `FindADate`.
|
||||
return new IntlMessage('create.options.error.notEnoughDates');
|
||||
}
|
||||
|
||||
if (options.some((option) => !option.isValid)) {
|
||||
return new IntlMessage('create.options.error.invalidOption');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@action
|
||||
updateOptions(values: string[]) {
|
||||
this.options = new TrackedArray(
|
||||
values.map((value) => new FormDataOption(this, value)),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
addOption(value: string, afterPosition = this.options.length - 1) {
|
||||
const option = new FormDataOption(this, value);
|
||||
|
||||
this.options.splice(afterPosition + 1, 0, option);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteOption(option: FormDataOption) {
|
||||
this.options.splice(this.options.indexOf(option), 1);
|
||||
}
|
||||
|
||||
constructor(
|
||||
{ options }: { options: Set<string> },
|
||||
{ defaultOptionCount }: { defaultOptionCount: number },
|
||||
) {
|
||||
const normalizedOptions =
|
||||
options.size === 0 && defaultOptionCount > 0
|
||||
? ['', '']
|
||||
: Array.from(options);
|
||||
|
||||
this.options = new TrackedArray(
|
||||
normalizedOptions.map((value) => new FormDataOption(this, value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateOptionsSignature {
|
||||
Args: {
|
||||
poll: CreateOptionsRouteModel;
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateOptions extends Component<CreateOptionsSignature> {
|
||||
@service declare router: RouterService;
|
||||
|
||||
formData = new FormData(
|
||||
{ options: this.options },
|
||||
{ defaultOptionCount: this.args.poll.pollType === 'MakeAPoll' ? 2 : 0 },
|
||||
);
|
||||
|
||||
get options() {
|
||||
const { poll } = this.args;
|
||||
const { dateOptions, freetextOptions, pollType } = poll;
|
||||
|
||||
return pollType === 'FindADate' ? dateOptions : freetextOptions;
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.router.transitionTo('create.meta');
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
const { pollType } = this.args.poll;
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
this.router.transitionTo('create.options-datetime');
|
||||
} else {
|
||||
this.router.transitionTo('create.settings');
|
||||
}
|
||||
}
|
||||
|
||||
@action handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.options') {
|
||||
this.updatePoll();
|
||||
this.router.off('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
||||
updatePoll() {
|
||||
const { poll } = this.args;
|
||||
const { pollType } = poll;
|
||||
const { options } = this.formData;
|
||||
const pollOptions = options.map(({ value }) => value);
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
poll.dateOptions = new TrackedSet(pollOptions.sort());
|
||||
} else {
|
||||
poll.freetextOptions = new TrackedSet(pollOptions);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(owner: unknown, args: CreateOptionsSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
// Cannot use a destructor because that one runs _after_ the other component
|
||||
// rendered by the next route is initialized.
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateOptions: typeof CreateOptions;
|
||||
}
|
||||
}
|
113
app/components/create-settings.hbs
Normal file
113
app/components/create-settings.hbs
Normal file
|
@ -0,0 +1,113 @@
|
|||
<div class="cr-form-wrapper box">
|
||||
<BsForm
|
||||
@formLayout="horizontal"
|
||||
@model={{this}}
|
||||
@onInvalid={{(scroll-first-invalid-element-into-view-port)}}
|
||||
@onSubmit={{this.createPoll}}
|
||||
novalidate
|
||||
as |form|
|
||||
>
|
||||
<form.element
|
||||
@label={{t "create.settings.answerType.label"}}
|
||||
@property="answerType"
|
||||
@showValidationOn={{array "change" "focusOut"}}
|
||||
@useIcons={{false}}
|
||||
class="answer-type"
|
||||
as |el|
|
||||
>
|
||||
<select
|
||||
id={{el.id}}
|
||||
class="custom-select"
|
||||
{{on "change" (pick "target.value" el.setValue)}}
|
||||
{{autofocus}}
|
||||
>
|
||||
{{#each this.answerTypes as |answerType|}}
|
||||
<option
|
||||
value={{answerType.id}}
|
||||
selected={{eq el.value answerType.id}}
|
||||
>
|
||||
{{t answerType.labelTranslation}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</form.element>
|
||||
<form.element
|
||||
@controlType="select"
|
||||
@label={{t "create.settings.expirationDate.label"}}
|
||||
@property="expirationDuration"
|
||||
@showValidationOn={{array "change" "focusOut"}}
|
||||
@useIcons={{false}}
|
||||
class="expiration-duration"
|
||||
as |el|
|
||||
>
|
||||
<select
|
||||
id={{el.id}}
|
||||
{{on "change" (pick "target.value" el.setValue)}}
|
||||
class="custom-select"
|
||||
>
|
||||
{{#each this.expirationDurations as |duration|}}
|
||||
<option value={{duration.id}} selected={{eq el.value duration.id}}>
|
||||
{{t duration.labelTranslation}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</form.element>
|
||||
<form.element
|
||||
@controlType="checkbox"
|
||||
@label={{t "create.settings.anonymousUser.label"}}
|
||||
@showValidationOn="change"
|
||||
@property="anonymousUser"
|
||||
class="anonymous-user"
|
||||
/>
|
||||
<form.element
|
||||
@controlType="checkbox"
|
||||
@label={{t "create.settings.forceAnswer.label"}}
|
||||
@showValidationOn="change"
|
||||
@property="forceAnswer"
|
||||
class="force-answer"
|
||||
/>
|
||||
|
||||
<div class="row cr-steps-bottom-nav">
|
||||
<div class="col-6 col-md-8 order-12">
|
||||
<SaveButton
|
||||
@isPending={{form.isSubmitting}}
|
||||
data-test-button="submit"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton @onClick={{this.previousPage}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BsModal
|
||||
@onHidden={{this.resetSavingPollFailedState}}
|
||||
@onSubmit={{form.submit}}
|
||||
@open={{this.savingPollFailed}}
|
||||
data-test-modal="saving-failed"
|
||||
as |modal|
|
||||
>
|
||||
<modal.header
|
||||
@closeButton={{false}}
|
||||
@title={{t "error.poll.savingFailed.title"}}
|
||||
/>
|
||||
<modal.body>
|
||||
<p>
|
||||
{{t "error.poll.savingFailed.description"}}
|
||||
</p>
|
||||
</modal.body>
|
||||
<modal.footer>
|
||||
<BsButton @onClick={{modal.close}} data-test-button="abort">
|
||||
{{t "action.abort"}}
|
||||
</BsButton>
|
||||
<SaveButton
|
||||
@isPending={{form.isSubmitting}}
|
||||
@onClick={{modal.submit}}
|
||||
data-test-button="retry"
|
||||
type="button"
|
||||
>
|
||||
{{t "modal.save-retry.button-retry"}}
|
||||
</SaveButton>
|
||||
</modal.footer>
|
||||
</BsModal>
|
||||
</BsForm>
|
||||
</div>
|
188
app/components/create-settings.ts
Normal file
188
app/components/create-settings.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { generatePassphrase } from '../utils/encryption';
|
||||
import Poll from '../models/poll';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings';
|
||||
|
||||
export interface CreateSettingsSignature {
|
||||
Args: {
|
||||
poll: CreateSettingsRouteModel;
|
||||
};
|
||||
}
|
||||
|
||||
export default class CreateSettingsComponent extends Component<CreateSettingsSignature> {
|
||||
@service declare intl: IntlService;
|
||||
@service declare router: RouterService;
|
||||
|
||||
@tracked savingPollFailed = false;
|
||||
|
||||
get anonymousUser() {
|
||||
return this.args.poll.anonymousUser;
|
||||
}
|
||||
set anonymousUser(value) {
|
||||
this.args.poll.anonymousUser = value;
|
||||
}
|
||||
|
||||
get answerType() {
|
||||
return this.args.poll.answerType;
|
||||
}
|
||||
set answerType(value) {
|
||||
this.args.poll.answerType = value;
|
||||
}
|
||||
|
||||
get answerTypes() {
|
||||
return [
|
||||
{ id: 'YesNo', labelTranslation: 'answerTypes.yesNo.label' },
|
||||
{ id: 'YesNoMaybe', labelTranslation: 'answerTypes.yesNoMaybe.label' },
|
||||
{ id: 'FreeText', labelTranslation: 'answerTypes.freeText.label' },
|
||||
];
|
||||
}
|
||||
|
||||
get expirationDuration() {
|
||||
// TODO: must be calculated based on model.expirationDate
|
||||
return 'P3M';
|
||||
}
|
||||
set expirationDuration(value) {
|
||||
this.args.poll.expirationDate = isPresent(value)
|
||||
? (DateTime.local().plus(Duration.fromISO(value)).toISO() as string)
|
||||
: '';
|
||||
}
|
||||
|
||||
get expirationDurations() {
|
||||
return [
|
||||
{
|
||||
id: 'P7D',
|
||||
labelTranslation: 'create.settings.expirationDurations.P7D',
|
||||
},
|
||||
{
|
||||
id: 'P1M',
|
||||
labelTranslation: 'create.settings.expirationDurations.P1M',
|
||||
},
|
||||
{
|
||||
id: 'P3M',
|
||||
labelTranslation: 'create.settings.expirationDurations.P3M',
|
||||
},
|
||||
{
|
||||
id: 'P6M',
|
||||
labelTranslation: 'create.settings.expirationDurations.P6M',
|
||||
},
|
||||
{
|
||||
id: 'P1Y',
|
||||
labelTranslation: 'create.settings.expirationDurations.P1Y',
|
||||
},
|
||||
{ id: '', labelTranslation: 'create.settings.expirationDurations.never' },
|
||||
];
|
||||
}
|
||||
|
||||
get forceAnswer() {
|
||||
return this.args.poll.forceAnswer;
|
||||
}
|
||||
set forceAnswer(value) {
|
||||
this.args.poll.forceAnswer = value;
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
const { pollType } = this.args.poll;
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
this.router.transitionTo('create.options-datetime');
|
||||
} else {
|
||||
this.router.transitionTo('create.options');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async createPoll() {
|
||||
const { poll } = this.args;
|
||||
const {
|
||||
anonymousUser,
|
||||
answerType,
|
||||
description,
|
||||
expirationDate,
|
||||
forceAnswer,
|
||||
freetextOptions,
|
||||
dateOptions,
|
||||
timesForDateOptions,
|
||||
pollType,
|
||||
title,
|
||||
} = poll;
|
||||
|
||||
// calculate options
|
||||
const options: string[] = [];
|
||||
if (pollType === 'FindADate') {
|
||||
// merge date with times
|
||||
for (const date of dateOptions) {
|
||||
if (timesForDateOptions.has(date)) {
|
||||
for (const time of timesForDateOptions.get(date)!) {
|
||||
const [hour, minute] = time.split(':') as [string, string];
|
||||
options.push(
|
||||
DateTime.fromISO(date)
|
||||
.set({
|
||||
hour: parseInt(hour),
|
||||
minute: parseInt(minute),
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
})
|
||||
.toISO() as string,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
options.push(date);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
options.push(...freetextOptions);
|
||||
}
|
||||
|
||||
// save poll
|
||||
try {
|
||||
const encryptionKey = generatePassphrase();
|
||||
|
||||
// save poll
|
||||
const poll = await Poll.create(
|
||||
{
|
||||
anonymousUser,
|
||||
answerType,
|
||||
description,
|
||||
expirationDate,
|
||||
forceAnswer,
|
||||
options: options.map((option) => {
|
||||
return { title: option };
|
||||
}),
|
||||
pollType,
|
||||
title,
|
||||
},
|
||||
encryptionKey,
|
||||
);
|
||||
|
||||
// redirect to new poll
|
||||
await this.router.transitionTo('poll.participation', poll.id, {
|
||||
queryParams: {
|
||||
encryptionKey,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.savingPollFailed = true;
|
||||
|
||||
reportError(err);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
resetSavingPollFailedState() {
|
||||
this.savingPollFailed = false;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
CreateSettings: typeof CreateSettingsComponent;
|
||||
}
|
||||
}
|
|
@ -2,23 +2,24 @@
|
|||
@center={{@center}}
|
||||
@selected={{@selectedDays}}
|
||||
@onCenterChange={{@onCenterChange}}
|
||||
@onSelect={{@onSelect}} as |calendar|
|
||||
@onSelect={{@onSelect}}
|
||||
as |calendar|
|
||||
>
|
||||
<nav class="ember-power-calendar-nav">
|
||||
<button
|
||||
type="button"
|
||||
class="ember-power-calendar-nav-control"
|
||||
onclick={{action calendar.actions.moveCenter -1 "month"}}
|
||||
{{on "click" (fn calendar.actions.moveCenter -1 "month")}}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<div class="ember-power-calendar-nav-title">
|
||||
{{moment-format calendar.center "MMMM YYYY"}}
|
||||
{{format-date calendar.center month="long" year="numeric"}}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ember-power-calendar-nav-control"
|
||||
onclick={{action calendar.actions.moveCenter 1 "month"}}
|
||||
{{on "click" (fn calendar.actions.moveCenter 1 "month")}}
|
||||
>
|
||||
»
|
||||
</button>
|
|
@ -1,7 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { tagName } from '@ember-decorators/component';
|
||||
import Component from '@ember/component';
|
||||
|
||||
@classic
|
||||
@tagName('')
|
||||
export default class InlineDatepicker extends Component {}
|
23
app/components/inline-datepicker.ts
Normal file
23
app/components/inline-datepicker.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
import type { DateTime } from 'luxon';
|
||||
|
||||
interface InlineDatepickerSignature {
|
||||
Args: {
|
||||
Named: {
|
||||
center: DateTime;
|
||||
onCenterChange: (day: { datetime: DateTime }) => void;
|
||||
onSelect: (days: { datetime: DateTime[] }) => void;
|
||||
selectedDays: DateTime[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const InlineDatepicker = templateOnlyComponent<InlineDatepickerSignature>();
|
||||
|
||||
export default InlineDatepicker;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
InlineDatepicker: typeof InlineDatepicker;
|
||||
}
|
||||
}
|
8
app/components/language-select.hbs
Normal file
8
app/components/language-select.hbs
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{! template-lint-disable require-input-label }}
|
||||
<select class="language-select" {{on "change" this.handleChange}}>
|
||||
{{#each-in this.locales as |localeKey localeName|}}
|
||||
<option value={{localeKey}} selected={{eq localeKey this.currentLocale}}>
|
||||
{{localeName}}
|
||||
</option>
|
||||
{{/each-in}}
|
||||
</select>
|
|
@ -1,49 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { classNames, tagName } from '@ember-decorators/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { readOnly } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import localesMeta from 'croodle/locales/meta';
|
||||
|
||||
@classic
|
||||
@tagName('select')
|
||||
@classNames('language-select')
|
||||
export default class LanguageSelect extends Component {
|
||||
@service
|
||||
intl;
|
||||
|
||||
@service
|
||||
moment;
|
||||
|
||||
@service
|
||||
powerCalendar;
|
||||
|
||||
@readOnly('intl.primaryLocale')
|
||||
current;
|
||||
|
||||
@computed('intl.locales')
|
||||
get locales() {
|
||||
let currentLocale = this.intl.primaryLocale;
|
||||
|
||||
return Object.keys(localesMeta).map(function(locale) {
|
||||
return {
|
||||
id: locale,
|
||||
selected: locale === currentLocale,
|
||||
text: localesMeta[locale]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
change() {
|
||||
let locale = this.element.options[this.element.selectedIndex].value;
|
||||
|
||||
this.intl.set('locale', locale.includes('-') ? [locale, locale.split('-')[0]] : [locale]);
|
||||
this.moment.changeLocale(locale);
|
||||
this.powerCalendar.set('locale', locale);
|
||||
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem('locale', locale);
|
||||
}
|
||||
}
|
||||
}
|
38
app/components/language-select.ts
Normal file
38
app/components/language-select.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import localesMeta from 'croodle/locales/meta';
|
||||
import { action } from '@ember/object';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
import type PowerCalendarService from 'ember-power-calendar/services/power-calendar';
|
||||
|
||||
export default class LanguageSelect extends Component {
|
||||
@service declare intl: IntlService;
|
||||
@service declare powerCalendar: PowerCalendarService;
|
||||
|
||||
get currentLocale() {
|
||||
return this.intl.primaryLocale;
|
||||
}
|
||||
|
||||
locales = localesMeta;
|
||||
|
||||
@action
|
||||
handleChange(event: Event) {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
const locale = selectElement.value as keyof typeof this.locales;
|
||||
|
||||
this.intl.locale = locale.includes('-')
|
||||
? [locale, locale.split('-')[0] as string]
|
||||
: [locale];
|
||||
this.powerCalendar.locale = locale;
|
||||
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem('locale', locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
LanguageSelect: typeof LanguageSelect;
|
||||
}
|
||||
}
|
5
app/components/loading-spinner.hbs
Normal file
5
app/components/loading-spinner.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
13
app/components/loading-spinner.ts
Normal file
13
app/components/loading-spinner.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
|
||||
interface LoadingSpinnerSignature {}
|
||||
|
||||
const LoadingSpinner = templateOnlyComponent<LoadingSpinnerSignature>();
|
||||
|
||||
export default LoadingSpinner;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
LoadingSpinner: typeof LoadingSpinner;
|
||||
}
|
||||
}
|
20
app/components/next-button.ts
Normal file
20
app/components/next-button.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
|
||||
interface NextButtonSignature {
|
||||
Args: {
|
||||
Named: {
|
||||
isPending?: boolean;
|
||||
};
|
||||
};
|
||||
Element: HTMLButtonElement;
|
||||
}
|
||||
|
||||
const NextButton = templateOnlyComponent<NextButtonSignature>();
|
||||
|
||||
export default NextButton;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
NextButton: typeof NextButton;
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { readOnly } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import moment from 'moment';
|
||||
|
||||
const addArrays = function() {
|
||||
let args = Array.prototype.slice.call(arguments);
|
||||
let basis = args.shift();
|
||||
if (!isArray(basis)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
args.forEach(function(array) {
|
||||
array.forEach(function(value, index) {
|
||||
if (isPresent(value)) {
|
||||
basis[index] = basis[index] + value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return basis;
|
||||
};
|
||||
|
||||
@classic
|
||||
export default class PollEvaluationChart extends Component {
|
||||
@service
|
||||
intl;
|
||||
|
||||
@computed
|
||||
get chartOptions() {
|
||||
return {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
stacked: true
|
||||
}],
|
||||
yAxes: [{
|
||||
stacked: true,
|
||||
ticks: {
|
||||
callback(value) {
|
||||
return `${value} %`;
|
||||
},
|
||||
max: 100,
|
||||
min: 0
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'label',
|
||||
callbacks: {
|
||||
label(tooltipItem, data) {
|
||||
let { datasets } = data;
|
||||
let { datasetIndex } = tooltipItem;
|
||||
let { label } = datasets[datasetIndex];
|
||||
let value = tooltipItem.yLabel;
|
||||
return `${label}: ${value} %`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@computed('users.[]', 'options.{[],each.title}', 'currentLocale')
|
||||
get data() {
|
||||
let labels = this.options.map((option) => {
|
||||
let value = get(option, 'title');
|
||||
if (!this.isFindADate) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let hasTime = value.length > 10; // 'YYYY-MM-DD'.length === 10
|
||||
let momentFormat = hasTime ? 'LLLL' : this.momentLongDayFormat;
|
||||
let timezone = this.timezone;
|
||||
let date = hasTime && isPresent(timezone) ? moment.tz(value, timezone) : moment(value);
|
||||
date.locale(this.currentLocale);
|
||||
return date.format(momentFormat);
|
||||
});
|
||||
|
||||
let datasets = [];
|
||||
let participants = this.users.length;
|
||||
|
||||
let yes = this.users.map(({ selections }) => {
|
||||
return selections.map(({ type }) => type === 'yes' ? 1 : 0);
|
||||
});
|
||||
datasets.push({
|
||||
label: this.intl.t('answerTypes.yes.label').toString(),
|
||||
backgroundColor: 'rgba(151,187,205,0.5)',
|
||||
borderColor: 'rgba(151,187,205,0.8)',
|
||||
hoverBackgroundColor: 'rgba(151,187,205,0.75)',
|
||||
hoverBorderColor: 'rgba(151,187,205,1)',
|
||||
data: addArrays.apply(this, yes).map((value) => Math.round(value / participants * 100))
|
||||
});
|
||||
|
||||
if (this.answerType === 'YesNoMaybe') {
|
||||
let maybe = this.users.map(({ selections }) => {
|
||||
return selections.map(({ type }) => type === 'maybe' ? 1 : 0);
|
||||
});
|
||||
datasets.push({
|
||||
label: this.intl.t('answerTypes.maybe.label').toString(),
|
||||
backgroundColor: 'rgba(220,220,220,0.5)',
|
||||
borderColor: 'rgba(220,220,220,0.8)',
|
||||
hoverBackgroundColor: 'rgba(220,220,220,0.75)',
|
||||
hoverBorderColor: 'rgba(220,220,220,1)',
|
||||
data: addArrays.apply(this, maybe).map((value) => Math.round(value / participants * 100))
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
datasets,
|
||||
labels
|
||||
};
|
||||
}
|
||||
|
||||
@readOnly('poll.answerType')
|
||||
answerType;
|
||||
|
||||
@readOnly('intl.primaryLocale')
|
||||
currentLocale;
|
||||
|
||||
@readOnly('poll.isFindADate')
|
||||
isFindADate;
|
||||
|
||||
@readOnly('poll.options')
|
||||
options;
|
||||
|
||||
@readOnly('poll.users')
|
||||
users;
|
||||
}
|
86
app/components/poll-evaluation-participants-table.hbs
Normal file
86
app/components/poll-evaluation-participants-table.hbs
Normal file
|
@ -0,0 +1,86 @@
|
|||
<div class="participants-table">
|
||||
<table class="table" data-test-table-of="participants">
|
||||
<thead>
|
||||
{{#if @poll.hasTimes}}
|
||||
<tr>
|
||||
<th>
|
||||
{{! column for name }}
|
||||
</th>
|
||||
{{#each-in this.optionsPerDay as |jsDate count|}}
|
||||
{{!
|
||||
@glint-ignore
|
||||
We can be sure that count is a number because it is destructed from a
|
||||
Map, which values are only numbers. But somehow Glint / TypeScript
|
||||
is not sure about it.
|
||||
}}
|
||||
<th colspan={{count}}>
|
||||
{{format-date jsDate dateStyle="full" timeZone=@timeZone}}
|
||||
</th>
|
||||
{{/each-in}}
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
||||
<tr>
|
||||
<th>
|
||||
{{! column for name }}
|
||||
</th>
|
||||
{{#each @poll.options as |option|}}
|
||||
<th>
|
||||
{{#if (and @poll.isFindADate @poll.hasTimes)}}
|
||||
{{#if option.hasTime}}
|
||||
{{!
|
||||
@glint-ignore
|
||||
Narrowring is not working here correctly. Due to the only executing if
|
||||
`option.hasTime` is `true`, we know that `option.jsDate` cannot be `null`.
|
||||
But TypeScript does not support narrowing through a chain of getters
|
||||
currently.
|
||||
}}
|
||||
{{! @glint-ignore }}{{! prettier-ignore }}
|
||||
{{format-date option.jsDate
|
||||
timeStyle="short"
|
||||
timeZone=@timeZone
|
||||
}}
|
||||
{{/if}}
|
||||
{{else if @poll.isFindADate}}
|
||||
{{!
|
||||
@glint-ignore
|
||||
Narrowring is not working here correctly. Due to the only executing if
|
||||
`option.hasTime` is `true`, we know that `option.jsDate` cannot be `null`.
|
||||
But TypeScript does not support narrowing through a chain of getters
|
||||
currently.
|
||||
}}
|
||||
{{format-date option.jsDate dateStyle="full" timeZone=@timeZone}}
|
||||
{{else}}
|
||||
{{option.title}}
|
||||
{{/if}}
|
||||
</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.usersSorted as |user|}}
|
||||
<tr data-test-participant={{user.id}}>
|
||||
<td data-test-value-for="name">
|
||||
{{user.name}}
|
||||
</td>
|
||||
{{#each @poll.options as |option index|}}
|
||||
{{#let (get user.selections index) as |selection|}}
|
||||
<td
|
||||
class={{selection.type}}
|
||||
data-test-is-selection-cell
|
||||
data-test-value-for={{option.title}}
|
||||
>
|
||||
{{#if selection.labelTranslation}}
|
||||
{{t selection.labelTranslation}}
|
||||
{{else}}
|
||||
{{selection.label}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,29 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { readOnly } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import { raw } from 'ember-awesome-macros';
|
||||
import { groupBy, sort } from 'ember-awesome-macros/array';
|
||||
|
||||
@classic
|
||||
export default class PollEvaluationParticipantsTable extends Component {
|
||||
@readOnly('poll.hasTimes')
|
||||
hasTimes;
|
||||
|
||||
@readOnly('poll.isFindADate')
|
||||
isFindADate;
|
||||
|
||||
@readOnly('poll.isFreeText')
|
||||
isFreeText;
|
||||
|
||||
@readOnly('poll.options')
|
||||
options;
|
||||
|
||||
@groupBy('options', raw('day'))
|
||||
optionsGroupedByDays;
|
||||
|
||||
@readOnly('poll.users')
|
||||
users;
|
||||
|
||||
@sort('users', ['creationDate'])
|
||||
usersSorted;
|
||||
}
|
52
app/components/poll-evaluation-participants-table.ts
Normal file
52
app/components/poll-evaluation-participants-table.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import Component from '@glimmer/component';
|
||||
import type Poll from 'croodle/models/poll';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export interface PollEvaluationParticipantsTableSignature {
|
||||
Args: {
|
||||
poll: Poll;
|
||||
timeZone: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export default class PollEvaluationParticipantsTable extends Component<PollEvaluationParticipantsTableSignature> {
|
||||
get optionsPerDay() {
|
||||
const { poll } = this.args;
|
||||
|
||||
const optionsPerDay: Map<string, number> = new Map();
|
||||
for (const option of poll.options) {
|
||||
if (!option.day) {
|
||||
throw new Error(
|
||||
`Excepts all options to have a valid ISO8601 date string when using optionsPerDay getter`,
|
||||
);
|
||||
}
|
||||
|
||||
optionsPerDay.set(
|
||||
option.day,
|
||||
optionsPerDay.has(option.day)
|
||||
? (optionsPerDay.get(option.day) as number) + 1
|
||||
: 0,
|
||||
);
|
||||
}
|
||||
|
||||
return new Map(
|
||||
Array.from(optionsPerDay.entries()).map(([dayString, count]) => [
|
||||
DateTime.fromISO(dayString).toJSDate(),
|
||||
count,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
get usersSorted() {
|
||||
const { poll } = this.args;
|
||||
return Array.from(poll.users).sort((a, b) =>
|
||||
a.creationDate > b.creationDate ? 1 : -1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
PollEvaluationParticipantsTable: typeof PollEvaluationParticipantsTable;
|
||||
}
|
||||
}
|
|
@ -1,23 +1,28 @@
|
|||
{{!--
|
||||
{{!
|
||||
There must not be a line break between option text and "</strong>." cause otherwise
|
||||
we will see a space between option string and following dot.
|
||||
--}}
|
||||
{{#if @isFindADate}}
|
||||
{{! Need to disable block indentation rule cause there shouldn't be a space between date and dot }}
|
||||
{{! template-lint-disable block-indentation }}
|
||||
<strong class="best-option-value">
|
||||
{{moment-format
|
||||
@evaluationBestOption.option.title
|
||||
(if @evaluationBestOption.option.hasTime "LLLL" @momentLongDayFormat)
|
||||
locale=@currentLocale
|
||||
timeZone=@timezone
|
||||
}}
|
||||
{{!
|
||||
Checking `@evaluationBestOption.option.jsDate` is the same as checking `@isFindADate`.
|
||||
If poll type is `FindADate` we can be sure that every option is a valid ISO861
|
||||
string. Therefore `Option.jsDate` must be `true` by design. But Glint / TypeScript
|
||||
does not understand that. Therefore we need to use the less readable form.
|
||||
}}
|
||||
{{#if @evaluationBestOption.option.jsDate}}
|
||||
<strong data-test-best-option={{@evaluationBestOption.option.title}}>
|
||||
{{format-date
|
||||
@evaluationBestOption.option.jsDate
|
||||
dateStyle="full"
|
||||
timeStyle=(if @evaluationBestOption.option.hasTime "short" undefined)
|
||||
timeZone=(if @timeZone @timeZone undefined)
|
||||
}}</strong>.
|
||||
{{! template-lint-enable block-indentation }}
|
||||
{{else}}
|
||||
<strong class="best-option-value">{{@evaluationBestOption.option.title}}</strong>.
|
||||
<strong
|
||||
data-test-best-option={{@evaluationBestOption.option.title}}
|
||||
>{{@evaluationBestOption.option.title}}</strong>.
|
||||
{{/if}}
|
||||
|
||||
<br>
|
||||
<br />
|
||||
|
||||
{{#if @isFindADate}}
|
||||
{{#if @evaluationBestOption.answers.yes}}
|
24
app/components/poll-evaluation-summary-option.ts
Normal file
24
app/components/poll-evaluation-summary-option.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
import type { BestOption } from './poll-evaluation-summary';
|
||||
|
||||
interface PollEvaluationSummaryOptionSignature {
|
||||
Args: {
|
||||
Named: {
|
||||
evaluationBestOption: BestOption;
|
||||
isFindADate: boolean;
|
||||
timeZone: string | undefined;
|
||||
};
|
||||
};
|
||||
Element: HTMLButtonElement;
|
||||
}
|
||||
|
||||
const PollEvaluationSummaryOption =
|
||||
templateOnlyComponent<PollEvaluationSummaryOptionSignature>();
|
||||
|
||||
export default PollEvaluationSummaryOption;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
PollEvaluationSummaryOption: typeof PollEvaluationSummaryOption;
|
||||
}
|
||||
}
|
62
app/components/poll-evaluation-summary.hbs
Normal file
62
app/components/poll-evaluation-summary.hbs
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="evaluation-summary">
|
||||
<h2>
|
||||
{{t "poll.evaluation.label"}}
|
||||
</h2>
|
||||
|
||||
<p class="participants">
|
||||
{{t "poll.evaluation.participants" count=@poll.users.length}}
|
||||
</p>
|
||||
|
||||
<p class="best-options">
|
||||
{{#if @poll.isFindADate}}
|
||||
{{t
|
||||
"poll.evaluation.bestOption.label.findADate"
|
||||
count=this.bestOptions.length
|
||||
}}
|
||||
{{else}}
|
||||
{{t
|
||||
"poll.evaluation.bestOption.label.makeAPoll"
|
||||
count=this.bestOptions.length
|
||||
}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.bestOptions.length 1)}}
|
||||
<ul>
|
||||
{{#each this.bestOptions as |evaluationBestOption|}}
|
||||
<li>
|
||||
<PollEvaluationSummaryOption
|
||||
@evaluationBestOption={{evaluationBestOption}}
|
||||
@isFindADate={{@poll.isFindADate}}
|
||||
@timeZone={{@timeZone}}
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<PollEvaluationSummaryOption
|
||||
{{!
|
||||
@glint-ignore
|
||||
We can be sure that `this.bestOptions` contains at least one item
|
||||
as a poll must always have at least one option.
|
||||
}}
|
||||
@evaluationBestOption={{get this.bestOptions 0}}
|
||||
@isFindADate={{@poll.isFindADate}}
|
||||
@timeZone={{@timeZone}}
|
||||
/>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<p class="last-participation">
|
||||
{{#if this.lastParticipationAt}}
|
||||
{{t
|
||||
"poll.evaluation.lastParticipation"
|
||||
ago=(format-date-relative this.lastParticipationAt)
|
||||
}}
|
||||
{{else}}
|
||||
{{!
|
||||
No need for the else block as user cannot enter evaluation page if
|
||||
no one participated in the poll yet.
|
||||
}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
|
@ -1,96 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { classNames } from '@ember-decorators/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { readOnly, max, mapBy, gt } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import { copy } from '@ember/object/internals';
|
||||
import { isEmpty } from '@ember/utils';
|
||||
|
||||
@classic
|
||||
@classNames('evaluation-summary')
|
||||
export default class PollEvaluationSummary extends Component {
|
||||
@service
|
||||
intl;
|
||||
|
||||
@computed('users.[]')
|
||||
get bestOptions() {
|
||||
// can not evaluate answer type free text
|
||||
if (this.get('poll.isFreeText')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// can not evaluate a poll without users
|
||||
if (isEmpty(this.users)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let answers = this.poll.answers.reduce((answers, answer) => {
|
||||
answers[answer.get('type')] = 0;
|
||||
return answers;
|
||||
}, {});
|
||||
let evaluation = this.poll.options.map((option) => {
|
||||
return {
|
||||
answers: copy(answers),
|
||||
option,
|
||||
score: 0
|
||||
};
|
||||
});
|
||||
let bestOptions = [];
|
||||
|
||||
this.users.forEach((user) => {
|
||||
user.selections.forEach(({ type }, i) => {
|
||||
evaluation[i].answers[type]++;
|
||||
|
||||
switch (type) {
|
||||
case 'yes':
|
||||
evaluation[i].score += 2;
|
||||
break;
|
||||
|
||||
case 'maybe':
|
||||
evaluation[i].score += 1;
|
||||
break;
|
||||
|
||||
case 'no':
|
||||
evaluation[i].score -= 2;
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
evaluation.sort((a, b) => b.score - a.score);
|
||||
|
||||
let bestScore = evaluation[0].score;
|
||||
for (let i = 0; i < evaluation.length; i++) {
|
||||
if (
|
||||
bestScore === evaluation[i].score
|
||||
) {
|
||||
bestOptions.push(
|
||||
evaluation[i]
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bestOptions;
|
||||
}
|
||||
|
||||
@readOnly('intl.primaryLocale')
|
||||
currentLocale;
|
||||
|
||||
@gt('bestOptions.length', 1)
|
||||
multipleBestOptions;
|
||||
|
||||
@max('participationDates')
|
||||
lastParticipationAt;
|
||||
|
||||
@mapBy('users', 'creationDate')
|
||||
participationDates;
|
||||
|
||||
@readOnly('users.length')
|
||||
participantsCount;
|
||||
|
||||
@readOnly('poll.users')
|
||||
users;
|
||||
}
|
124
app/components/poll-evaluation-summary.ts
Normal file
124
app/components/poll-evaluation-summary.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
import type Option from 'croodle/models/option';
|
||||
import type User from 'croodle/models/user';
|
||||
import type { Answer } from 'croodle/utils/answers-for-answer-type';
|
||||
import type Poll from 'croodle/models/poll';
|
||||
|
||||
export interface PollEvaluationSummarySignature {
|
||||
Args: {
|
||||
poll: Poll;
|
||||
timeZone: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BestOption {
|
||||
answers: Record<'yes' | 'no' | 'maybe', number>;
|
||||
option: Option;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export default class PollEvaluationSummary extends Component<PollEvaluationSummarySignature> {
|
||||
@service declare intl: IntlService;
|
||||
|
||||
get bestOptions(): BestOption[] | null {
|
||||
const { poll } = this.args;
|
||||
const { isFreeText, options, users } = poll;
|
||||
|
||||
// can not evaluate answer type free text
|
||||
if (isFreeText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// can not evaluate a poll without users
|
||||
if (users.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const answers = poll.answers.reduce(
|
||||
(answers, answer: Answer) => {
|
||||
answers[answer.type] = 0;
|
||||
return answers;
|
||||
},
|
||||
{} as Record<'yes' | 'no' | 'maybe', number>,
|
||||
);
|
||||
const evaluation: BestOption[] = options.map((option: Option) => {
|
||||
return {
|
||||
answers: { ...answers },
|
||||
option,
|
||||
score: 0,
|
||||
};
|
||||
});
|
||||
|
||||
users.forEach((user: User) => {
|
||||
user.selections.forEach(({ type }, i) => {
|
||||
if (!type) {
|
||||
// type may be undefined if poll does not force an answer to all options
|
||||
return;
|
||||
}
|
||||
|
||||
const evaluationForOption = evaluation[i];
|
||||
if (evaluationForOption === undefined) {
|
||||
throw new Error(
|
||||
'Mismatch between number of options in poll and selections for user',
|
||||
);
|
||||
}
|
||||
if (type !== 'yes' && type !== 'no' && type !== 'maybe') {
|
||||
throw new Error(
|
||||
`Encountered not supported type of user selection: ${type}`,
|
||||
);
|
||||
}
|
||||
evaluationForOption.answers[type]++;
|
||||
|
||||
switch (type) {
|
||||
case 'yes':
|
||||
evaluation[i]!.score += 2;
|
||||
break;
|
||||
|
||||
case 'maybe':
|
||||
evaluation[i]!.score += 1;
|
||||
break;
|
||||
|
||||
case 'no':
|
||||
evaluation[i]!.score -= 2;
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
evaluation.sort((a, b) => b.score - a.score);
|
||||
|
||||
const bestOptions = [];
|
||||
const bestScore = evaluation[0]!.score;
|
||||
for (const evaluationForOption of evaluation) {
|
||||
if (evaluationForOption.score === bestScore) {
|
||||
bestOptions.push(evaluationForOption);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bestOptions;
|
||||
}
|
||||
|
||||
get lastParticipationAt() {
|
||||
const { users } = this.args.poll;
|
||||
|
||||
let lastParticipationAt = null;
|
||||
|
||||
for (const { creationDate } of users) {
|
||||
if (lastParticipationAt === null || creationDate >= lastParticipationAt) {
|
||||
lastParticipationAt = creationDate;
|
||||
}
|
||||
}
|
||||
|
||||
return lastParticipationAt;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
PollEvaluationSummary: typeof PollEvaluationSummary;
|
||||
}
|
||||
}
|
27
app/components/save-button.hbs
Normal file
27
app/components/save-button.hbs
Normal file
|
@ -0,0 +1,27 @@
|
|||
<BsButton
|
||||
@type="primary"
|
||||
{{!
|
||||
Due to a bug in Ember, conditional modifiers cannot be used with the "on"
|
||||
modifier. Need to always apply the modifier and fallback to a noop function
|
||||
(returned by noop helper) instead of only applying the modifier when needed.
|
||||
See https://github.com/emberjs/ember.js/issues/19869.
|
||||
}}
|
||||
{{on "click" (if @onClick @onClick (noop))}}
|
||||
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
|
||||
type="submit"
|
||||
...attributes
|
||||
>
|
||||
<span class="cr-steps-bottom-nav__label">
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{t "action.save"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if @isPending}}
|
||||
<LoadingSpinner />
|
||||
{{else}}
|
||||
<span class="cr-steps-bottom-nav__icon oi oi-circle-check"></span>
|
||||
{{/if}}
|
||||
</BsButton>
|
24
app/components/save-button.ts
Normal file
24
app/components/save-button.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import templateOnlyComponent from '@ember/component/template-only';
|
||||
|
||||
interface SaveButtonSignature {
|
||||
Args: {
|
||||
Named: {
|
||||
isPending: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
};
|
||||
Blocks: {
|
||||
default: [];
|
||||
};
|
||||
Element: HTMLButtonElement;
|
||||
}
|
||||
|
||||
const SaveButton = templateOnlyComponent<SaveButtonSignature>();
|
||||
|
||||
export default SaveButton;
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
SaveButton: typeof SaveButton;
|
||||
}
|
||||
}
|
32
app/components/share-poll-url.hbs
Normal file
32
app/components/share-poll-url.hbs
Normal file
|
@ -0,0 +1,32 @@
|
|||
<div class="box poll-link cr-poll-link">
|
||||
<p>{{t "poll.share.title"}}</p>
|
||||
<p class="link cr-poll-link__link">
|
||||
<small>
|
||||
<code class="cr-poll-link__url" data-test-poll-url>{{this.pollUrl}}</code>
|
||||
</small>
|
||||
<CopyButton
|
||||
@text={{this.pollUrl}}
|
||||
@onSuccess={{this.showCopySuccessMessage}}
|
||||
class="btn btn-secondary cr-poll-link__copy-btn btn-sm"
|
||||
data-test-button="copy-link"
|
||||
>
|
||||
{{t "poll.link.copy-label"}}
|
||||
<span
|
||||
class="oi oi-clipboard"
|
||||
title={{t "poll.link.copy-label"}}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<BsTooltip
|
||||
@placement="bottom"
|
||||
@triggerEvents="click"
|
||||
data-test-tooltip="copied"
|
||||
>
|
||||
{{t "poll.link.copied"}}
|
||||
</BsTooltip>
|
||||
</CopyButton>
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
{{t "poll.share.notice"}}
|
||||
</small>
|
||||
</div>
|
40
app/components/share-poll-url.ts
Normal file
40
app/components/share-poll-url.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class SharePollUrlComponent extends Component {
|
||||
@service declare router: RouterService;
|
||||
|
||||
@tracked shouldShowCopySuccessMessage = false;
|
||||
|
||||
get pollUrl() {
|
||||
// RouterService.currentURL is relative to the webserver's
|
||||
// directory from which Croodle is served.
|
||||
const relativeUrl = this.router.currentURL;
|
||||
|
||||
if (!relativeUrl) {
|
||||
throw new Error('Relative URL is `null`. Cannot calculate poll URL.');
|
||||
}
|
||||
|
||||
// The URL under which Croodle is served, is not known at
|
||||
// build-time. But we can construct it ourself, by parsing
|
||||
// window.location.href and replacing the hash part.
|
||||
const absoluteUrl = new URL(window.location.href);
|
||||
absoluteUrl.hash = relativeUrl;
|
||||
|
||||
return absoluteUrl.toString();
|
||||
}
|
||||
|
||||
@action
|
||||
showCopySuccessMessage() {
|
||||
this.shouldShowCopySuccessMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
SharePollUrl: typeof SharePollUrlComponent;
|
||||
}
|
||||
}
|
14
app/config/environment.d.ts
vendored
Normal file
14
app/config/environment.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Type declarations for
|
||||
* import config from 'croodle/config/environment'
|
||||
*/
|
||||
declare const config: {
|
||||
environment: string;
|
||||
modulePrefix: string;
|
||||
podModulePrefix: string;
|
||||
locationType: 'history' | 'hash' | 'none';
|
||||
rootURL: string;
|
||||
APP: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,61 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action, computed } from '@ember/object';
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class CreateController extends Controller {
|
||||
@service
|
||||
router;
|
||||
|
||||
@computed('model.pollType', 'visitedSteps')
|
||||
get canEnterMetaStep() {
|
||||
return this.visitedSteps.has('meta') && this.model.pollType;
|
||||
}
|
||||
|
||||
@computed('model.title', 'visitedSteps')
|
||||
get canEnterOptionsStep() {
|
||||
let { title } = this.model;
|
||||
return this.visitedSteps.has('options') &&
|
||||
typeof title === 'string' && title.length >= 2;
|
||||
}
|
||||
|
||||
@computed('model.options.[]', 'visitedSteps')
|
||||
get canEnterOptionsDatetimeStep() {
|
||||
return this.visitedSteps.has('options-datetime') && this.model.options.length >= 1;
|
||||
}
|
||||
|
||||
@computed('model.options.[]', 'visitedSteps')
|
||||
get canEnterSettingsStep() {
|
||||
return this.visitedSteps.has('settings') && this.model.options.length >= 1;
|
||||
}
|
||||
|
||||
@computed('model.pollType')
|
||||
get isFindADate() {
|
||||
return this.model.pollType === 'FindADate';
|
||||
}
|
||||
|
||||
@action
|
||||
updateVisitedSteps() {
|
||||
let { currentRouteName } = this.router;
|
||||
|
||||
// currentRouteName might not be defined in some edge cases
|
||||
if (!currentRouteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let step = currentRouteName.split('.').pop();
|
||||
this.visitedSteps.add(step);
|
||||
|
||||
// as visitedSteps is a Set must notify about changes manually
|
||||
this.notifyPropertyChange('visitedSteps');
|
||||
}
|
||||
|
||||
listenForStepChanges() {
|
||||
this.set('visitedSteps', new Set());
|
||||
|
||||
this.router.on('routeDidChange', this.updateVisitedSteps);
|
||||
}
|
||||
|
||||
clearListenerForStepChanges() {
|
||||
this.router.off('routeDidChange', this.updateVisitedSteps);
|
||||
}
|
||||
}
|
73
app/controllers/create.ts
Normal file
73
app/controllers/create.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import Controller from '@ember/controller';
|
||||
import { TrackedSet } from 'tracked-built-ins';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { CreateRouteModel } from 'croodle/routes/create';
|
||||
|
||||
export default class CreateController extends Controller {
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateRouteModel;
|
||||
|
||||
visitedSteps = new TrackedSet();
|
||||
|
||||
get canEnterMetaStep() {
|
||||
return this.visitedSteps.has('meta') && this.model.pollType;
|
||||
}
|
||||
|
||||
get canEnterOptionsStep() {
|
||||
const { title } = this.model;
|
||||
return (
|
||||
this.visitedSteps.has('options') &&
|
||||
typeof title === 'string' &&
|
||||
title.length >= 2
|
||||
);
|
||||
}
|
||||
|
||||
get canEnterOptionsDatetimeStep() {
|
||||
return (
|
||||
this.visitedSteps.has('options-datetime') &&
|
||||
this.model.dateOptions.size >= 1
|
||||
);
|
||||
}
|
||||
|
||||
get canEnterSettingsStep() {
|
||||
const { model, visitedSteps } = this;
|
||||
const { dateOptions, freetextOptions, pollType } = model;
|
||||
const options = pollType === 'FindADate' ? dateOptions : freetextOptions;
|
||||
|
||||
return visitedSteps.has('settings') && options.size >= 1;
|
||||
}
|
||||
|
||||
get isFindADate() {
|
||||
return this.model.pollType === 'FindADate';
|
||||
}
|
||||
|
||||
@action
|
||||
updateVisitedSteps() {
|
||||
const { currentRouteName } = this.router;
|
||||
|
||||
// currentRouteName might not be defined in some edge cases
|
||||
if (!currentRouteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const step = currentRouteName.split('.').pop();
|
||||
this.visitedSteps.add(step);
|
||||
}
|
||||
|
||||
@action transitionTo(route: string) {
|
||||
this.router.transitionTo(route);
|
||||
}
|
||||
|
||||
listenForStepChanges() {
|
||||
this.set('visitedSteps', new TrackedSet());
|
||||
|
||||
this.router.on('routeDidChange', this.updateVisitedSteps);
|
||||
}
|
||||
|
||||
clearListenerForStepChanges() {
|
||||
this.router.off('routeDidChange', this.updateVisitedSteps);
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
|
||||
const Validations = buildValidations({
|
||||
pollType: [
|
||||
validator('presence', {
|
||||
presence: true,
|
||||
dependentKeys: ['model.intl.locale']
|
||||
}),
|
||||
validator('inclusion', {
|
||||
in: ['FindADate', 'MakeAPoll'],
|
||||
dependentKeys: ['model.intl.locale']
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default class CreateIndex extends Controller.extend(Validations) {
|
||||
@service
|
||||
intl;
|
||||
|
||||
@alias('model.pollType')
|
||||
pollType;
|
||||
|
||||
@action
|
||||
submit() {
|
||||
if (this.get('validations.isValid')) {
|
||||
this.transitionToRoute('create.meta');
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.intl.locale;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
|
||||
const Validations = buildValidations({
|
||||
title: [
|
||||
validator('presence', {
|
||||
presence: true,
|
||||
dependentKeys: ['model.intl.locale']
|
||||
}),
|
||||
validator('length', {
|
||||
min: 2,
|
||||
dependentKeys: ['model.intl.locale']
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default class CreateMetaController extends Controller.extend(Validations) {
|
||||
@service
|
||||
intl;
|
||||
|
||||
@alias('model.description')
|
||||
description;
|
||||
|
||||
@alias('model.title')
|
||||
title;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.get('intl.locale');
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.transitionToRoute('create.index');
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
if (this.get('validations.isValid')) {
|
||||
this.transitionToRoute('create.options');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { action } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import moment from 'moment';
|
||||
|
||||
@classic
|
||||
export default class CreateOptionsDatetimeController extends Controller {
|
||||
@action
|
||||
nextPage() {
|
||||
this.normalizeOptions();
|
||||
|
||||
this.transitionToRoute('create.settings');
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.transitionToRoute('create.options');
|
||||
}
|
||||
|
||||
normalizeOptions() {
|
||||
const options = this.options;
|
||||
|
||||
// remove all days from options which haven't a time but there is atleast
|
||||
// one option with time for that day
|
||||
const daysWithTime = options.map((option) => {
|
||||
if (moment(option.get('title'), 'YYYY-MM-DD', true).isValid()) {
|
||||
return null;
|
||||
} else {
|
||||
return moment(option.get('title')).format('YYYY-MM-DD');
|
||||
}
|
||||
}).uniq().filter((option) => option !== null);
|
||||
const removeObjects = options.filter((option) => {
|
||||
return daysWithTime.indexOf(option.get('title')) !== -1;
|
||||
});
|
||||
options.removeObjects(
|
||||
removeObjects
|
||||
);
|
||||
|
||||
// sort options
|
||||
// ToDo: Find a better way without reseting the options
|
||||
this.set('options', options.sortBy('title'));
|
||||
}
|
||||
|
||||
@alias('model.options')
|
||||
options;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { action } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
@classic
|
||||
export default class CreateOptionsController extends Controller {
|
||||
@action
|
||||
nextPage() {
|
||||
if (this.isFindADate) {
|
||||
this.transitionToRoute('create.options-datetime');
|
||||
} else {
|
||||
this.transitionToRoute('create.settings');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
this.transitionToRoute('create.meta');
|
||||
}
|
||||
|
||||
@alias('model.isFindADate')
|
||||
isFindADate;
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { action, computed } from '@ember/object';
|
||||
import answersForAnswerType from 'croodle/utils/answers-for-answer-type';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
import moment from 'moment';
|
||||
|
||||
const Validations = buildValidations({
|
||||
anonymousUser: validator('presence', {
|
||||
presence: true,
|
||||
dependentKeys: ['model.intl.locale']
|
||||
}),
|
||||
answerType: [
|
||||
validator('presence', {
|
||||
presence: true,
|
||||
dependentKeys: ['model.intl.locale']
|
||||
}),
|
||||
validator('inclusion', {
|
||||
in: ['YesNo', 'YesNoMaybe', 'FreeText'],
|
||||
dependentKeys: ['model.intl.locale']
|
||||
})
|
||||
],
|
||||
forceAnswer: validator('presence', true)
|
||||
});
|
||||
|
||||
export default class CreateSettings extends Controller.extend(Validations) {
|
||||
@service
|
||||
encryption;
|
||||
|
||||
@service
|
||||
intl;
|
||||
|
||||
@alias('model.anonymousUser')
|
||||
anonymousUser;
|
||||
|
||||
@alias('model.answerType')
|
||||
answerType;
|
||||
|
||||
@computed
|
||||
get answerTypes() {
|
||||
return [
|
||||
{ id: 'YesNo', labelTranslation: 'answerTypes.yesNo.label' },
|
||||
{ id: 'YesNoMaybe', labelTranslation: 'answerTypes.yesNoMaybe.label' },
|
||||
{ id: 'FreeText', labelTranslation: 'answerTypes.freeText.label' },
|
||||
];
|
||||
}
|
||||
|
||||
@computed('model.expirationDate')
|
||||
get expirationDuration() {
|
||||
// TODO: must be calculated based on model.expirationDate
|
||||
return 'P3M';
|
||||
}
|
||||
set expirationDuration(value) {
|
||||
this.set(
|
||||
'model.expirationDate',
|
||||
isPresent(value) ? moment().add(moment.duration(value)).toISOString(): ''
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@computed
|
||||
get expirationDurations() {
|
||||
return [
|
||||
{ id: 'P7D', labelTranslation: 'create.settings.expirationDurations.P7D' },
|
||||
{ id: 'P1M', labelTranslation: 'create.settings.expirationDurations.P1M' },
|
||||
{ id: 'P3M', labelTranslation: 'create.settings.expirationDurations.P3M' },
|
||||
{ id: 'P6M', labelTranslation: 'create.settings.expirationDurations.P6M' },
|
||||
{ id: 'P1Y', labelTranslation: 'create.settings.expirationDurations.P1Y' },
|
||||
{ id: '', labelTranslation: 'create.settings.expirationDurations.never' },
|
||||
];
|
||||
}
|
||||
|
||||
@alias('model.forceAnswer')
|
||||
forceAnswer;
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
let { isFindADate } = this.model;
|
||||
|
||||
if (isFindADate) {
|
||||
this.transitionToRoute('create.options-datetime');
|
||||
} else {
|
||||
this.transitionToRoute('create.options');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async submit() {
|
||||
if (!this.validations.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
let poll = this.model;
|
||||
|
||||
// set timezone if there is atleast one option with time
|
||||
if (
|
||||
poll.isFindADate &&
|
||||
poll.options.any(({ title }) => {
|
||||
return !moment(title, 'YYYY-MM-DD', true).isValid();
|
||||
})
|
||||
) {
|
||||
this.set('model.timezone', moment.tz.guess());
|
||||
}
|
||||
|
||||
// save poll
|
||||
try {
|
||||
await poll.save();
|
||||
} catch(err) {
|
||||
this.flashMessages.danger('error.poll.savingFailed');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
// reload as workaround for bug: duplicated records after save
|
||||
await poll.reload();
|
||||
|
||||
// redirect to new poll
|
||||
await this.transitionToRoute('poll', poll, {
|
||||
queryParams: {
|
||||
encryptionKey: this.encryption.key,
|
||||
},
|
||||
});
|
||||
} catch(err) {
|
||||
// TODO: show feedback to user
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateAnswerType(answerType) {
|
||||
this.set('model.answerType', answerType);
|
||||
this.set('model.answers', answersForAnswerType(answerType));
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.intl.locale;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
@classic
|
||||
export default class ErrorController extends Controller {}
|
|
@ -1,16 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { computed } from '@ember/object';
|
||||
import { equal } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import sjcl from 'sjcl';
|
||||
|
||||
@classic
|
||||
export default class PollErrorController extends Controller {
|
||||
@computed('model')
|
||||
get decryptionFailed() {
|
||||
return this.model instanceof sjcl.exception.corrupt;
|
||||
}
|
||||
|
||||
@equal('model.errors.firstObject.status', '404')
|
||||
notFound;
|
||||
}
|
13
app/controllers/poll-error.ts
Normal file
13
app/controllers/poll-error.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Controller from '@ember/controller';
|
||||
import sjcl from 'sjcl';
|
||||
import { NotFoundError } from '../utils/api';
|
||||
|
||||
export default class PollErrorController extends Controller {
|
||||
get decryptionFailed() {
|
||||
return this.model instanceof sjcl.exception.corrupt;
|
||||
}
|
||||
|
||||
get notFound() {
|
||||
return this.model instanceof NotFoundError;
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { readOnly } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { observes } from '@ember-decorators/object';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class PollController extends Controller {
|
||||
@service
|
||||
encryption;
|
||||
|
||||
@service
|
||||
flashMessages;
|
||||
|
||||
@service
|
||||
intl;
|
||||
|
||||
@service
|
||||
router;
|
||||
|
||||
queryParams = ['encryptionKey'];
|
||||
|
||||
encryptionKey = '';
|
||||
timezoneChoosen = false;
|
||||
useLocalTimezone = false;
|
||||
|
||||
@readOnly('intl.primaryLocale')
|
||||
currentLocale;
|
||||
|
||||
@computed('currentLocale')
|
||||
get momentLongDayFormat() {
|
||||
let currentLocale = this.currentLocale;
|
||||
return moment.localeData(currentLocale)
|
||||
.longDateFormat('LLLL')
|
||||
.replace(
|
||||
moment.localeData(currentLocale).longDateFormat('LT'), '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@readOnly('model')
|
||||
poll;
|
||||
|
||||
@computed('router.currentURL', 'encryptionKey')
|
||||
get pollUrl() {
|
||||
return window.location.href;
|
||||
}
|
||||
|
||||
@computed('poll.expirationDate')
|
||||
get showExpirationWarning() {
|
||||
let expirationDate = this.poll.expirationDate;
|
||||
if (isEmpty(expirationDate)) {
|
||||
return false;
|
||||
}
|
||||
return moment().add(2, 'weeks').isAfter(moment(expirationDate));
|
||||
}
|
||||
|
||||
/*
|
||||
* return true if current timezone differs from timezone poll got created with
|
||||
*/
|
||||
@computed('poll.timezone')
|
||||
get timezoneDiffers() {
|
||||
let modelTimezone = this.poll.timezone;
|
||||
return isPresent(modelTimezone) && moment.tz.guess() !== modelTimezone;
|
||||
}
|
||||
|
||||
@computed('timezoneDiffers', 'timezoneChoosen')
|
||||
get mustChooseTimezone() {
|
||||
return this.timezoneDiffers && !this.timezoneChoosen;
|
||||
}
|
||||
|
||||
@computed('useLocalTimezone')
|
||||
get timezone() {
|
||||
return this.useLocalTimezone ? undefined : this.poll.timezone;
|
||||
}
|
||||
|
||||
@action
|
||||
linkAction(type) {
|
||||
let flashMessages = this.flashMessages;
|
||||
switch (type) {
|
||||
case 'copied':
|
||||
flashMessages.success(`poll.link.copied`);
|
||||
break;
|
||||
|
||||
case 'selected':
|
||||
flashMessages.info(`poll.link.selected`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
useLocalTimezone() {
|
||||
this.set('useLocalTimezone', true);
|
||||
this.set('timezoneChoosen', true);
|
||||
}
|
||||
|
||||
// TODO: Remove this code. It's spooky.
|
||||
@observes('encryptionKey')
|
||||
preventEncryptionKeyChanges() {
|
||||
if (
|
||||
!isEmpty(this.encryption.key) &&
|
||||
this.encryptionKey !== this.encryption.key
|
||||
) {
|
||||
// work-a-round for url not being updated
|
||||
window.location.hash = window.location.hash.replace(this.encryptionKey, this.encryption.key);
|
||||
|
||||
this.set('encryptionKey', this.encryption.key);
|
||||
}
|
||||
}
|
||||
}
|
68
app/controllers/poll.ts
Normal file
68
app/controllers/poll.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
import { action } from '@ember/object';
|
||||
import { DateTime } from 'luxon';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { PollRouteModel } from 'croodle/routes/poll';
|
||||
|
||||
export default class PollController extends Controller {
|
||||
@service declare intl: IntlService;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: PollRouteModel;
|
||||
|
||||
queryParams = ['encryptionKey'];
|
||||
encryptionKey = '';
|
||||
|
||||
@tracked timezoneChoosen = false;
|
||||
@tracked shouldUseLocalTimezone = false;
|
||||
|
||||
get showExpirationWarning() {
|
||||
const { model: poll } = this;
|
||||
const { expirationDate } = poll;
|
||||
|
||||
if (isEmpty(expirationDate)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
DateTime.local().plus({ weeks: 2 }) >= DateTime.fromISO(expirationDate)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* return true if current timezone differs from timezone poll got created with
|
||||
*/
|
||||
get timezoneDiffers() {
|
||||
const { model: poll } = this;
|
||||
const { timezone: pollTimezone } = poll;
|
||||
|
||||
return (
|
||||
isPresent(pollTimezone) &&
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone !== pollTimezone
|
||||
);
|
||||
}
|
||||
|
||||
get mustChooseTimezone() {
|
||||
return this.timezoneDiffers && !this.timezoneChoosen;
|
||||
}
|
||||
|
||||
get timezone() {
|
||||
const { model: poll, shouldUseLocalTimezone } = this;
|
||||
|
||||
return shouldUseLocalTimezone || !poll.timezone ? undefined : poll.timezone;
|
||||
}
|
||||
|
||||
@action
|
||||
useLocalTimezone() {
|
||||
this.shouldUseLocalTimezone = true;
|
||||
this.timezoneChoosen = true;
|
||||
}
|
||||
|
||||
@action
|
||||
usePollTimezone() {
|
||||
this.timezoneChoosen = true;
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { readOnly, not, gt, and } from '@ember/object/computed';
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
|
||||
@classic
|
||||
export default class PollEvaluationController extends Controller {
|
||||
@readOnly('intl.primaryLocale')
|
||||
currentLocale;
|
||||
|
||||
@readOnly('poll.hasTimes')
|
||||
hasTimes;
|
||||
|
||||
@service
|
||||
intl;
|
||||
|
||||
@readOnly('pollController.momentLongDayFormat')
|
||||
momentLongDayFormat;
|
||||
|
||||
@readOnly('model')
|
||||
poll;
|
||||
|
||||
@controller('poll')
|
||||
pollController;
|
||||
|
||||
@readOnly('pollController.timezone')
|
||||
timezone;
|
||||
|
||||
@readOnly('poll.users')
|
||||
users;
|
||||
|
||||
/*
|
||||
* evaluates poll data
|
||||
* if free text answers are allowed evaluation is disabled
|
||||
*/
|
||||
@computed('users.[]')
|
||||
get evaluation() {
|
||||
if (!this.isEvaluable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let evaluation = [];
|
||||
let options = [];
|
||||
let lookup = [];
|
||||
|
||||
// init options array
|
||||
this.poll.options.forEach((option, index) => {
|
||||
options[index] = 0;
|
||||
});
|
||||
|
||||
// init array of evalutation objects
|
||||
// create object for every possible answer
|
||||
this.poll.answers.forEach((answer) => {
|
||||
evaluation.push({
|
||||
id: answer.label,
|
||||
label: answer.label,
|
||||
options: [...options],
|
||||
});
|
||||
});
|
||||
// create object for no answer if answers are not forced
|
||||
if (!this.poll.forceAnswer) {
|
||||
evaluation.push({
|
||||
id: null,
|
||||
label: 'no answer',
|
||||
options: [...options],
|
||||
});
|
||||
}
|
||||
|
||||
// create lookup array
|
||||
evaluation.forEach(function(value, index) {
|
||||
lookup[value.id] = index;
|
||||
});
|
||||
|
||||
// loop over all users
|
||||
this.poll.users.forEach((user) => {
|
||||
// loop over all selections of the user
|
||||
user.selections.forEach(function(selection, optionIndex) {
|
||||
let answerIndex;
|
||||
|
||||
// get answer index by lookup array
|
||||
if (typeof lookup[selection.value.label] === 'undefined') {
|
||||
answerIndex = lookup[null];
|
||||
} else {
|
||||
answerIndex = lookup[selection.get('value.label')];
|
||||
}
|
||||
|
||||
// increment counter
|
||||
try {
|
||||
evaluation[answerIndex].options[optionIndex]++;
|
||||
} catch (e) {
|
||||
// ToDo: Throw an error
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return evaluation;
|
||||
}
|
||||
|
||||
@gt('poll.users.length', 0)
|
||||
hasUsers;
|
||||
|
||||
@not('poll.isFreeText')
|
||||
isNotFreeText;
|
||||
|
||||
@and('hasUsers', 'isNotFreeText')
|
||||
isEvaluable;
|
||||
}
|
21
app/controllers/poll/evaluation.ts
Normal file
21
app/controllers/poll/evaluation.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
import type PollController from '../poll';
|
||||
import type { PollEvaluationRouteModel } from 'croodle/routes/poll/evaluation';
|
||||
|
||||
export default class PollEvaluationController extends Controller {
|
||||
@service declare intl: IntlService;
|
||||
|
||||
@controller('poll') declare pollController: PollController;
|
||||
|
||||
declare model: PollEvaluationRouteModel;
|
||||
|
||||
get isEvaluable() {
|
||||
const { model: poll } = this;
|
||||
const { isFreeText, users } = poll;
|
||||
const hasUsers = users.length > 0;
|
||||
|
||||
return hasUsers && !isFreeText;
|
||||
}
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
import classic from 'ember-classic-decorator';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { not, readOnly } from '@ember/object/computed';
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
import EmberObject, { computed } from '@ember/object';
|
||||
import {
|
||||
validator, buildValidations
|
||||
}
|
||||
from 'ember-cp-validations';
|
||||
import moment from 'moment';
|
||||
import config from 'croodle/config/environment';
|
||||
|
||||
const validCollection = function(collection) {
|
||||
// return false if any object in collection is inValid
|
||||
return !collection.any((object) => {
|
||||
return object.get('validations.isInvalid');
|
||||
});
|
||||
};
|
||||
const Validations = buildValidations({
|
||||
name: [
|
||||
validator('presence', {
|
||||
presence: true,
|
||||
disabled: readOnly('model.anonymousUser'),
|
||||
dependentKeys: ['model.intl.locale']
|
||||
}),
|
||||
validator('unique', {
|
||||
parent: 'poll',
|
||||
attributeInParent: 'users',
|
||||
dependentKeys: ['model.poll.users.[]', 'model.poll.users.@each.name', 'model.intl.locale'],
|
||||
disable: readOnly('model.anonymousUser'),
|
||||
messageKey: 'errors.uniqueName',
|
||||
ignoreNewRecords: true,
|
||||
})
|
||||
],
|
||||
|
||||
selections: [
|
||||
validator('collection', true),
|
||||
|
||||
// all selection objects must be valid
|
||||
// if forceAnswer is true in poll settings
|
||||
validator(validCollection, {
|
||||
dependentKeys: ['model.forceAnswer', 'model.selections.[]', 'model.selections.@each.value', 'model.intl.locale']
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const SelectionValidations = buildValidations({
|
||||
value: validator('presence', {
|
||||
presence: true,
|
||||
disabled: not('model.forceAnswer'),
|
||||
messageKey: computed('model.isFreeText', function() {
|
||||
return this.get('model.isFreeText') ? 'errors.present' : 'errors.answerRequired';
|
||||
}),
|
||||
dependentKeys: ['model.intl.locale']
|
||||
})
|
||||
});
|
||||
|
||||
@classic
|
||||
class SelectionObject extends EmberObject.extend(SelectionValidations) {
|
||||
@service intl;
|
||||
|
||||
value = null;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
// current locale needs to be consumed in order to be observeable
|
||||
// for localization of validation messages
|
||||
this.intl.locale;
|
||||
}
|
||||
}
|
||||
|
||||
export default Controller.extend(Validations, {
|
||||
actions: {
|
||||
async submit() {
|
||||
if (!this.get('validations.isValid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let poll = this.poll;
|
||||
let selections = this.selections.map(({ value }) => {
|
||||
if (value === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this.isFreeText) {
|
||||
return {
|
||||
label: value,
|
||||
};
|
||||
}
|
||||
|
||||
// map selection to answer if it's not freetext
|
||||
let answer = poll.answers.findBy('type', value);
|
||||
let { icon, label, labelTranslation, type } = answer;
|
||||
|
||||
return {
|
||||
icon,
|
||||
label,
|
||||
labelTranslation,
|
||||
type,
|
||||
};
|
||||
});
|
||||
let user = this.store.createRecord('user', {
|
||||
creationDate: new Date(),
|
||||
name: this.name,
|
||||
poll,
|
||||
selections,
|
||||
version: config.APP.version,
|
||||
});
|
||||
|
||||
this.set('newUserRecord', user);
|
||||
await this.actions.save.bind(this)();
|
||||
},
|
||||
async save() {
|
||||
let user = this.newUserRecord;
|
||||
|
||||
try {
|
||||
await user.save();
|
||||
|
||||
this.set('savingFailed', false);
|
||||
} catch (error) {
|
||||
// couldn't save user model
|
||||
this.set('savingFailed', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// reset form
|
||||
this.set('name', '');
|
||||
this.selections.forEach((selection) => {
|
||||
selection.set('value', null);
|
||||
});
|
||||
|
||||
this.transitionToRoute('poll.evaluation', this.model, {
|
||||
queryParams: { encryptionKey: this.encryption.key }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
anonymousUser: readOnly('poll.anonymousUser'),
|
||||
currentLocale: readOnly('intl.locale'),
|
||||
encryption: service(),
|
||||
forceAnswer: readOnly('poll.forceAnswer'),
|
||||
intl: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
// current locale needs to be consumed in order to be observeable
|
||||
// for localization of validation messages
|
||||
this.intl.locale;
|
||||
},
|
||||
|
||||
isFreeText: readOnly('poll.isFreeText'),
|
||||
isFindADate: readOnly('poll.isFindADate'),
|
||||
|
||||
momentLongDayFormat: readOnly('pollController.momentLongDayFormat'),
|
||||
|
||||
name: '',
|
||||
|
||||
options: readOnly('poll.options'),
|
||||
|
||||
poll: readOnly('model'),
|
||||
pollController: controller('poll'),
|
||||
|
||||
possibleAnswers: computed('poll.answers', function() {
|
||||
return this.get('poll.answers').map((answer) => {
|
||||
const owner = getOwner(this);
|
||||
|
||||
const AnswerObject = EmberObject.extend({
|
||||
icon: answer.get('icon'),
|
||||
type: answer.get('type')
|
||||
});
|
||||
|
||||
if (!isEmpty(answer.get('labelTranslation'))) {
|
||||
return AnswerObject.extend({
|
||||
intl: service(),
|
||||
label: computed('intl.locale', function() {
|
||||
return this.intl.t(this.labelTranslation);
|
||||
}),
|
||||
labelTranslation: answer.get('labelTranslation'),
|
||||
}).create(owner.ownerInjection());
|
||||
} else {
|
||||
return AnswerObject.extend({
|
||||
label: answer.get('label')
|
||||
});
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
savingFailed: false,
|
||||
|
||||
selections: computed('options', 'pollController.dates', function() {
|
||||
let options = this.options;
|
||||
let isFindADate = this.isFindADate;
|
||||
let lastDate;
|
||||
|
||||
return options.map((option) => {
|
||||
let labelValue;
|
||||
let momentFormat;
|
||||
let value = option.get('title');
|
||||
|
||||
// format label
|
||||
if (isFindADate) {
|
||||
let hasTime = value.length > 10; // 'YYYY-MM-DD'.length === 10
|
||||
let timezone = this.timezone;
|
||||
let date = isPresent(timezone) ? moment.tz(value, timezone) : moment(value);
|
||||
if (hasTime && lastDate && date.format('YYYY-MM-DD') === lastDate.format('YYYY-MM-DD')) {
|
||||
labelValue = value;
|
||||
// do not repeat dates for different times
|
||||
momentFormat = 'LT';
|
||||
} else {
|
||||
labelValue = value;
|
||||
momentFormat = hasTime ? 'LLLL' : 'day';
|
||||
lastDate = date;
|
||||
}
|
||||
} else {
|
||||
labelValue = value;
|
||||
}
|
||||
|
||||
// https://github.com/offirgolan/ember-cp-validations#basic-usage---objects
|
||||
// To lookup validators, container access is required which can cause an issue with Object creation
|
||||
// if the object is statically imported. The current fix for this is as follows.
|
||||
let owner = getOwner(this);
|
||||
return SelectionObject.create(owner.ownerInjection(), {
|
||||
labelValue,
|
||||
momentFormat,
|
||||
|
||||
// forceAnswer and isFreeText must be included in model
|
||||
// cause otherwise validations can't depend on it
|
||||
forceAnswer: this.forceAnswer,
|
||||
isFreeText: this.isFreeText,
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
timezone: readOnly('pollController.timezone')
|
||||
});
|
107
app/controllers/poll/participation.ts
Normal file
107
app/controllers/poll/participation.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import User from '../../models/user';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type PollController from '../poll';
|
||||
import type { PollParticipationRouteModel } from 'croodle/routes/poll/participation';
|
||||
import type Poll from 'croodle/models/poll';
|
||||
import type { SelectionInput } from 'croodle/models/selection';
|
||||
|
||||
export default class PollParticipationController extends Controller {
|
||||
@service declare router: RouterService;
|
||||
|
||||
@controller('poll') declare pollController: PollController;
|
||||
|
||||
declare model: PollParticipationRouteModel;
|
||||
|
||||
@tracked name = '';
|
||||
@tracked savingFailed = false;
|
||||
|
||||
newUserData: {
|
||||
name: string | null;
|
||||
poll: Poll;
|
||||
selections: SelectionInput[];
|
||||
} | null = null;
|
||||
|
||||
@action
|
||||
async submit() {
|
||||
const { formData, poll } = this.model;
|
||||
const { name } = formData;
|
||||
const { answers, isFreeText } = poll;
|
||||
const selections = formData.selections.map(({ value }) => {
|
||||
if (value === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isFreeText) {
|
||||
return {
|
||||
label: value,
|
||||
};
|
||||
}
|
||||
|
||||
// map selection to answer if it's not freetext
|
||||
const answer = answers.find(({ type }) => type === value);
|
||||
if (!answer) {
|
||||
throw new Error('Mapping selection to answer failed');
|
||||
}
|
||||
|
||||
const { icon, labelTranslation, type } = answer;
|
||||
|
||||
return {
|
||||
icon,
|
||||
labelTranslation,
|
||||
type,
|
||||
};
|
||||
});
|
||||
|
||||
this.newUserData = {
|
||||
name,
|
||||
poll,
|
||||
selections,
|
||||
};
|
||||
await this.save();
|
||||
}
|
||||
|
||||
@action
|
||||
async save() {
|
||||
const { model, newUserData: userData } = this;
|
||||
const { poll } = model;
|
||||
// As know that the route is `poll.participation`, which means that there
|
||||
// is a parent `poll` for sure.
|
||||
const { encryptionKey } = this.router.currentRoute?.parent?.queryParams as {
|
||||
encryptionKey: string;
|
||||
};
|
||||
|
||||
if (!userData) {
|
||||
throw new Error(
|
||||
'save method called before submit method has set the user data',
|
||||
);
|
||||
}
|
||||
|
||||
if (!encryptionKey) {
|
||||
throw new Error('Can not lookup encryption key');
|
||||
}
|
||||
|
||||
try {
|
||||
await User.create(userData, encryptionKey);
|
||||
|
||||
this.savingFailed = false;
|
||||
} catch (error) {
|
||||
// couldn't save user model
|
||||
this.savingFailed = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.transitionTo('poll.evaluation', poll.id, {
|
||||
queryParams: { encryptionKey },
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
resetSavingStatus() {
|
||||
this.savingFailed = false;
|
||||
}
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
export default {
|
||||
};
|
||||
export default {};
|
||||
|
|
35
app/helpers/format-date-relative.ts
Normal file
35
app/helpers/format-date-relative.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Helper from '@ember/component/helper';
|
||||
import { DateTime } from 'luxon';
|
||||
import { inject as service } from '@ember/service';
|
||||
import type IntlService from 'ember-intl/services/intl';
|
||||
|
||||
type Positional = [date: Date | string];
|
||||
|
||||
export interface FormatDateRelativeHelperSignature {
|
||||
Args: {
|
||||
Positional: Positional;
|
||||
};
|
||||
Return: string;
|
||||
}
|
||||
|
||||
export default class FormatDateRelative extends Helper<FormatDateRelativeHelperSignature> {
|
||||
@service declare intl: IntlService;
|
||||
|
||||
compute([dateOrIsoString]: Positional) {
|
||||
const isoString =
|
||||
dateOrIsoString instanceof Date
|
||||
? dateOrIsoString.toISOString()
|
||||
: dateOrIsoString;
|
||||
|
||||
return DateTime.fromISO(isoString).toRelative({
|
||||
locale: this.intl.primaryLocale,
|
||||
padding: 1000,
|
||||
})!;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@glint/environment-ember-loose/registry' {
|
||||
export default interface Registry {
|
||||
'format-date-relative': typeof FormatDateRelative;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue