remove old client
@ -18,7 +18,7 @@ matrix:
|
||||
- flake8 openslides tests
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- python -m mypy openslides/
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=75
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=70
|
||||
|
||||
- language: python
|
||||
cache:
|
||||
@ -35,7 +35,7 @@ matrix:
|
||||
- flake8 openslides tests
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- python -m mypy openslides/
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=75
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=70
|
||||
|
||||
- language: node_js
|
||||
node_js:
|
||||
|
@ -43,31 +43,18 @@ See step 1. b. in the installation section in the `README.rst
|
||||
<https://github.com/OpenSlides/OpenSlides/blob/master/README.rst>`_.
|
||||
|
||||
|
||||
d. Install dependencies
|
||||
'''''''''''''''''''''''
|
||||
d. Finish the server
|
||||
''''''''''''''''''''
|
||||
|
||||
Install all required Python packages::
|
||||
|
||||
$ pip install --requirement requirements.txt
|
||||
|
||||
Install all Node.js and Bower packages and run several JavaScript build tasks::
|
||||
Create a settings file, run migrations and start the server::
|
||||
|
||||
$ yarn
|
||||
|
||||
Optional: To enhance performance run Gulp in production mode::
|
||||
|
||||
$ node_modules/.bin/gulp --production
|
||||
|
||||
|
||||
e. Start OpenSlides
|
||||
'''''''''''''''''''
|
||||
|
||||
Use the command-line interface::
|
||||
|
||||
$ python manage.py start
|
||||
|
||||
See step 1. d. in the installation section in the `README.rst
|
||||
<https://github.com/OpenSlides/OpenSlides/blob/master/README.rst>`_.
|
||||
$ python manage.py createsettings
|
||||
$ python manage.py migrate
|
||||
$ python manage.py runserver
|
||||
|
||||
To get help on the command line options run::
|
||||
|
||||
@ -84,13 +71,25 @@ When debugging something email related change the email backend to console::
|
||||
|
||||
$ python manage.py start --debug-email
|
||||
|
||||
To start OpenSlides with Daphne run::
|
||||
e. Setup and start the client
|
||||
'''''''''''''''''''''''''''''
|
||||
|
||||
$ python manage.py runserver
|
||||
Go in the client's directory in a second command-line interface::
|
||||
|
||||
Use gulp watch in a second command-line interface::
|
||||
$ cd client/
|
||||
|
||||
$ node_modules/.bin/gulp watch
|
||||
Install all dependencies and start the development server::
|
||||
|
||||
$ npm install
|
||||
$ yarn start
|
||||
|
||||
Now the client is available under ``localhost:4200``.
|
||||
|
||||
If you do not need to work with the client, you can build the client and let it be delivered by the server directly::
|
||||
|
||||
$ yarn build
|
||||
|
||||
The client's address is now ``localhost:8000``.
|
||||
|
||||
|
||||
2. Installation on Windows
|
||||
@ -127,16 +126,20 @@ To run some server tests see `.travis.yml
|
||||
<https://github.com/OpenSlides/OpenSlides/blob/master/.travis.yml>`_.
|
||||
|
||||
|
||||
b. Running AngularJS test cases
|
||||
'''''''''''''''''''''''''''''''
|
||||
b. Client tests and commands
|
||||
''''''''''''''''''''''''''''
|
||||
|
||||
Run client tests by starting karma::
|
||||
Change to the client's directory to run every client related command. Run client tests::
|
||||
|
||||
$ yarn run karma
|
||||
$ yarn test
|
||||
|
||||
Watch for file changes and run the tests automatically after each change::
|
||||
Fix the code format and lint it with::
|
||||
|
||||
$ yarn run karma:watch
|
||||
$ yarn pretty-quick && yarn lint
|
||||
|
||||
To extract translations run::
|
||||
|
||||
$ yarn extract
|
||||
|
||||
OpenSlides in big mode
|
||||
======================
|
||||
@ -207,23 +210,7 @@ This is an example configuration for a single Daphne listen on port 8000::
|
||||
|
||||
server_name _;
|
||||
|
||||
location ~* ^/projector.*$ {
|
||||
rewrite ^.*$ /static/templates/projector-container.html;
|
||||
}
|
||||
location ~* ^/real-projector.*$ {
|
||||
rewrite ^.*$ /static/templates/projector.html;
|
||||
}
|
||||
location ~* ^/webclient.*$ {
|
||||
rewrite ^/webclient/(site|projector).*$ /static/js/webclient-$1.js;
|
||||
}
|
||||
location /static {
|
||||
alias <your path to>/collected-static;
|
||||
}
|
||||
location ~* ^/(?!ws|wss|media|rest|views).*$ {
|
||||
rewrite ^.*$ /static/templates/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
location ~* ^/(ws|wss|media|rest|apps).*$ {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@ -232,4 +219,7 @@ This is an example configuration for a single Daphne listen on port 8000::
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
}
|
||||
location / {
|
||||
alias <your path to>/collected-static;
|
||||
}
|
||||
}
|
||||
|
48
README.rst
@ -165,53 +165,7 @@ OpenSlides uses the following projects or parts of them:
|
||||
|
||||
* Several Python packages (see ``requirements/production.txt`` and ``requirements/big_mode.txt``).
|
||||
|
||||
* Several JavaScript packages (see ``bower.json``)
|
||||
|
||||
* `angular <http://angularjs.org>`_, License: MIT
|
||||
* `angular-animate <http://angularjs.org>`_, License: MIT
|
||||
* `angular-bootstrap-colorpicker <https://github.com/buberdds/angular-bootstrap-colorpicker>`_, License: MIT
|
||||
* `angular-chosen-localytics <http://github.com/leocaseiro/angular-chosen>`_, License: MIT
|
||||
* `angular-ckeditor <https://github.com/lemonde/angular-ckeditor>`_, License: MIT
|
||||
* `angular-file-saver <https://github.com/alferov/angular-file-saver>`_, License: MIT
|
||||
* `angular-formly <http://formly-js.github.io/angular-formly/>`_, License: MIT
|
||||
* `angular-formly-templates-bootstrap <https://github.com/formly-js/angular-formly-templates-bootstrap>`_, License: MIT
|
||||
* `angular-gettext <http://angular-gettext.rocketeer.be/>`_, License: MIT
|
||||
* `angular-messages <http://angularjs.org>`_, License: MIT
|
||||
* `angular-pdf <http://github.com/sayanee/angularjs-pdf>`_, License: MIT
|
||||
* `angular-sanitize <http://angularjs.org>`_, License: MIT
|
||||
* `angular-scroll-glue <https://github.com/Luegg/angularjs-scroll-glue>`_, License: MIT
|
||||
* `angular-ui-bootstrap <http://angular-ui.github.io/bootstrap/>`_, License: MIT
|
||||
* `angular-ui-router <http://angular-ui.github.io/ui-router/>`_, License: MIT
|
||||
* `angular-ui-router-title <https://github.com/nonplus/angular-ui-router-title>`_, License: MIT
|
||||
* `angular-ui-tree <https://github.com/angular-ui-tree/angular-ui-tree>`_, License: MIT
|
||||
* `angular-xeditable <https://github.com/vitalets/angular-xeditable>`_, License: MIT
|
||||
* `angularjs-scroll-glue <https://github.com/Luegg/angularjs-scroll-glue>`_, License: MIT
|
||||
* `angularjs-slider <https://github.com/angular-slider/angularjs-slider>`_, License: MIT
|
||||
* `api-check <https://github.com/kentcdodds/api-check>`_, License: MIT
|
||||
* `blob-polyfill <https://github.com/bjornstar/blob-polyfill>`_, License: MIT
|
||||
* `bootstrap <http://getbootstrap.com>`_, License: MIT
|
||||
* `bootstrap <http://getbootstrap.com>`_, License: MIT
|
||||
* `bootstrap-css-only <https://getbootstrap.com/>`_, License: MIT
|
||||
* `bootstrap-ui-datetime-picker <https://github.com/Gillardo/bootstrap-ui-datetime-picker>`_, License: MIT
|
||||
* `chosen <https://harvesthq.github.io/chosen/>`_, License: MIT
|
||||
* `ckeditor <https://ckeditor.com/ckeditor-4/>`_, License: (GPL-2.0 OR LGPL-2.1 OR MPL-1.1)
|
||||
* `docxtemplater <https://github.com/open-xml-templating/docxtemplater>`_, License: MIT
|
||||
* `file-saver.js <https://github.com/Teleborder/FileSaver.js>`_, License: LICENSE.md
|
||||
* `font-awesome-bower <https://github.com/tdg5/font-awesome-bower>`_, License: MIT
|
||||
* `jquery <https://jquery.com>`_, License: MIT
|
||||
* `jquery.cookie <https://plugins.jquery.com/cookie>`_, License: MIT
|
||||
* `js-data <http://www.js-data.io>`_, License: MIT
|
||||
* `js-data-angular <https://github.com/js-data/js-data-angular>`_, License: MIT
|
||||
* `jszip <http://stuartk.com/jszip>`_, License: MIT or GPLv3
|
||||
* `lodash <https://lodash.com/>`_, License: MIT
|
||||
* `ng-dialog <https://github.com/likeastore/ngDialog>`_, License: MIT
|
||||
* `ng-file-upload <https://github.com/danialfarid/ng-file-upload>`_, License: MIT
|
||||
* `ngbootbox <https://github.com/eriktufvesson/ngBootbox>`_, License: MIT
|
||||
* `ngStorage <https://github.com/gsklee/ngStorage>`_, License: MIT
|
||||
* `papaparse <http://papaparse.com>`_, License: MIT
|
||||
* `pdfjs-dist <http://mozilla.github.io/pdf.js/>`_, License: Apache-2.0
|
||||
* `pdfmake <https://bpampuch.github.io/pdfmake>`_, License: MIT
|
||||
* `roboto-fontface <https://github.com/choffmeister/roboto-fontface-bower>`_, License: Apache-2.0
|
||||
* Several JavaScript packages (see ``client/package.json``)
|
||||
|
||||
|
||||
License and authors
|
||||
|
64
bower.json
@ -1,64 +0,0 @@
|
||||
{
|
||||
"name": "openslides",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "~3.3.7",
|
||||
"jquery": "~3.1.0",
|
||||
"angular": "~1.5.8",
|
||||
"angular-animate": "~1.5.8",
|
||||
"angular-bootstrap": "~2.1.3",
|
||||
"angular-bootstrap-colorpicker": "~3.0.25",
|
||||
"angular-chosen-localytics": "~1.5.0",
|
||||
"angular-ckeditor": "~1.0.3",
|
||||
"angular-file-saver": "~1.1.2",
|
||||
"angular-formly": "~8.4.0",
|
||||
"angular-formly-templates-bootstrap": "~6.2.0",
|
||||
"angular-gettext": "~2.3.7",
|
||||
"angular-messages": "~1.5.8",
|
||||
"angular-pdf": "1.3.0",
|
||||
"angular-sanitize": "~1.5.8",
|
||||
"angular-scroll-glue": "~2.0.7",
|
||||
"angular-ui-router": "~0.3.1",
|
||||
"angular-ui-tree": "https://github.com/FinnStutzenstein/angular-ui-tree.git#94dbaaff6b36f9d7a514843b73c28d2a3684c0c0",
|
||||
"angular-xeditable": "~0.5.0",
|
||||
"angularjs-slider": "~6.2.2",
|
||||
"bootstrap-css-only": "~3.3.6",
|
||||
"bootstrap-ui-datetime-picker": "~2.4.0",
|
||||
"ckeditor": "~4.7.2",
|
||||
"docxtemplater": "~2.1.5",
|
||||
"font-awesome-bower": "~4.7.0",
|
||||
"jquery.cookie": "~1.4.1",
|
||||
"js-data": "~2.9.0",
|
||||
"js-data-angular": "~3.2.1",
|
||||
"jszip": "~3.1.3",
|
||||
"lodash": "~4.16.0",
|
||||
"ng-dialog": "~0.6.4",
|
||||
"ng-file-upload": "~11.2.3",
|
||||
"ngstorage": "~0.3.11",
|
||||
"ngBootbox": "~0.1.3",
|
||||
"papaparse": "~4.1.2",
|
||||
"pdfmake": "0.1.37",
|
||||
"roboto-fontface": "~0.6.0"
|
||||
},
|
||||
"overrides": {
|
||||
"pdfmake": {
|
||||
"main": []
|
||||
},
|
||||
"pdfjs-dist": {
|
||||
"main": "build/pdf.combined.js"
|
||||
},
|
||||
"roboto-fontface": {
|
||||
"main": [
|
||||
"fonts/Roboto/Roboto-Regular.woff",
|
||||
"fonts/Roboto/Roboto-Medium.woff",
|
||||
"fonts/Roboto-Condensed/Roboto-Condensed-Regular.woff",
|
||||
"fonts/Roboto-Condensed/Roboto-Condensed-Light.woff"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": ">=1.5 <1.6",
|
||||
"jquery": ">=3.1 <3.2",
|
||||
"angular-bootstrap": "~2.1.3"
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/client",
|
||||
"outputPath": "../openslides/static",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
|
28
client/package-lock.json
generated
@ -5307,13 +5307,15 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": false,
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -5330,19 +5332,22 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -5473,7 +5478,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -5487,6 +5493,7 @@
|
||||
"resolved": false,
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -5503,6 +5510,7 @@
|
||||
"resolved": false,
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -5511,13 +5519,15 @@
|
||||
"version": "0.0.8",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"resolved": false,
|
||||
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@ -5538,6 +5548,7 @@
|
||||
"resolved": false,
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -5626,7 +5637,8 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -5640,6 +5652,7 @@
|
||||
"resolved": false,
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -5777,6 +5790,7 @@
|
||||
"resolved": false,
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -10,8 +10,7 @@
|
||||
"e2e": "ng e2e",
|
||||
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p src/tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -w -t -o --port",
|
||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
|
||||
"format:fix": "pretty-quick --staged",
|
||||
"precommit": "run-s format:fix lint"
|
||||
"format:fix": "pretty-quick --staged"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
362
gulpfile.js
@ -1,362 +0,0 @@
|
||||
/**
|
||||
* Gulp tasks for development and production.
|
||||
*
|
||||
* Run
|
||||
*
|
||||
* $ ./node_modules/.bin/gulp
|
||||
*
|
||||
* for development and
|
||||
*
|
||||
* $ ./node_modules/.bin/gulp --production
|
||||
*
|
||||
* for production mode.
|
||||
*/
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
gulp = require('gulp'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
gulpif = require('gulp-if'),
|
||||
gettext = require('gulp-angular-gettext'),
|
||||
inject = require('gulp-inject-string'),
|
||||
jshint = require('gulp-jshint'),
|
||||
mainBowerFiles = require('main-bower-files'),
|
||||
path = require('path'),
|
||||
rename = require('gulp-rename'),
|
||||
sass = require('gulp-sass'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
templateCache = require('gulp-angular-templatecache'),
|
||||
through = require('through2'),
|
||||
uglify = require('gulp-uglify'),
|
||||
vsprintf = require('sprintf-js').vsprintf;
|
||||
|
||||
|
||||
// Directory where the results go to
|
||||
var output_directory = path.join('openslides', 'static');
|
||||
|
||||
|
||||
// Container for all watchers
|
||||
var watchers = [];
|
||||
|
||||
|
||||
/**
|
||||
* Default tasks to be run before start.
|
||||
*/
|
||||
|
||||
// Catches all JavaScript files (excluded worker files) from all core apps and concats them to one
|
||||
// file js/openslides.js. In production mode the file is uglified.
|
||||
var js_src = [
|
||||
path.join('openslides', '*', 'static', 'js', '**', '*.js'),
|
||||
'!' + path.join('openslides', 'core', 'static', 'js', 'core', 'pdf-worker.js'),
|
||||
];
|
||||
|
||||
gulp.task('js', function () {
|
||||
return gulp.src(js_src)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('openslides.js'))
|
||||
.pipe(sourcemaps.write())
|
||||
//TODO: Needs rework in all js files that uglified code works correctly.
|
||||
//.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'js')));
|
||||
});
|
||||
|
||||
watchers.push(function () {
|
||||
gulp.watch(js_src, gulp.series('js'));
|
||||
});
|
||||
|
||||
|
||||
// Catches all JavaScript files from all bower components and concats them to
|
||||
// one file js/openslides-libs.js. In production mode the file is uglified.
|
||||
gulp.task('js_libs', function () {
|
||||
return gulp.src(mainBowerFiles({
|
||||
filter: /\.js$/
|
||||
}))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('openslides-libs.js'))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(inject.prepend("/* set basepath of CKEditor */\n" +
|
||||
"window.CKEDITOR_BASEPATH = '/static/ckeditor/';\n\n"))
|
||||
.pipe(inject.prepend("/* Workaround for IE and pdfjs-dist#1.3.100 (see PR#3714) */\n" +
|
||||
"PDFJS = {workerSrc: 'not used but set'};\n\n"))
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'js')));
|
||||
});
|
||||
|
||||
|
||||
// Catches all pdfmake files for pdf worker and pdfmake library.
|
||||
var pdf_worker_src = path.join('openslides', 'core', 'static', 'js', 'core', 'pdf-worker.js');
|
||||
|
||||
gulp.task('pdf_worker', function () {
|
||||
return gulp.src(pdf_worker_src)
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'js', 'workers')));
|
||||
});
|
||||
|
||||
gulp.task('pdf_worker_libs', function () {
|
||||
return gulp.src(path.join('bower_components', 'pdfmake', 'build', 'pdfmake.min.js'))
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(rename('pdf-worker-libs.js'))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'js', 'workers')));
|
||||
});
|
||||
|
||||
watchers.push(function () {
|
||||
gulp.watch(pdf_worker_src, gulp.series('pdf_worker'));
|
||||
});
|
||||
|
||||
|
||||
// Catches all template files from all core apps and concats them to one
|
||||
// file js/openslides-templates.js. In production mode the file is uglified.
|
||||
var templates_src = path.join('openslides', '*', 'static', 'templates', '**', '*.html');
|
||||
|
||||
gulp.task('templates', function () {
|
||||
return gulp.src(templates_src)
|
||||
.pipe(templateCache('openslides-templates.js', {
|
||||
module: 'OpenSlidesApp-templates',
|
||||
standalone: true,
|
||||
moduleSystem: 'IIFE',
|
||||
transformUrl: function (url) {
|
||||
var pathList = url.split(path.sep);
|
||||
pathList.shift();
|
||||
return pathList.join(path.sep);
|
||||
},
|
||||
}))
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'js')));
|
||||
});
|
||||
|
||||
watchers.push(function () {
|
||||
gulp.watch(templates_src, gulp.series('templates'));
|
||||
});
|
||||
|
||||
|
||||
// Build the openslides-site.css file from the main file core/static/css/site.scss.
|
||||
// Minimizes the outputfile if the production flag is given.
|
||||
gulp.task('css_site', function () {
|
||||
return gulp.src(path.join('openslides', 'core', 'static', 'css', 'site.scss'))
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(gulpif(argv.production, cssnano({safe: true})))
|
||||
.pipe(rename('openslides-site.css'))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'css')));
|
||||
});
|
||||
|
||||
|
||||
// Build the openslides-projector.css file from the main file core/static/css/projector.scss.
|
||||
// Minimizes the outputfile if the production flag is given.
|
||||
gulp.task('css_projector', function () {
|
||||
return gulp.src(path.join('openslides', 'core', 'static', 'css', 'projector.scss'))
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(gulpif(argv.production, cssnano({safe: true})))
|
||||
.pipe(rename('openslides-projector.css'))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'css')));
|
||||
});
|
||||
|
||||
|
||||
// Watcher for scss files.
|
||||
// We cannot differentiate between all scss files which belong to each realm. So if
|
||||
// one scss file changes the site and projector css is rebuild.
|
||||
watchers.push(function () {
|
||||
gulp.watch(path.join('openslides', '*', 'static', 'css', '**', '*.scss'), gulp.parallel('css_site', 'css_projector'));
|
||||
});
|
||||
|
||||
|
||||
// Catches all CSS files from all bower components and concats them to one file
|
||||
// css/openslides-libs.css. In production mode the file is uglified.
|
||||
gulp.task('css_libs', function () {
|
||||
return gulp.src(mainBowerFiles({
|
||||
filter: /\.css$/
|
||||
}))
|
||||
.pipe(concat('openslides-libs.css'))
|
||||
.pipe(gulpif(argv.production, cssnano({safe: true})))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'css')));
|
||||
});
|
||||
|
||||
|
||||
// Catches all font files from all bower components.
|
||||
gulp.task('fonts_libs', function () {
|
||||
return gulp.src(mainBowerFiles({
|
||||
filter: /\.(woff)|(woff2)$/
|
||||
}))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'fonts')));
|
||||
});
|
||||
|
||||
|
||||
// Catches image files for angular-chosen.
|
||||
gulp.task('angular_chosen_img', function () {
|
||||
return gulp.src(path.join('bower_components', 'chosen', '*.png'))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'css')));
|
||||
});
|
||||
|
||||
|
||||
// Tasks for CKEditor
|
||||
gulp.task('ckeditor_defaults', function () {
|
||||
return gulp.src([
|
||||
path.join('bower_components', 'ckeditor', 'styles.js'),
|
||||
path.join('bower_components', 'ckeditor', 'contents.css'),
|
||||
])
|
||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
|
||||
});
|
||||
|
||||
gulp.task('ckeditor_skins', function () {
|
||||
return gulp.src(
|
||||
[
|
||||
path.join('bower_components', 'ckeditor', 'skins', 'moono-lisa', '**', '*'),
|
||||
],
|
||||
{
|
||||
base: path.join('bower_components', 'ckeditor', 'skins')
|
||||
}
|
||||
)
|
||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor', 'skins')));
|
||||
});
|
||||
|
||||
gulp.task('ckeditor_plugins', function () {
|
||||
return gulp.src(
|
||||
[
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'clipboard', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'colorbutton', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'colordialog', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'dialog', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'find', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'image', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'justify', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'link', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'liststyle', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'magicline', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'pastefromword', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'panelbutton', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'showblocks', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'specialchar', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'sourcedialog', '**', '*'),
|
||||
path.join('bower_components', 'ckeditor', 'plugins', 'table', '**', '*'),
|
||||
],
|
||||
{
|
||||
base: path.join('bower_components', 'ckeditor', 'plugins')
|
||||
}
|
||||
)
|
||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor', 'plugins')));
|
||||
});
|
||||
|
||||
gulp.task('ckeditor_lang', function () {
|
||||
return gulp.src([
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'en.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'de.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'pt.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'es.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'fr.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'cs.js'),
|
||||
path.join('bower_components', 'ckeditor', 'lang', 'ru.js'),
|
||||
])
|
||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor', 'lang')));
|
||||
});
|
||||
|
||||
gulp.task('ckeditor', gulp.parallel('ckeditor_defaults', 'ckeditor_skins', 'ckeditor_plugins', 'ckeditor_lang'));
|
||||
|
||||
|
||||
// Compiles translation files (*.po) to *.json and saves them in the directory
|
||||
// openslides/static/i18n/.
|
||||
gulp.task('translations', function () {
|
||||
return gulp.src(path.join('openslides', 'locale', 'angular-gettext', '*.po'))
|
||||
.pipe(gettext.compile({
|
||||
format: 'json'
|
||||
}))
|
||||
.pipe(gulp.dest(path.join(output_directory, 'i18n')));
|
||||
});
|
||||
|
||||
|
||||
// Gulp default task. Runs all other tasks before.
|
||||
gulp.task('default', gulp.parallel(
|
||||
'js',
|
||||
'js_libs',
|
||||
'pdf_worker',
|
||||
'pdf_worker_libs',
|
||||
'templates',
|
||||
'css_site',
|
||||
'css_projector',
|
||||
'css_libs',
|
||||
'fonts_libs',
|
||||
'angular_chosen_img',
|
||||
'ckeditor',
|
||||
'translations'
|
||||
));
|
||||
|
||||
|
||||
/**
|
||||
* Extra tasks that have to be called manually. Useful for development.
|
||||
*/
|
||||
|
||||
// Watches changes in JavaScript and templates.
|
||||
gulp.task('watching', function () {
|
||||
// This tasks never completes because it starts all watchers and let them
|
||||
// watch forever ...
|
||||
for (var i = 0; i < watchers.length; i++) {
|
||||
watchers[i]();
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('watch', gulp.series(gulp.parallel('js', 'pdf_worker', 'templates', 'css_site', 'css_projector'), 'watching'));
|
||||
|
||||
|
||||
// Extracts translatable strings using angular-gettext and saves them in file
|
||||
// openslides/locale/angular-gettext/template-en.pot.
|
||||
gulp.task('pot', function () {
|
||||
return gulp.src([
|
||||
templates_src,
|
||||
path.join('openslides', '*', 'static', 'js', '**', '*.js'),
|
||||
])
|
||||
.pipe(gettext.extract('template-en.pot', {}))
|
||||
.pipe(gulp.dest('openslides/locale/angular-gettext/'));
|
||||
});
|
||||
|
||||
|
||||
// Checks JavaScript using JSHint
|
||||
gulp.task('jshint', function () {
|
||||
return gulp.src([
|
||||
'gulpfile.js',
|
||||
path.join('openslides', '*', 'static', 'js', '**', '*.js'),
|
||||
])
|
||||
.pipe(jshint())
|
||||
.pipe(jshint.reporter('default'))
|
||||
.pipe(jshint.reporter('fail'));
|
||||
});
|
||||
|
||||
|
||||
// Extracts names, URLs and licensed of all uses bower components and prints
|
||||
// it to the console. This is useful to update the README.rst during release
|
||||
// process.
|
||||
gulp.task('bower-components-for-readme', function () {
|
||||
var files = [];
|
||||
return gulp.src([
|
||||
path.join('bower_components', '*', 'bower.json'),
|
||||
path.join('bower_components', '*', 'package.json'),
|
||||
path.join('bower_components', '*', 'component.json'),
|
||||
])
|
||||
.pipe(
|
||||
through.obj(
|
||||
function (chunk, encoding, callback) {
|
||||
files.push(chunk);
|
||||
callback();
|
||||
},
|
||||
function (callback) {
|
||||
// Extract JSON from bower.json or components.json file.
|
||||
var extracted = [];
|
||||
for (var index = 0; index < files.length; index++) {
|
||||
extracted.push(JSON.parse(files[index].contents.toString()));
|
||||
}
|
||||
// Sort files.
|
||||
extracted.sort(function (a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
});
|
||||
// Print out line for README.rst.
|
||||
for (var index2 = 0; index2 < extracted.length; index2++) {
|
||||
var data = [
|
||||
extracted[index2].name,
|
||||
extracted[index2].homepage,
|
||||
extracted[index2].license,
|
||||
];
|
||||
console.log(vsprintf(' * `%s <%s>`_, License: %s', data));
|
||||
}
|
||||
// End stream without further file processing.
|
||||
callback();
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
/* List of speakers */
|
||||
.lastSpeakers {
|
||||
color: #9a9898;
|
||||
margin-left: 27px;
|
||||
}
|
||||
|
||||
.currentSpeaker {
|
||||
font-weight: bold;
|
||||
|
||||
i.fa-microphone {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.nextSpeakers {
|
||||
margin-left: 13px;
|
||||
|
||||
li {
|
||||
line-height: 150%;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
@import 'list_of_speakers';
|
||||
|
||||
/* Agenda list */
|
||||
.agendalist-table {
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
td.number {
|
||||
padding-left: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p.mainitem {
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
p.subitem {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
@import 'list_of_speakers';
|
||||
|
||||
#agenda-table {
|
||||
.icon-column {
|
||||
padding: 3px;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.title-column {
|
||||
padding-left: 3px;
|
||||
padding-right: 10px;
|
||||
width: calc(95% - 15px );
|
||||
}
|
||||
|
||||
.caret-spacer {
|
||||
width: 15px;
|
||||
padding: 3px;
|
||||
color: #337ab7;
|
||||
font-size: 115%;
|
||||
}
|
||||
}
|
||||
|
||||
.agenda-sort {
|
||||
.internal {
|
||||
padding: 7px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.angular-ui-tree-node {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.speakers-toolbar {
|
||||
margin: -20px -20px 30px -20px;
|
||||
padding: 12px 15px 10px 15px;
|
||||
|
||||
}
|
@ -1,441 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
|
||||
|
||||
.factory('Speaker', [
|
||||
'DS',
|
||||
function(DS) {
|
||||
return DS.defineResource({
|
||||
name: 'agenda/speaker',
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'users/user': {
|
||||
localField: 'user',
|
||||
localKey: 'user_id',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Agenda', [
|
||||
'$http',
|
||||
'DS',
|
||||
'Speaker',
|
||||
'jsDataModel',
|
||||
'Projector',
|
||||
'ProjectHelper',
|
||||
'gettextCatalog',
|
||||
'gettext',
|
||||
'CamelCase',
|
||||
'EditForm',
|
||||
function($http, DS, Speaker, jsDataModel, Projector, ProjectHelper, gettextCatalog,
|
||||
gettext, CamelCase, EditForm) {
|
||||
var name = 'agenda/item';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
verboseName: gettext('Agenda'),
|
||||
computed: {
|
||||
is_public: function () {
|
||||
return !this.is_internal && !this.is_hidden;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
},
|
||||
getContentObject: function () {
|
||||
return DS.get(this.content_object.collection, this.content_object.id);
|
||||
},
|
||||
getContentObjectDetailState: function () {
|
||||
return CamelCase(this.content_object.collection).replace('/', '.') +
|
||||
'.detail({id: ' + this.content_object.id + '})';
|
||||
},
|
||||
getContentObjectForm: function () {
|
||||
return EditForm.fromCollectionString(this.content_object.collection);
|
||||
},
|
||||
getContentResource: function () {
|
||||
return DS.definitions[this.content_object.collection];
|
||||
},
|
||||
getTitle: function () {
|
||||
var title;
|
||||
try {
|
||||
title = this.getContentObject().getAgendaTitle();
|
||||
} catch (e) {
|
||||
// when the content object is not in the DS store.
|
||||
title = this.title;
|
||||
}
|
||||
if (this.item_number) {
|
||||
title = this.item_number + ' · ' + title;
|
||||
}
|
||||
return title;
|
||||
},
|
||||
getListOfSpeakersTitle: function () {
|
||||
var title;
|
||||
try {
|
||||
title = this.getContentObject().getListOfSpeakersTitle();
|
||||
if (this.item_number) {
|
||||
title = this.item_number + ' · ' + title;
|
||||
}
|
||||
} catch (e) {
|
||||
title = this.getTitle();
|
||||
}
|
||||
return title;
|
||||
},
|
||||
getAgendaTitle: function () {
|
||||
return this.title;
|
||||
},
|
||||
getCSVExportText: function () {
|
||||
var text;
|
||||
try {
|
||||
text = this.getContentObject().getCSVExportText();
|
||||
} catch (e) {
|
||||
// when the content object is not in the DS store
|
||||
// or 'getCSVExportText' is not defined return nothing.
|
||||
}
|
||||
return text;
|
||||
},
|
||||
// link name which is shown in search result
|
||||
getSearchResultName: function () {
|
||||
return this.getAgendaTitle();
|
||||
},
|
||||
// return true if a specific relation matches for given searchquery
|
||||
// (here: speakers)
|
||||
hasSearchResult: function (results) {
|
||||
var item = this;
|
||||
// search for speakers (check if any user.id from already found users matches)
|
||||
return _.some(results, function(result) {
|
||||
if (result.getResourceName() === "users/user") {
|
||||
if (_.some(item.speakers, {'user_id': result.id})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getProjectorTitle: function () {
|
||||
try {
|
||||
return this.getContentObject().getAgendaListViewTitle();
|
||||
} catch (e) {
|
||||
// when the content object is not in the DS store
|
||||
return this.list_view_title;
|
||||
}
|
||||
},
|
||||
getListViewTitle: function () {
|
||||
var title = this.getProjectorTitle();
|
||||
if (this.item_number) {
|
||||
title = this.item_number + ' · ' + title;
|
||||
}
|
||||
return title;
|
||||
},
|
||||
getItemNumberWithAncestors: function (agendaId) {
|
||||
if (!agendaId) {
|
||||
agendaId = this.id;
|
||||
}
|
||||
var agendaItem = DS.get(name, agendaId);
|
||||
if (!agendaItem) {
|
||||
return '';
|
||||
} else if (agendaItem.item_number) {
|
||||
return agendaItem.item_number;
|
||||
} else if (agendaItem.parent_id) {
|
||||
return this.getItemNumberWithAncestors(agendaItem.parent_id);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
// override project function of jsDataModel factory
|
||||
project: function (projectorId, tree) {
|
||||
if (tree) {
|
||||
var isProjectedIds = this.isProjected(tree);
|
||||
var requestData = {
|
||||
clear_ids: isProjectedIds,
|
||||
};
|
||||
// Activate, if the projector_id is a new projector.
|
||||
if (_.indexOf(isProjectedIds, projectorId) == -1) {
|
||||
requestData.prune = {
|
||||
id: projectorId,
|
||||
element: {
|
||||
name: 'agenda/item-list',
|
||||
tree: true,
|
||||
id: this.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
return ProjectHelper.project(requestData);
|
||||
} else { // Project the content object
|
||||
var contentObject = DS.get(this.content_object.collection, this.content_object.id);
|
||||
return contentObject.project(projectorId);
|
||||
}
|
||||
},
|
||||
// override isProjected function of jsDataModel factory
|
||||
isProjected: function (tree) {
|
||||
// Returns all ids of all projectors with an agenda-item element. Otherwise an empty list.
|
||||
if (typeof tree === 'undefined') {
|
||||
tree = false;
|
||||
}
|
||||
var self = this;
|
||||
var predicate = function (element) {
|
||||
var value;
|
||||
if (tree) {
|
||||
// Item tree slide for sub tree
|
||||
value = element.name == 'agenda/item-list' &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.id;
|
||||
} else {
|
||||
// Releated item detail slide
|
||||
value = element.name == self.content_object.collection &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.content_object.id;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
var isProjectedIds = [];
|
||||
Projector.getAll().forEach(function (projector) {
|
||||
if (typeof _.findKey(projector.elements, predicate) === 'string') {
|
||||
isProjectedIds.push(projector.id);
|
||||
}
|
||||
});
|
||||
return isProjectedIds;
|
||||
},
|
||||
isRelatedProjected: function () {
|
||||
// related objects for agenda items: list of speakers slide.
|
||||
return this.isListOfSpeakersProjected();
|
||||
},
|
||||
// project list of speakers
|
||||
projectListOfSpeakers: function(projectorId) {
|
||||
var isProjectedIds = this.isListOfSpeakersProjected();
|
||||
var requestData = {
|
||||
clear_ids: isProjectedIds,
|
||||
};
|
||||
if (_.indexOf(isProjectedIds, projectorId) == -1) {
|
||||
requestData.prune = {
|
||||
id: projectorId,
|
||||
element: {
|
||||
name: 'agenda/list-of-speakers',
|
||||
id: this.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
return ProjectHelper.project(requestData);
|
||||
},
|
||||
// check if list of speakers is projected
|
||||
isListOfSpeakersProjected: function () {
|
||||
// Returns all ids of all projectors with an element with the
|
||||
// name 'agenda/list-of-speakers' and the same id. Else returns an empty list.
|
||||
var self = this;
|
||||
var predicate = function (element) {
|
||||
return element.name == 'agenda/list-of-speakers' &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.id;
|
||||
};
|
||||
var isProjecteds = [];
|
||||
Projector.getAll().forEach(function (projector) {
|
||||
if (typeof _.findKey(projector.elements, predicate) === 'string') {
|
||||
isProjecteds.push(projector.id);
|
||||
}
|
||||
});
|
||||
return isProjecteds;
|
||||
},
|
||||
hasSubitems: function(items) {
|
||||
var self = this;
|
||||
var hasChild = false;
|
||||
// Returns true if the item has at least one child item.
|
||||
_.each(items, function (item) {
|
||||
if (item.parent_id == self.id) {
|
||||
hasChild = true;
|
||||
}
|
||||
});
|
||||
return hasChild;
|
||||
}
|
||||
},
|
||||
relations: {
|
||||
hasMany: {
|
||||
'core/tag': {
|
||||
localField: 'tags',
|
||||
localKeys: 'tags_id',
|
||||
},
|
||||
'agenda/speaker': {
|
||||
localField: 'speakers',
|
||||
foreignKey: 'item_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeInject: function (resource, instance) {
|
||||
Speaker.ejectAll({where: {item_id: {'==': instance.id}}});
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('AgendaTree', [
|
||||
function () {
|
||||
return {
|
||||
getTree: function (items) {
|
||||
// Sort items after there weight
|
||||
items.sort(function(itemA, itemB) {
|
||||
return itemA.weight - itemB.weight;
|
||||
});
|
||||
|
||||
// Build a dict with all children (dict-value) to a specific
|
||||
// item id (dict-key).
|
||||
var itemChildren = {};
|
||||
|
||||
_.each(items, function (item) {
|
||||
if (item.parent_id) {
|
||||
// Add item to his parent. If it is the first child, then
|
||||
// create a new list.
|
||||
try {
|
||||
itemChildren[item.parent_id].push(item);
|
||||
} catch (error) {
|
||||
itemChildren[item.parent_id] = [item];
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Recursive function that generates a nested list with all
|
||||
// items with there children
|
||||
function getChildren(items) {
|
||||
var returnItems = [];
|
||||
_.each(items, function (item) {
|
||||
returnItems.push({
|
||||
item: item,
|
||||
children: getChildren(itemChildren[item.id]),
|
||||
id: item.id,
|
||||
});
|
||||
});
|
||||
return returnItems;
|
||||
}
|
||||
|
||||
// Generates the list of root items (with no parents)
|
||||
var parentItems = items.filter(function (item) {
|
||||
return !item.parent_id;
|
||||
});
|
||||
return getChildren(parentItems);
|
||||
},
|
||||
|
||||
// Returns a list of all items as a flat tree
|
||||
getFlatTree: function(items) {
|
||||
var tree = this.getTree(items);
|
||||
var flatItems = [];
|
||||
|
||||
function generateFlatTree(tree, parentCount) {
|
||||
_.each(tree, function (item) {
|
||||
item.item.childrenCount = item.children.length;
|
||||
item.item.parentCount = parentCount;
|
||||
flatItems.push(item.item);
|
||||
generateFlatTree(item.children, parentCount + 1);
|
||||
});
|
||||
}
|
||||
generateFlatTree(tree, 0);
|
||||
return flatItems;
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('CurrentListOfSpeakersItem', [
|
||||
'Projector',
|
||||
'Agenda',
|
||||
function (Projector, Agenda) {
|
||||
return {
|
||||
getItem: function (projectorId) {
|
||||
var projector = Projector.get(projectorId), item;
|
||||
if (projector) {
|
||||
_.forEach(projector.elements, function(element) {
|
||||
if (element.agenda_item_id) {
|
||||
item = Agenda.get(element.agenda_item_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('CurrentListOfSpeakersSlide', [
|
||||
'$http',
|
||||
'Projector',
|
||||
function($http, Projector) {
|
||||
var name = 'agenda/current-list-of-speakers';
|
||||
return {
|
||||
project: function (projectorId, overlay) {
|
||||
var isProjected = this.isProjectedWithOverlayStatus();
|
||||
_.forEach(isProjected, function (mapping) {
|
||||
$http.post('/rest/core/projector/' + mapping.projectorId + '/deactivate_elements/',
|
||||
[mapping.uuid]
|
||||
);
|
||||
});
|
||||
|
||||
// The slide was projected, if the id matches. If the overlay is given, also
|
||||
// the overlay is checked
|
||||
var wasProjectedBefore = _.some(isProjected, function (mapping) {
|
||||
var value = (mapping.projectorId === projectorId);
|
||||
if (overlay !== undefined) {
|
||||
value = value && (mapping.overlay === overlay);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
overlay = overlay || false; // set overlay if it wasn't defined
|
||||
|
||||
if (!wasProjectedBefore) {
|
||||
var activate = function () {
|
||||
return $http.post(
|
||||
'/rest/core/projector/' + projectorId + '/activate_elements/',
|
||||
[{name: name,
|
||||
stable: overlay, // if this is an overlay, it should not be
|
||||
// removed by changing the main content
|
||||
overlay: overlay}]
|
||||
);
|
||||
};
|
||||
if (!overlay) {
|
||||
// clear all elements on this projector, because we are _not_ using the overlay.
|
||||
$http.post('/rest/core/projector/' + projectorId + '/clear_elements/').then(activate);
|
||||
} else {
|
||||
activate();
|
||||
}
|
||||
}
|
||||
},
|
||||
isProjected: function () {
|
||||
// Returns the ids of all projectors with an agenda-item element. Else return an empty list.
|
||||
var predicate = function (element) {
|
||||
return element.name === name;
|
||||
};
|
||||
var isProjectedIds = [];
|
||||
Projector.getAll().forEach(function (projector) {
|
||||
if (typeof _.findKey(projector.elements, predicate) === 'string') {
|
||||
isProjectedIds.push(projector.id);
|
||||
}
|
||||
});
|
||||
return isProjectedIds;
|
||||
},
|
||||
// Returns a list of mappings between pojector id, overlay and uuid.
|
||||
isProjectedWithOverlayStatus: function () {
|
||||
var mapping = [];
|
||||
_.forEach(Projector.getAll(), function (projector) {
|
||||
_.forEach(projector.elements, function (element, uuid) {
|
||||
if (element.name === name) {
|
||||
mapping.push({
|
||||
projectorId: projector.id,
|
||||
uuid: uuid,
|
||||
overlay: element.overlay || false,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return mapping;
|
||||
},
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
|
||||
// Make sure that the Agenda resource is loaded.
|
||||
.run(['Agenda', function(Agenda) {}]);
|
||||
|
||||
}());
|
@ -1,41 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda.csv', [])
|
||||
|
||||
.factory('AgendaCsvExport', [
|
||||
'HumanTimeConverter',
|
||||
'gettextCatalog',
|
||||
'CsvDownload',
|
||||
function (HumanTimeConverter, gettextCatalog, CsvDownload) {
|
||||
var makeHeaderline = function () {
|
||||
var headerline = ['Title', 'Text', 'Duration', 'Comment', 'Internal item'];
|
||||
return _.map(headerline, function (entry) {
|
||||
return gettextCatalog.getString(entry);
|
||||
});
|
||||
};
|
||||
return {
|
||||
export: function (agenda) {
|
||||
var csvRows = [
|
||||
makeHeaderline()
|
||||
];
|
||||
_.forEach(agenda, function (item) {
|
||||
var row = [];
|
||||
var duration = item.duration ? HumanTimeConverter.secondsToHumanTime(item.duration*60,
|
||||
{ seconds: 'disabled',
|
||||
hours: 'enabled' }) : '';
|
||||
row.push('"' + (item.title || '') + '"');
|
||||
row.push('"' + (item.getCSVExportText() || '') + '"');
|
||||
row.push('"' + duration + '"');
|
||||
row.push('"' + (item.comment || '') + '"');
|
||||
row.push('"' + (item.is_hidden ? '1' : '') + '"');
|
||||
csvRows.push(row);
|
||||
});
|
||||
CsvDownload(csvRows, gettextCatalog.getString('Agenda') + '.csv');
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,84 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda.docx', ['OpenSlidesApp.core.docx'])
|
||||
|
||||
.factory('AgendaDocxExport', [
|
||||
'$http',
|
||||
'gettextCatalog',
|
||||
'FileSaver',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
'Config',
|
||||
function ($http, gettextCatalog, FileSaver, Agenda, AgendaTree, Config) {
|
||||
|
||||
var getData = function (items) {
|
||||
// Item structure: The top layer has subitems, that are flat.
|
||||
// The first layer is bold and all sublayers not. The docx
|
||||
// templater cannot render items recursively, so the second
|
||||
// layer are all subitems flated out. Spacing is done with tabs.
|
||||
var tree = AgendaTree.getTree(items);
|
||||
var subitems = []; // This will be used as a temporary variable.
|
||||
var flatSubitems = function (children, parentCount) {
|
||||
_.forEach(children, function (child) {
|
||||
var taps = _.repeat('\t', parentCount - 1);
|
||||
subitems.push({
|
||||
item_number: taps + child.item.item_number,
|
||||
item_title: child.item.list_view_title,
|
||||
});
|
||||
flatSubitems(child.children, parentCount + 1);
|
||||
});
|
||||
};
|
||||
var twoLayerTree = _.map(tree, function (mainItem) {
|
||||
subitems = [];
|
||||
flatSubitems(mainItem.children, 1);
|
||||
return {
|
||||
item_number: mainItem.item.item_number,
|
||||
item_title: mainItem.item.list_view_title,
|
||||
subitems: subitems,
|
||||
};
|
||||
});
|
||||
|
||||
// header
|
||||
var headerline1 = [
|
||||
Config.translate(Config.get('general_event_name').value),
|
||||
Config.translate(Config.get('general_event_description').value)
|
||||
].filter(Boolean).join(' – ');
|
||||
var headerline2 = [
|
||||
Config.get('general_event_location').value,
|
||||
Config.get('general_event_date').value
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
// Data structure for the docx templater.
|
||||
return {
|
||||
header: [headerline1, headerline2].join('\n'),
|
||||
agenda_translation: gettextCatalog.getString('Agenda'),
|
||||
top_list: twoLayerTree,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
export: function (items) {
|
||||
// TODO: use filtered items.
|
||||
var filename = gettextCatalog.getString('Agenda') + '.docx';
|
||||
$http.get('/agenda/docxtemplate/').then(function (success) {
|
||||
var content = window.atob(success.data);
|
||||
var doc = new Docxgen(content);
|
||||
|
||||
var data = getData(items);
|
||||
doc.setData(data);
|
||||
doc.render();
|
||||
|
||||
var zip = doc.getZip();
|
||||
//zip = converter.updateZipFile(zip);
|
||||
|
||||
var out = zip.generate({type: 'blob'});
|
||||
FileSaver.saveAs(out, filename);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
})();
|
@ -1,93 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
.factory('AgendaContentProvider', [
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
function(gettextCatalog, PDFLayout) {
|
||||
|
||||
var createInstance = function(items) {
|
||||
|
||||
// page title
|
||||
var title = PDFLayout.createTitle(gettextCatalog.getString("Agenda"));
|
||||
|
||||
// generate the item list with all subitems
|
||||
var createItemList = function() {
|
||||
var agenda_items = [];
|
||||
_.forEach(items, function (item) {
|
||||
if (item.is_public) {
|
||||
var itemIndent = item.parentCount * 15;
|
||||
|
||||
var itemStyle;
|
||||
if (item.parentCount === 0) {
|
||||
itemStyle = 'listParent';
|
||||
} else {
|
||||
itemStyle = 'listChild';
|
||||
}
|
||||
|
||||
var agendaJsonString = {
|
||||
style: itemStyle,
|
||||
columns: [
|
||||
{
|
||||
width: itemIndent,
|
||||
text: ''
|
||||
},
|
||||
{
|
||||
width: 60,
|
||||
text: item.item_number
|
||||
},
|
||||
{
|
||||
text: item.title
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
agenda_items.push(agendaJsonString);
|
||||
}
|
||||
});
|
||||
return agenda_items;
|
||||
};
|
||||
|
||||
var getContent = function() {
|
||||
return [
|
||||
title,
|
||||
createItemList()
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
getContent: getContent
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
.factory('AgendaPdfExport', [
|
||||
'gettextCatalog',
|
||||
'AgendaContentProvider',
|
||||
'PdfMakeDocumentProvider',
|
||||
'PdfCreate',
|
||||
'Messaging',
|
||||
function (gettextCatalog, AgendaContentProvider, PdfMakeDocumentProvider, PdfCreate, Messaging) {
|
||||
return {
|
||||
export: function (items) {
|
||||
var filename = gettextCatalog.getString('Agenda') + '.pdf';
|
||||
var agendaContentProvider = AgendaContentProvider.createInstance(items);
|
||||
PdfMakeDocumentProvider.createInstance(agendaContentProvider).then(function (documentProvider) {
|
||||
PdfCreate.download(documentProvider, filename);
|
||||
}, function (error) {
|
||||
Messaging.addMessage(error.msg, 'error');
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,137 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
|
||||
|
||||
.config([
|
||||
'slidesProvider',
|
||||
function(slidesProvider) {
|
||||
slidesProvider.registerSlide('agenda/list-of-speakers', {
|
||||
template: 'static/templates/agenda/slide-list-of-speakers.html',
|
||||
});
|
||||
slidesProvider.registerSlide('agenda/item-list', {
|
||||
template: 'static/templates/agenda/slide-item-list.html',
|
||||
});
|
||||
slidesProvider.registerSlide('agenda/current-list-of-speakers', {
|
||||
template: 'static/templates/agenda/slide-current-list-of-speakers.html',
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideCurrentListOfSpeakersCtrl', [
|
||||
'$scope',
|
||||
'Agenda',
|
||||
'CurrentListOfSpeakersItem',
|
||||
'Config',
|
||||
'Projector',
|
||||
function ($scope, Agenda, CurrentListOfSpeakersItem, Config, Projector) {
|
||||
$scope.overlay = $scope.element.overlay;
|
||||
// Watch for changes in the current list of speakers reference
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('projector_currentListOfSpeakers_reference');
|
||||
}, function () {
|
||||
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
|
||||
$scope.updateCurrentListOfSpeakers();
|
||||
});
|
||||
// Watch for changes in the referenced projector
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified($scope.currentListOfSpeakersReference);
|
||||
}, function () {
|
||||
$scope.updateCurrentListOfSpeakers();
|
||||
});
|
||||
// Watch for changes in the current item.
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified();
|
||||
}, function () {
|
||||
$scope.updateCurrentListOfSpeakers();
|
||||
});
|
||||
$scope.updateCurrentListOfSpeakers = function () {
|
||||
$scope.agendaItem = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideListOfSpeakersCtrl', [
|
||||
'$scope',
|
||||
'Agenda',
|
||||
'User',
|
||||
function ($scope, Agenda, User) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
Agenda.bindOne(id, $scope, 'item');
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideItemListCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$filter',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
'Config',
|
||||
function ($scope, $http, $filter, Agenda, AgendaTree, Config) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
|
||||
// Bind agenda tree to the scope
|
||||
var items;
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified() +
|
||||
Config.lastModified('agenda_hide_internal_items_on_projector');
|
||||
}, function () {
|
||||
if ($scope.element.id) {
|
||||
// remove hidden items
|
||||
items = _.filter(Agenda.getAll(), function (item) {
|
||||
return !item.is_hidden;
|
||||
});
|
||||
if (Config.get('agenda_hide_internal_items_on_projector').value) {
|
||||
items = _.filter(items, function (item) {
|
||||
return item.is_public;
|
||||
});
|
||||
}
|
||||
var tree = AgendaTree.getTree(items);
|
||||
|
||||
var getRootNode = function (node) {
|
||||
if (node.id == $scope.element.id) {
|
||||
return node;
|
||||
}
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
var result = getRootNode(node.children[i]);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
_.forEach(tree, function (node) {
|
||||
var result = getRootNode(node);
|
||||
if (result) {
|
||||
$scope.rootItem = result.item;
|
||||
$scope.tree = result.children;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} else if ($scope.element.tree) {
|
||||
items = _.filter(Agenda.getAll(), function (item) {
|
||||
return item.is_public;
|
||||
});
|
||||
$scope.tree = AgendaTree.getTree(items);
|
||||
} else {
|
||||
items = Agenda.filter({
|
||||
where: { parent_id: null },
|
||||
orderBy: 'weight'
|
||||
});
|
||||
items = _.filter(items, function (item) {
|
||||
return item.is_public;
|
||||
});
|
||||
$scope.tree = AgendaTree.getTree(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,890 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.agenda.site', [
|
||||
'OpenSlidesApp.agenda',
|
||||
'OpenSlidesApp.core.pdf',
|
||||
'OpenSlidesApp.agenda.pdf',
|
||||
'OpenSlidesApp.agenda.csv',
|
||||
'OpenSlidesApp.agenda.docx',
|
||||
])
|
||||
|
||||
.config([
|
||||
'mainMenuProvider',
|
||||
'gettext',
|
||||
function (mainMenuProvider, gettext) {
|
||||
mainMenuProvider.register({
|
||||
'ui_sref': 'agenda.item.list',
|
||||
'img_class': 'calendar-o',
|
||||
'title': gettext('Agenda'),
|
||||
'weight': 200,
|
||||
'perm': 'agenda.can_see',
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.config([
|
||||
'SearchProvider',
|
||||
'gettext',
|
||||
function (SearchProvider, gettext) {
|
||||
SearchProvider.register({
|
||||
'verboseName': gettext('Agenda'),
|
||||
'collectionName': 'agenda/item',
|
||||
'urlDetailState': 'agenda.item.detail',
|
||||
'weight': 200,
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.config([
|
||||
'$stateProvider',
|
||||
'gettext',
|
||||
function ($stateProvider, gettext) {
|
||||
$stateProvider
|
||||
.state('agenda', {
|
||||
url: '/agenda',
|
||||
abstract: true,
|
||||
template: "<ui-view/>",
|
||||
data: {
|
||||
title: gettext('Agenda'),
|
||||
basePerm: 'agenda.can_see',
|
||||
},
|
||||
})
|
||||
.state('agenda.item', {
|
||||
abstract: true,
|
||||
template: "<ui-view/>",
|
||||
})
|
||||
.state('agenda.item.list', {})
|
||||
.state('agenda.item.detail', {
|
||||
resolve: {
|
||||
itemId: ['$stateParams', function($stateParams) {
|
||||
return $stateParams.id;
|
||||
}],
|
||||
}
|
||||
})
|
||||
.state('agenda.item.sort', {
|
||||
url: '/sort',
|
||||
controller: 'AgendaSortCtrl',
|
||||
})
|
||||
.state('agenda.current-list-of-speakers', {
|
||||
url: '/speakers',
|
||||
controller: 'CurrentListOfSpeakersViewCtrl',
|
||||
data: {
|
||||
title: gettext('Current list of speakers'),
|
||||
},
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Set the sensitivity of moving nodes horizontal for the ui-tree.
|
||||
.config([
|
||||
'treeConfig',
|
||||
function (treeConfig) {
|
||||
treeConfig.dragMoveSensitivity = 20;
|
||||
}
|
||||
])
|
||||
|
||||
.factory('ShowAsAgendaItemField', [
|
||||
'operator',
|
||||
'gettext',
|
||||
'gettextCatalog',
|
||||
function (operator, gettext, gettextCatalog) {
|
||||
return function (managePermission) {
|
||||
return {
|
||||
key: 'agenda_type',
|
||||
type: 'select-single',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Agenda visibility'),
|
||||
options: [
|
||||
{type: 1, displayName: gettext('Public item')},
|
||||
{type: 2, displayName: gettext('Internal item')},
|
||||
{type: 3, displayName: gettext('Hidden item')}
|
||||
],
|
||||
ngOptions: 'type.type as (type.displayName | translate) for type in to.options',
|
||||
},
|
||||
hide: !(operator.hasPerms(managePermission) && operator.hasPerms('agenda.can_manage'))
|
||||
};
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('ItemListCtrl', [
|
||||
'$scope',
|
||||
'$filter',
|
||||
'$http',
|
||||
'$state',
|
||||
'DS',
|
||||
'operator',
|
||||
'ngDialog',
|
||||
'Agenda',
|
||||
'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
|
||||
'AgendaTree',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
'gettextCatalog',
|
||||
'gettext',
|
||||
'osTableFilter',
|
||||
'osTablePagination',
|
||||
'AgendaCsvExport',
|
||||
'AgendaPdfExport',
|
||||
'AgendaDocxExport',
|
||||
'ErrorMessage',
|
||||
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm,
|
||||
AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter,
|
||||
osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) {
|
||||
|
||||
$scope.AGENDA_ITEM = 1;
|
||||
$scope.INTERNAL_ITEM = 2;
|
||||
$scope.HIDDEN_ITEM = 3;
|
||||
|
||||
// Bind agenda tree to the scope
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified();
|
||||
}, function () {
|
||||
// Filter out items that doesn't have the list_item_title. This happens, if the
|
||||
// item is a hidden item but provides the list of speakers, but should not be
|
||||
// visible in the list view.
|
||||
var allowedItems = _.filter(Agenda.getAll(), function (item) {
|
||||
return item.list_view_title;
|
||||
});
|
||||
$scope.items = AgendaTree.getFlatTree(allowedItems);
|
||||
$scope.agendaHasSubitems = $filter('filter')($scope.items, {'parent_id': ''}).length;
|
||||
});
|
||||
Projector.bindAll({}, $scope, 'projectors');
|
||||
$scope.mainListTree = true;
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
var projectiondefault = ProjectionDefault.filter({name: 'agenda_all_items'})[0];
|
||||
if (projectiondefault) {
|
||||
$scope.defaultProjectorId_all_items = projectiondefault.projector_id;
|
||||
}
|
||||
$scope.projectionDefaults = ProjectionDefault.getAll();
|
||||
});
|
||||
$scope.alert = {};
|
||||
|
||||
|
||||
// Filtering
|
||||
$scope.filter = osTableFilter.createInstance('AgendaTableFilter');
|
||||
|
||||
if (!$scope.filter.existsStorageEntry()) {
|
||||
$scope.filter.booleanFilters = {
|
||||
closed: {
|
||||
value: undefined,
|
||||
defaultValue: undefined,
|
||||
displayName: gettext('Closed items'),
|
||||
choiceYes: gettext('Closed items'),
|
||||
choiceNo: gettext('Open items'),
|
||||
},
|
||||
// The next filters are just on-off, so no undefined there
|
||||
is_public: {
|
||||
value: true,
|
||||
defaultValue: true,
|
||||
choiceYes: gettext('Public items'),
|
||||
choiceNo: gettext('No public items'),
|
||||
},
|
||||
is_internal: {
|
||||
value: true,
|
||||
defaultValue: true,
|
||||
choiceYes: gettext('Internal items'),
|
||||
choiceNo: gettext('No internal items'),
|
||||
permission: 'agenda.can_see_internal_items',
|
||||
},
|
||||
is_hidden: {
|
||||
value: false,
|
||||
defaultValue: false,
|
||||
choiceYes: gettext('Hidden items'),
|
||||
choiceNo: gettext('No hidden items'),
|
||||
permission: 'agenda.can_manage',
|
||||
},
|
||||
};
|
||||
}
|
||||
$scope.filter.propertyList = ['item_number', 'title', 'title_list_view', 'comment', 'duration'];
|
||||
$scope.filter.propertyFunctionList = [
|
||||
function (item) {return item.getListViewTitle();},
|
||||
];
|
||||
$scope.areFiltersSet = function () {
|
||||
return ($scope.areVisibilityFiltersSet() ||
|
||||
$scope.filter.booleanFilters.closed.value !== $scope.filter.booleanFilters.closed.defaultValue);
|
||||
};
|
||||
$scope.areVisibilityFiltersSet = function () {
|
||||
return ($scope.filter.booleanFilters.is_public.value !== $scope.filter.booleanFilters.is_public.defaultValue ||
|
||||
$scope.filter.booleanFilters.is_internal.value !== $scope.filter.booleanFilters.is_internal.defaultValue ||
|
||||
$scope.filter.booleanFilters.is_hidden.value !== $scope.filter.booleanFilters.is_hidden.defaultValue);
|
||||
|
||||
};
|
||||
$scope.resetFilters = function (isSelectMode) {
|
||||
if (!isSelectMode) {
|
||||
_.forEach($scope.filter.booleanFilters, function (filter) {
|
||||
filter.value = filter.defaultValue;
|
||||
});
|
||||
$scope.filter.save();
|
||||
}
|
||||
};
|
||||
|
||||
// Expand all items during searching.
|
||||
$scope.filter.changed = function () {
|
||||
$scope.collapseState = true;
|
||||
$scope.toggleCollapseState();
|
||||
};
|
||||
|
||||
// pagination
|
||||
$scope.pagination = osTablePagination.createInstance('AgendaTablePagination', 50);
|
||||
|
||||
// parse duration for inline editing
|
||||
$scope.generateDurationText = function (item) {
|
||||
//convert data from model format (m) to view format (hh:mm)
|
||||
if (item.duration) {
|
||||
var time = "",
|
||||
totalminutes = item.duration;
|
||||
if (totalminutes < 0) {
|
||||
time = "-";
|
||||
totalminutes = -totalminutes;
|
||||
}
|
||||
var hh = Math.floor(totalminutes / 60);
|
||||
var mm = Math.floor(totalminutes % 60);
|
||||
// Add leading "0" for double digit values
|
||||
mm = ("0"+mm).slice(-2);
|
||||
time += hh + ":" + mm;
|
||||
item.durationText = time;
|
||||
} else {
|
||||
item.durationText = "";
|
||||
}
|
||||
};
|
||||
$scope.setDurationText = function (item) {
|
||||
//convert data from view format (hh:mm) to model format (m)
|
||||
var time = item.durationText.replace('h', '').split(':');
|
||||
var data;
|
||||
if (time.length > 1 && !isNaN(time[0]) && !isNaN(time[1])) {
|
||||
data = (+time[0]) * 60 + (+time[1]);
|
||||
if (data < 0) {
|
||||
data = "-"+data;
|
||||
}
|
||||
item.duration = parseInt(data);
|
||||
} else if (time.length == 1 && !isNaN(time[0])) {
|
||||
data = (+time[0]);
|
||||
item.duration = parseInt(data);
|
||||
} else {
|
||||
item.duration = 0;
|
||||
}
|
||||
$scope.save(item);
|
||||
};
|
||||
|
||||
/** Duration calculations **/
|
||||
$scope.sumDurations = function () {
|
||||
var totalDuration = 0;
|
||||
$scope.items.forEach(function (item) {
|
||||
if (item.duration) {
|
||||
totalDuration += item.duration;
|
||||
}
|
||||
});
|
||||
return totalDuration;
|
||||
};
|
||||
$scope.calculateEndTime = function () {
|
||||
var totalDuration = $scope.sumDurations();
|
||||
var startTimestamp = $scope.config('agenda_start_event_date_time');
|
||||
if (startTimestamp) {
|
||||
var endTimestamp = startTimestamp + totalDuration * 60 * 1000;
|
||||
var endDate = new Date(endTimestamp);
|
||||
var mm = ("0" + endDate.getMinutes()).slice(-2);
|
||||
var dateStr = endDate.getHours() + ':' + mm;
|
||||
return dateStr;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Agenda collapse function
|
||||
$scope.toggleCollapseState = function () {
|
||||
$scope.collapseState = !$scope.collapseState;
|
||||
_.forEach($scope.items, function (item) {
|
||||
item.hideChildren = $scope.collapseState;
|
||||
});
|
||||
};
|
||||
|
||||
// Check, if an item has childs in all filtered items
|
||||
$scope.hasChildren = function (item) {
|
||||
return _.some($scope.itemsFiltered, function (_item) {
|
||||
return _item.parent_id == item.id;
|
||||
});
|
||||
};
|
||||
|
||||
// returns true, if the agenda has at least two layers
|
||||
$scope.agendaHasMultipleLayers = function () {
|
||||
return _.some($scope.items, function (item) {
|
||||
return item.parent_id;
|
||||
});
|
||||
};
|
||||
|
||||
/** Agenda item functions **/
|
||||
// open dialog for new topics // TODO Remove this. Don't forget import button in template.
|
||||
$scope.newDialog = function () {
|
||||
ngDialog.open(TopicForm.getDialog());
|
||||
};
|
||||
// save changed item
|
||||
$scope.save = function (item) {
|
||||
Agenda.save(item).then(
|
||||
function (success) {
|
||||
$scope.alert.show = false;
|
||||
},
|
||||
function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
// delete related item
|
||||
$scope.deleteRelatedItem = function (item) {
|
||||
DS.destroy(item.content_object.collection, item.content_object.id);
|
||||
};
|
||||
// auto numbering of agenda items
|
||||
$scope.autoNumbering = function() {
|
||||
$http.post('/rest/agenda/item/numbering/', {});
|
||||
};
|
||||
// check open permission
|
||||
// TODO: Use generic solution here.
|
||||
$scope.isAllowedToSeeOpenLink = function (item) {
|
||||
var collection = item.content_object.collection;
|
||||
switch (collection) {
|
||||
case 'topics/topic':
|
||||
return operator.hasPerms('agenda.can_see');
|
||||
case 'motions/motion':
|
||||
return operator.hasPerms('motions.can_see');
|
||||
case 'motions/motion-block':
|
||||
return operator.hasPerms('motions.can_see');
|
||||
case 'assignments/assignment':
|
||||
return operator.hasPerms('assignments.can_see');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$scope.edit = function (item) {
|
||||
ngDialog.open(item.getContentObjectForm().getDialog({id: item.content_object.id}));
|
||||
};
|
||||
|
||||
// export
|
||||
$scope.pdfExport = function () {
|
||||
AgendaPdfExport.export($scope.itemsFiltered);
|
||||
};
|
||||
$scope.csvExport = function () {
|
||||
AgendaCsvExport.export($scope.itemsFiltered);
|
||||
};
|
||||
$scope.docxExport = function () {
|
||||
AgendaDocxExport.export($scope.itemsFiltered);
|
||||
};
|
||||
|
||||
/** select mode functions **/
|
||||
$scope.isSelectMode = false;
|
||||
// check all checkboxes
|
||||
$scope.checkAll = function () {
|
||||
$scope.selectedAll = !$scope.selectedAll;
|
||||
angular.forEach($scope.items, function (item) {
|
||||
item.selected = $scope.selectedAll;
|
||||
});
|
||||
};
|
||||
// uncheck all checkboxes if isDeleteMode is closed
|
||||
$scope.uncheckAll = function () {
|
||||
if (!$scope.isSelectMode) {
|
||||
$scope.selectedAll = false;
|
||||
angular.forEach($scope.items, function (item) {
|
||||
item.selected = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
// set type for selected items
|
||||
$scope.setTypeMultiple = function (type) {
|
||||
_.forEach($scope.items, function (item) {
|
||||
if (item.selected) {
|
||||
item.type = type;
|
||||
$scope.save(item);
|
||||
}
|
||||
});
|
||||
$scope.isSelectMode = false;
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
// set closed for selected items
|
||||
$scope.setStateMultiple = function (closed) {
|
||||
_.forEach($scope.items, function (item) {
|
||||
if (item.selected) {
|
||||
item.closed = closed;
|
||||
$scope.save(item);
|
||||
}
|
||||
});
|
||||
$scope.isSelectMode = false;
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
// delete selected items
|
||||
$scope.deleteMultiple = function () {
|
||||
_.forEach($scope.items, function (item) {
|
||||
if (item.selected) {
|
||||
DS.destroy(item.content_object.collection, item.content_object.id);
|
||||
}
|
||||
});
|
||||
$scope.isSelectMode = false;
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
|
||||
/** Project functions **/
|
||||
// get ProjectionDefault for item
|
||||
$scope.getProjectionDefault = function (item) {
|
||||
if (item.tree) {
|
||||
return $scope.defaultProjectorId_all_items;
|
||||
} else {
|
||||
var app_name = item.content_object.collection.split('/')[0];
|
||||
var id = 1;
|
||||
$scope.projectionDefaults.forEach(function (projectionDefault) {
|
||||
if (projectionDefault.name == app_name) {
|
||||
id = projectionDefault.projector_id;
|
||||
}
|
||||
});
|
||||
return id;
|
||||
}
|
||||
};
|
||||
// project agenda
|
||||
$scope.projectAgenda = function (projectorId, tree, id) {
|
||||
var isAgendaProjectedIds = $scope.isAgendaProjected($scope.mainListTree);
|
||||
_.forEach(isAgendaProjectedIds, function (id) {
|
||||
$http.post('/rest/core/projector/' + id + '/clear_elements/');
|
||||
});
|
||||
if (_.indexOf(isAgendaProjectedIds, projectorId) == -1) {
|
||||
$http.post('/rest/core/projector/' + projectorId + '/prune_elements/',
|
||||
[{name: 'agenda/item-list', tree: tree, id: id}]);
|
||||
}
|
||||
};
|
||||
// change whether all items or only main items should be projected
|
||||
$scope.changeMainListTree = function () {
|
||||
var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree);
|
||||
$scope.mainListTree = !$scope.mainListTree;
|
||||
if (isAgendaProjectedId > 0) {
|
||||
$scope.projectAgenda(isAgendaProjectedId, $scope.mainListTree);
|
||||
}
|
||||
};
|
||||
// change whether one item or all subitems should be projected
|
||||
$scope.changeItemTree = function (item) {
|
||||
var tree = item.tree;
|
||||
item.tree = !item.tree;
|
||||
var isProjected = item.isProjected(tree);
|
||||
_.forEach(isProjected, function (projectorId) {
|
||||
// Deactivate and reactivate
|
||||
item.project(projectorId, tree).then(function (s) {
|
||||
item.project(projectorId, !tree);
|
||||
});
|
||||
});
|
||||
};
|
||||
// check if agenda is projected
|
||||
$scope.isAgendaProjected = function (tree) {
|
||||
// Returns the ids of all projectors with an element with
|
||||
// the name 'agenda/item-list'. Else returns an empty list.
|
||||
var predicate = function (element) {
|
||||
var value;
|
||||
if (tree) {
|
||||
// tree with all agenda items
|
||||
value = element.name == 'agenda/item-list' &&
|
||||
typeof element.id === 'undefined' &&
|
||||
element.tree;
|
||||
} else {
|
||||
// only main agenda items
|
||||
value = element.name == 'agenda/item-list' &&
|
||||
typeof element.id === 'undefined' &&
|
||||
!element.tree;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
var projectorIds = [];
|
||||
$scope.projectors.forEach(function (projector) {
|
||||
if (typeof _.findKey(projector.elements, predicate) === 'string') {
|
||||
projectorIds.push(projector.id);
|
||||
}
|
||||
});
|
||||
return projectorIds;
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Filter for the item type that filters the selected items by type. filters
|
||||
// are the boolean filters from the ui.
|
||||
.filter('itemTypeFilter', [
|
||||
function () {
|
||||
return function (items, filters) {
|
||||
return _.filter(items, function (item) {
|
||||
return (item.is_public && filters.is_public.value) ||
|
||||
(item.is_internal && filters.is_internal.value) ||
|
||||
(item.is_hidden && filters.is_hidden.value);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// filter to hide collapsed items. Items has to be a flat tree.
|
||||
.filter('collapsedItemFilter', [
|
||||
function () {
|
||||
return function (items) {
|
||||
return _.filter(items, function (item) {
|
||||
var index = _.findIndex(items, item);
|
||||
var parentId = item.parent_id;
|
||||
// Search for parents, if one has the hideChildren attribute set. All parents
|
||||
// have a higher index as this item, because items is a flat tree.
|
||||
// If a parent has this attribute, we should remove this item. Else if we hit
|
||||
// the top or an item on the first layer, the item is not collapsed.
|
||||
for (--index; index >= 0 && parentId !== null; index--) {
|
||||
var p = items[index];
|
||||
if (p.id === parentId) {
|
||||
if (p.hideChildren) {
|
||||
return false;
|
||||
} else {
|
||||
parentId = p.parent_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('ItemDetailCtrl', [
|
||||
'$scope',
|
||||
'$filter',
|
||||
'Agenda',
|
||||
'itemId',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
'gettextCatalog',
|
||||
'WebpageTitle',
|
||||
'ErrorMessage',
|
||||
function ($scope, $filter, Agenda, itemId, Projector, ProjectionDefault, gettextCatalog, WebpageTitle,
|
||||
ErrorMessage) {
|
||||
$scope.alert = {};
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified(itemId);
|
||||
}, function () {
|
||||
$scope.item = Agenda.get(itemId);
|
||||
WebpageTitle.updateTitle(gettextCatalog.getString('List of speakers') + ' ' +
|
||||
gettextCatalog.getString('of') + ' ' + $scope.item.getTitle());
|
||||
// all speakers
|
||||
$scope.speakers = $filter('orderBy')($scope.item.speakers, 'weight');
|
||||
// next speakers
|
||||
$scope.nextSpeakers = $filter('filter')($scope.speakers, {'begin_time': null});
|
||||
// current speaker
|
||||
$scope.currentSpeaker = $filter('filter')($scope.speakers, {'begin_time': '!!', 'end_time': null});
|
||||
// last speakers
|
||||
$scope.lastSpeakers = $filter('filter')($scope.speakers, {'end_time': '!!'});
|
||||
$scope.lastSpeakers = $filter('orderBy')($scope.lastSpeakers, 'begin_time');
|
||||
});
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
var item_app_name = $scope.item.content_object.collection.split('/')[0];
|
||||
var projectiondefaultItem = ProjectionDefault.filter({name: item_app_name})[0];
|
||||
if (projectiondefaultItem) {
|
||||
$scope.defaultProjectorItemId = projectiondefaultItem.projector_id;
|
||||
}
|
||||
var projectiondefaultListOfSpeakers = ProjectionDefault.filter({name: 'agenda_list_of_speakers'})[0];
|
||||
if (projectiondefaultListOfSpeakers) {
|
||||
$scope.defaultProjectorListOfSpeakersId = projectiondefaultListOfSpeakers.projector_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
/* This is the controller for the list of speakers partial management template.
|
||||
* The parent controller needs to provide a $scope.item, $scope.speakers, $scope.nextSpeakers,
|
||||
* $scope.currentSpeakers, $scope.lastSpeakers. See (as example) ItemDetailCtrl. */
|
||||
.controller('ListOfSpeakersManagementCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$filter',
|
||||
'Agenda',
|
||||
'User',
|
||||
'operator',
|
||||
'ErrorMessage',
|
||||
function ($scope, $http, $filter, Agenda, User, operator, ErrorMessage) {
|
||||
User.bindAll({}, $scope, 'users');
|
||||
$scope.speakerSelectBox = {};
|
||||
|
||||
// close/open list of speakers of current item
|
||||
$scope.closeList = function (listClosed) {
|
||||
$scope.item.speaker_list_closed = listClosed;
|
||||
Agenda.save($scope.item);
|
||||
};
|
||||
|
||||
// add user to list of speakers
|
||||
$scope.addSpeaker = function (userId) {
|
||||
$http.post('/rest/agenda/item/' + $scope.item.id + '/manage_speaker/', {'user': userId}).then(
|
||||
function (success) {
|
||||
$scope.alert.show = false;
|
||||
$scope.speakerSelectBox = {};
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
$scope.speakerSelectBox = {};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// delete speaker(!) from list of speakers
|
||||
$scope.removeSpeaker = function (speakerId) {
|
||||
$http.delete(
|
||||
'/rest/agenda/item/' + $scope.item.id + '/manage_speaker/',
|
||||
{headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({speaker: speakerId})}
|
||||
)
|
||||
.then(function (success) {
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
//delete all speakers from list of speakers
|
||||
$scope.removeAllSpeakers = function () {
|
||||
var speakersOnList = [];
|
||||
angular.forEach($scope.item.speakers, function (speaker) {
|
||||
speakersOnList.push(speaker.id);
|
||||
});
|
||||
$http.delete(
|
||||
'/rest/agenda/item/' + $scope.item.id + '/manage_speaker/',
|
||||
{headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({speaker: speakersOnList})}
|
||||
)
|
||||
.then(function (success) {
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
// Return true if the requested user is allowed to do a specific action
|
||||
// and see the corresponding button (e.g. 'add me' or 'remove me').
|
||||
$scope.isAllowed = function (action) {
|
||||
var nextUsers = [];
|
||||
angular.forEach($scope.nextSpeakers, function (speaker) {
|
||||
nextUsers.push(speaker.user_id);
|
||||
});
|
||||
switch (action) {
|
||||
case 'add':
|
||||
return (operator.hasPerms('agenda.can_be_speaker') &&
|
||||
!$scope.item.speaker_list_closed &&
|
||||
$.inArray(operator.user.id, nextUsers) == -1);
|
||||
case 'remove':
|
||||
if (operator.user) {
|
||||
return ($.inArray(operator.user.id, nextUsers) != -1);
|
||||
}
|
||||
return false;
|
||||
case 'removeAll':
|
||||
return (operator.hasPerms('agenda.can_manage_list_of_speakers') &&
|
||||
$scope.speakers.length > 0);
|
||||
case 'showLastSpeakers':
|
||||
return $scope.lastSpeakers.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
// begin speech of selected/next speaker
|
||||
$scope.beginSpeech = function (speakerId) {
|
||||
$http.put('/rest/agenda/item/' + $scope.item.id + '/speak/', {'speaker': speakerId})
|
||||
.then(function (success) {
|
||||
$scope.alert.show = false;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
// end speech of current speaker
|
||||
$scope.endSpeech = function () {
|
||||
$http.delete(
|
||||
'/rest/agenda/item/' + $scope.item.id + '/speak/',
|
||||
{headers: {'Content-Type': 'application/json'}, data: {}}
|
||||
).then(
|
||||
function (success) {},
|
||||
function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
// gets speech duration of selected speaker in seconds
|
||||
$scope.getDuration = function (speaker) {
|
||||
var beginTimestamp = new Date(speaker.begin_time).getTime();
|
||||
var endTimestamp = new Date(speaker.end_time).getTime();
|
||||
// calculate duration in seconds
|
||||
return Math.floor((endTimestamp - beginTimestamp) / 1000);
|
||||
|
||||
};
|
||||
// save reordered list of speakers
|
||||
$scope.treeOptions = {
|
||||
dropped: function (event) {
|
||||
var sortedSpeakers = _.map($scope.nextSpeakers, function (speaker) {
|
||||
return speaker.id;
|
||||
});
|
||||
$http.post('/rest/agenda/item/' + $scope.item.id + '/sort_speakers/',
|
||||
{speakers: sortedSpeakers}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Marking a speaker
|
||||
$scope.toggleMarked = function (speaker) {
|
||||
$http.patch('/rest/agenda/item/' + $scope.item.id + '/manage_speaker/', {
|
||||
user: speaker.user.id,
|
||||
marked: !speaker.marked,
|
||||
}).then(function (success) {
|
||||
$scope.alert.show = false;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AgendaSortCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
'ErrorMessage',
|
||||
function($scope, $http, Agenda, AgendaTree, ErrorMessage) {
|
||||
// Bind agenda tree to the scope
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified();
|
||||
}, function () {
|
||||
$scope.items = AgendaTree.getTree(Agenda.getAll());
|
||||
});
|
||||
$scope.showInternalItems = true;
|
||||
$scope.alert = {};
|
||||
|
||||
// save parent and weight of moved agenda item (and all items on same level)
|
||||
$scope.treeOptions = {
|
||||
dropped: function(event) {
|
||||
var parentID = null;
|
||||
var droppedItemID = event.source.nodeScope.$modelValue.id;
|
||||
if (event.dest.nodesScope.item) {
|
||||
parentID = event.dest.nodesScope.item.id;
|
||||
}
|
||||
$http.post('/rest/agenda/item/sort/', {
|
||||
nodes: event.dest.nodesScope.$modelValue,
|
||||
parent_id: parentID}
|
||||
).then(
|
||||
function(success) {},
|
||||
function(error){
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('CurrentListOfSpeakersViewCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$filter',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
'Agenda',
|
||||
'Config',
|
||||
'CurrentListOfSpeakersItem',
|
||||
'CurrentListOfSpeakersSlide',
|
||||
'gettextCatalog',
|
||||
'WebpageTitle',
|
||||
function($scope, $http, $filter, Projector, ProjectionDefault, Agenda, Config,
|
||||
CurrentListOfSpeakersItem, CurrentListOfSpeakersSlide, gettextCatalog, WebpageTitle) {
|
||||
$scope.alert = {};
|
||||
$scope.currentListOfSpeakers = CurrentListOfSpeakersSlide;
|
||||
|
||||
// Watch for changes in the current list of speakers reference
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('projector_currentListOfSpeakers_reference');
|
||||
}, function () {
|
||||
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
|
||||
$scope.updateCurrentListOfSpeakersItem();
|
||||
});
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function() {
|
||||
$scope.projectors = Projector.getAll();
|
||||
// If there is just one projector we provide just the overlay.
|
||||
if ($scope.projectors.length === 1) {
|
||||
$scope.currentListOfSpeakersAsOverlay = true;
|
||||
}
|
||||
$scope.updateCurrentListOfSpeakersItem();
|
||||
|
||||
$scope.listOfSpeakersDefaultProjectorId = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0].projector_id;
|
||||
});
|
||||
|
||||
$scope.$watch(function () {
|
||||
return $scope.item ? Agenda.lastModified($scope.item.id) : void 0;
|
||||
}, function () {
|
||||
$scope.updateCurrentListOfSpeakersItem();
|
||||
});
|
||||
|
||||
$scope.updateCurrentListOfSpeakersItem = function () {
|
||||
$scope.item = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
|
||||
if ($scope.item) {
|
||||
// all speakers
|
||||
$scope.speakers = $filter('orderBy')($scope.item.speakers, 'weight');
|
||||
// next speakers
|
||||
$scope.nextSpeakers = $filter('filter')($scope.speakers, {'begin_time': null});
|
||||
// current speaker
|
||||
$scope.currentSpeaker = $filter('filter')($scope.speakers, {'begin_time': '!!', 'end_time': null});
|
||||
// last speakers
|
||||
$scope.lastSpeakers = $filter('filter')($scope.speakers, {'end_time': '!!'});
|
||||
$scope.lastSpeakers = $filter('orderBy')($scope.lastSpeakers, 'begin_time');
|
||||
} else {
|
||||
$scope.speakers = void 0;
|
||||
$scope.nextSpeakers = void 0;
|
||||
$scope.currentSpeaker = void 0;
|
||||
$scope.lastSpeakers = void 0;
|
||||
}
|
||||
if ($scope.item) {
|
||||
WebpageTitle.updateTitle(gettextCatalog.getString('Current list of speakers') + ' ' +
|
||||
gettextCatalog.getString('of') + ' ' + $scope.item.getTitle());
|
||||
} else {
|
||||
WebpageTitle.updateTitle(gettextCatalog.getString('Current list of speakers'));
|
||||
}
|
||||
};
|
||||
|
||||
// Set the current overlay status
|
||||
if ($scope.currentListOfSpeakers.isProjected().length) {
|
||||
var isProjected = $scope.currentListOfSpeakers.isProjectedWithOverlayStatus();
|
||||
$scope.currentListOfSpeakersAsOverlay = isProjected[0].overlay;
|
||||
} else {
|
||||
$scope.currentListOfSpeakersAsOverlay = false;
|
||||
}
|
||||
$scope.setOverlay = function (overlay) {
|
||||
$scope.currentListOfSpeakersAsOverlay = overlay;
|
||||
var isProjected = $scope.currentListOfSpeakers.isProjectedWithOverlayStatus();
|
||||
if (isProjected.length) {
|
||||
_.forEach(isProjected, function (mapping) {
|
||||
if (mapping.overlay != overlay) { // change the overlay if it is different
|
||||
$scope.currentListOfSpeakers.project(mapping.projectorId, overlay);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
//mark all agenda config strings for translation with Javascript
|
||||
.config([
|
||||
'gettext',
|
||||
function (gettext) {
|
||||
gettext('Enable numbering for agenda items');
|
||||
gettext('Numbering prefix for agenda items');
|
||||
gettext('This prefix will be set if you run the automatic agenda numbering.');
|
||||
gettext('Agenda');
|
||||
gettext('Invalid input.');
|
||||
gettext('Numeral system for agenda items');
|
||||
gettext('Arabic');
|
||||
gettext('Roman');
|
||||
gettext('Begin of event');
|
||||
gettext('Input format: DD.MM.YYYY HH:MM');
|
||||
gettext('Hide internal items when projecting subitems');
|
||||
gettext('Number of last speakers to be shown on the projector');
|
||||
gettext('List of speakers');
|
||||
gettext('Show orange countdown in the last x seconds of speaking time');
|
||||
gettext('Enter duration in seconds. Choose 0 to disable warning color.');
|
||||
gettext('Couple countdown with the list of speakers');
|
||||
gettext('[Begin speech] starts the countdown, [End speech] stops the ' +
|
||||
'countdown.');
|
||||
gettext('Agenda visibility');
|
||||
gettext('Default visibility for new agenda items (except topics)');
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,60 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="agenda.item.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Back to overview</translate>
|
||||
</a>
|
||||
<!-- Current list of speakers projector button -->
|
||||
<div class="btn-group button" uib-dropdown
|
||||
uib-tooltip="{{ 'Project the current list of speakers' | translate }}"
|
||||
os-perms="core.can_manage_projector">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
title="{{ 'Project current list of speakers' | translate }}"
|
||||
ng-click="currentListOfSpeakers.project(listOfSpeakersDefaultProjectorId, currentListOfSpeakersAsOverlay)"
|
||||
ng-class="{ 'btn-primary': currentListOfSpeakers.isProjected().length && inArray(currentListOfSpeakers.isProjected(), listOfSpeakersDefaultProjectorId)}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
<translate>Current list of speakers</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
ng-if="projectors.length > 1"
|
||||
uib-dropdown-toggle
|
||||
ng-class="{ 'btn-primary': currentListOfSpeakers.isProjected().length && !inArray(currentListOfSpeakers.isProjected(), listOfSpeakersDefaultProjectorId)}">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button" ng-if="projectors.length > 1">
|
||||
<li role="menuitem">
|
||||
<a href="" ng-click="setOverlay(false); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="currentListOfSpeakersAsOverlay ? 'fa-circle-o' : 'fa-check-circle-o'"></i>
|
||||
<translate>Project as slide</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="" ng-click="setOverlay(true); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="currentListOfSpeakersAsOverlay ? 'fa-check-circle-o' : 'fa-circle-o'"></i>
|
||||
<translate>Project as overlay</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href="" ng-click="currentListOfSpeakers.project(projector.id, currentListOfSpeakersAsOverlay)"
|
||||
ng-class="{ 'projected': inArray(currentListOfSpeakers.isProjected(), projector.id) }">
|
||||
<i class="fa fa-video-camera" ng-show="inArray(currentListOfSpeakers.isProjected(), projector.id)"></i>
|
||||
{{ projector.name | translate }}
|
||||
<span ng-if="projector.id == listOfSpeakersDefaultProjectorId">(<translate>Default</translate>)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 translate>Current list of speakers</h1>
|
||||
<h2> {{ item.getTitle() }}
|
||||
<span class="slimlabel label label-danger ng-scope" style="" ng-if="item.speaker_list_closed" translate>
|
||||
Closed
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-include src="'static/templates/agenda/list-of-speakers-partial-management.html'"></ng-include>
|
@ -1,54 +0,0 @@
|
||||
<div ng-if="item" class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="agenda.item.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Agenda</translate>
|
||||
</a>
|
||||
<a ui-sref="{{ item.getContentObjectDetailState() }}" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
{{ item.getContentResource().verboseName | translate }}
|
||||
</a>
|
||||
<!-- project list of speakers -->
|
||||
<span class="btn-group" style="min-width:54px;" uib-dropdown
|
||||
uib-tooltip="{{ 'Projector' | translate }} {{ item.isListOfSpeakersProjected()[0] || '' }}"
|
||||
tooltip-enable="item.isListofSpeakersProjected().length"
|
||||
os-perms="core.can_manage_projector">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
ng-click="item.projectListOfSpeakers(defaultProjectorListOfSpeakersId)"
|
||||
ng-class="{ 'btn-primary': inArray(item.isListOfSpeakersProjected(), defaultProjectorListOfSpeakersId) }">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
<translate>List of speakers</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm slimDropDown"
|
||||
ng-class="{ 'btn-primary': (item.isListOfSpeakersProjected().length && !inArray(item.isListOfSpeakersProjected(), defaultProjectorListOfSpeakersId) ) }"
|
||||
ng-if="projectors.length > 1"
|
||||
uib-dropdown-toggle>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" ng-if="projectors.length > 1">
|
||||
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href="" ng-click="item.projectListOfSpeakers(projector.id)"
|
||||
ng-class="{ 'projected': inArray(item.isListOfSpeakersProjected(), projector.id) }">
|
||||
<i class="fa fa-video-camera" ng-show="inArray(item.isListOfSpeakersProjected(), projector.id) "></i>
|
||||
{{ projector.name | translate }}
|
||||
<span ng-if="defaultProjectorListOfSpeakersId == projector.id">(<translate>Default</translate>)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- project -->
|
||||
<projector-button model="item" default-projector-id="defaultProjectorItemId"
|
||||
content="{{ item.getContentResource().verboseName | translate }}">
|
||||
</projector-button>
|
||||
</div>
|
||||
<h1>{{ item.getTitle() }}</h1>
|
||||
<h2>
|
||||
<translate>List of speakers</translate>
|
||||
<span ng-if="item.speaker_list_closed" class="slimlabel label label-danger"
|
||||
translate>Closed</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-include src="'static/templates/agenda/list-of-speakers-partial-management.html'"></ng-include>
|
@ -1,551 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<!-- new -->
|
||||
<a ng-click="newDialog()" os-perms="agenda.can_manage"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</a>
|
||||
<!-- import -->
|
||||
<span os-perms="agenda.can_manage">
|
||||
<a ui-sref="topics.topic.import"
|
||||
os-perms="agenda.can_see_internal_items"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-download fa-lg"></i>
|
||||
<translate>Import</translate>
|
||||
</a>
|
||||
</span>
|
||||
<!-- current list of speakers -->
|
||||
<a ui-sref="agenda.current-list-of-speakers" os-perms="users.can_see_name"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
<translate>Current list of speakers</translate>
|
||||
</a>
|
||||
<!-- project agenda button -->
|
||||
<div class="btn-group button" uib-dropdown
|
||||
uib-tooltip="{{ 'Projector' | translate }} {{ isAgendaProjected(mainListTree)[0] || '' }}"
|
||||
tooltip-enable="isAgendaProjected(mainListTree).length"
|
||||
os-perms="core.can_manage_projector">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
title="{{ 'Project agenda' | translate }}"
|
||||
ng-click="projectAgenda(defaultProjectorId_all_items, mainListTree)"
|
||||
ng-class="{ 'btn-primary': isAgendaProjected(mainListTree).length && inArray(isAgendaProjected(mainListTree), defaultProjectorId_all_items)}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
<translate>Agenda</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" uib-dropdown-toggle
|
||||
ng-class="{ 'btn-primary': isAgendaProjected(mainListTree).length && !inArray(isAgendaProjected(mainListTree), defaultProjectorId_all_items)}"
|
||||
ng-if="agendaHasSubitems || projectors.length > 1">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button" ng-if="agendaHasSubitems || projectors.length > 1">
|
||||
<li role="menuitem" ng-show="agendaHasSubitems">
|
||||
<a href="" ng-click="changeMainListTree(); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="mainListTree ? 'fa-square-o' : 'fa-check-square-o'"></i>
|
||||
<translate>Only main agenda items</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider" ng-show="agendaHasSubitems && projectors.length > 1"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'" ng-show="projectors.length > 1">
|
||||
<a href="" ng-click="projectAgenda(projectorId=projector.id, tree=mainListTree)"
|
||||
ng-class="{ 'projected': inArray(isAgendaProjected(mainListTree), projector.id) }">
|
||||
<i class="fa fa-video-camera" ng-show="inArray(isAgendaProjected(mainListTree), projector.id) "></i>
|
||||
{{ projector.name | translate }}
|
||||
<span ng-if="projector.id == defaultProjectorId_all_items">(<translate>Default</translate>)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<h1 translate>Agenda</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<!-- select mode -->
|
||||
<button os-perms="agenda.can_manage" class="btn btn-sm"
|
||||
ng-class="$parent.isSelectMode ? 'btn-primary' : 'btn-default'"
|
||||
ng-click="$parent.isSelectMode = !$parent.isSelectMode; uncheckAll()">
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
<translate>Select ...</translate>
|
||||
</button>
|
||||
<!-- sort button -->
|
||||
<span os-perms="agenda.can_see_internal_items">
|
||||
<a ui-sref="agenda.item.sort" os-perms="agenda.can_manage" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-sitemap fa-lg"></i>
|
||||
<translate>Sort ...</translate>
|
||||
</a>
|
||||
</span>
|
||||
<!-- auto numbering button -->
|
||||
<span ng-if="config('agenda_enable_numbering')">
|
||||
<button os-perms="agenda.can_manage" class="btn btn-default btn-sm"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to number all agenda items?' | translate }}"
|
||||
ng-bootbox-confirm-action="autoNumbering()">
|
||||
<i class="fa fa-sort-numeric-asc"></i>
|
||||
<translate>Numbering</translate>
|
||||
</button>
|
||||
</span>
|
||||
<!-- Export drop down list (for manager) -->
|
||||
<div os-perms="motions.can_manage" class="pull-right" uib-dropdown>
|
||||
<button type="button" class="btn btn-default btn-sm" id="dropdownExport" uib-dropdown-toggle>
|
||||
<i class="fa fa-upload"></i>
|
||||
<span ng-if="itemsFiltered.length == items.length" translate>
|
||||
Export all
|
||||
</span>
|
||||
<span ng-if="itemsFiltered.length != items.length" translate>
|
||||
Export filtered
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
|
||||
<!-- PDF export -->
|
||||
<li>
|
||||
<a href="" ng-click="pdfExport()">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
PDF
|
||||
</a>
|
||||
</li>
|
||||
<!-- CSV export -->
|
||||
<li os-perms="agenda.can_manage">
|
||||
<a href="" id="downloadLinkCSV"
|
||||
ng-click="csvExport()">
|
||||
<i class="fa fa-file-text-o"></i>
|
||||
CSV
|
||||
</a>
|
||||
</li>
|
||||
<!-- DOCX export -->
|
||||
<li>
|
||||
<a href="" ng-click="docxExport()">
|
||||
<i class="fa fa-file-word-o"></i>
|
||||
DOCX
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Export button (for normal users) -->
|
||||
<button type="button" class="btn btn-default btn-sm pull-right"
|
||||
os-perms="!motions.can_manage" ng-click="pdfExport()">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<span ng-if="itemsFiltered.length == items.length" translate>
|
||||
Export all
|
||||
</span>
|
||||
<span ng-if="itemsFiltered.length != items.length" translate>
|
||||
Export filtered
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div uib-collapse="!isSelectMode" class="row spacer">
|
||||
<div class="col-sm-12 text-left form-inline" os-perms="agenda.can_manage">
|
||||
<!-- actions -->
|
||||
<select ng-model="selectedAction" class="form-control input-sm">
|
||||
<option value="" translate>--- Select action ---</option>
|
||||
<option value="delete" translate>Delete</option>
|
||||
<option value="setType" translate>Set visibility</option>
|
||||
<option value="setState" translate>Set state</option>
|
||||
</select>
|
||||
<!-- Type (visibility) -->
|
||||
<select ng-show="selectedAction == 'setType'" ng-model="selectedType" class="form-control input-sm">
|
||||
<option value="" translate>--- Select visibility ---</option>
|
||||
<option value="{{ AGENDA_ITEM }}" translate>
|
||||
Public
|
||||
</option>
|
||||
<option value="{{ INTERNAL_ITEM }}" translate>
|
||||
Internal
|
||||
</option>
|
||||
<option value="{{ HIDDEN_ITEM }}" translate>
|
||||
Hidden
|
||||
</option>
|
||||
</select>
|
||||
<!-- set type button -->
|
||||
<a ng-show="selectedAction == 'setType' && selectedType"
|
||||
ng-click="setTypeMultiple(selectedType)" class="btn btn-default btn-sm">
|
||||
<translate>Set visibility</translate>
|
||||
</a>
|
||||
<!-- set state buttons -->
|
||||
<a ng-show="selectedAction == 'setState'"
|
||||
ng-click="setStateMultiple(true)" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
<translate>Done</translate>
|
||||
</a>
|
||||
<a ng-show="selectedAction == 'setState'"
|
||||
ng-click="setStateMultiple(false)" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-square-o"></i>
|
||||
<translate>Open</translate>
|
||||
</a>
|
||||
<!-- delete button -->
|
||||
<a ng-if="selectedAction === 'delete'"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected agenda items?' | translate }}"
|
||||
ng-bootbox-confirm-action="deleteMultiple()"
|
||||
class="btn btn-default btn-sm btn-danger">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Delete selected items</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer-top-lg italic row">
|
||||
<div class="col-md-6">
|
||||
<span os-perms="agenda.can_see_internal_items">{{ itemsFiltered.length }} /</span>
|
||||
{{ (items|filter:{is_hidden:false}).length }} {{ "items" | translate }}<span ng-if="(items|filter:{selected:true}).length > 0">,
|
||||
{{(items|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
<span os-perms="agenda.can_see_internal_items" class="optional">
|
||||
<span ng-if="sumDurations() > 0">·
|
||||
<translate>Duration</translate>:
|
||||
{{ sumDurations() | osMinutesToTime }}h
|
||||
<span ng-if="config('agenda_start_event_date_time')">
|
||||
(<translate>Estimated end:</translate> {{ calculateEndTime() }})
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="agendaHasMultipleLayers() && items.length === itemsSearched.length">
|
||||
·
|
||||
<a href="" ng-click="toggleCollapseState()">
|
||||
<span ng-if="collapseState" translate>Expand all</span>
|
||||
<span ng-if="!collapseState" translate>Collapse all</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6" ng-show="itemsFiltered.length > pagination.itemsPerPage">
|
||||
<span class="pull-right">
|
||||
<a href="" class="pagination-arrow" ng-click="pagination.prevPage()"
|
||||
ng-if="pagination.showPrevPageArrow()">
|
||||
«
|
||||
</a>
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ pagination.getPageCount(itemsFiltered) }}
|
||||
<a href="" class="pagination-arrow" ng-click="pagination.nextPage(itemsFiltered)"
|
||||
ng-if="pagination.showNextPageArrow(itemsFiltered)">
|
||||
»
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="agenda-table" class="os-table container-fluid">
|
||||
<div class="row header-row">
|
||||
<div class="col-xs-1 centered" ng-show="isSelectMode">
|
||||
<i class="fa text-danger pointer" ng-class="selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
|
||||
ng-click="checkAll()"></i>
|
||||
</div>
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- clear all filters -->
|
||||
<span class="sort-spacer pointer" ng-click="resetFilters(isSelectMode)"
|
||||
ng-if="areFiltersSet()" ng-disabled="isSelectMode"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-window-close"></i>
|
||||
<translate>Filter</translate>
|
||||
</span>
|
||||
<!-- boolean Filters -->
|
||||
<!-- State -->
|
||||
<span uib-dropdown>
|
||||
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
|
||||
ng-class="{'bold': filter.booleanFilters.closed.value !== filter.booleanFilters.closed.defaultValue,
|
||||
'disabled': isSelectMode}"
|
||||
ng-disabled="isSelectMode">
|
||||
<translate>State</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
|
||||
<li>
|
||||
<a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value ? undefined : true); filter.save();">
|
||||
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === true}"></i>
|
||||
{{ filter.booleanFilters.closed.choiceYes | translate }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value === false) ? undefined : false; filter.save();">
|
||||
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === false}"></i>
|
||||
{{ filter.booleanFilters.closed.choiceNo | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- Visibility -->
|
||||
<span uib-dropdown>
|
||||
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
|
||||
ng-class="{'bold': areVisibilityFiltersSet(),
|
||||
'disabled': isSelectMode}"
|
||||
ng-disabled="isSelectMode">
|
||||
<translate>Visibility</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
|
||||
<li>
|
||||
<a href ng-click="filter.booleanFilters.is_public.value = !filter.booleanFilters.is_public.value; filter.save();">
|
||||
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_public.value}"></i>
|
||||
<translate>Public items</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li os-perms="agenda.can_see_internal_items">
|
||||
<a href ng-click="filter.booleanFilters.is_internal.value = !filter.booleanFilters.is_internal.value; filter.save();">
|
||||
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_internal.value}"></i>
|
||||
<translate>Internal items</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li os-perms="agenda.can_see_internal_items agenda.can_manage">
|
||||
<a href ng-click="filter.booleanFilters.is_hidden.value = !filter.booleanFilters.is_hidden.value; filter.save();">
|
||||
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value}"></i>
|
||||
<translate>Hidden items</translate>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- search field -->
|
||||
<span class="form-group">
|
||||
<span class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
<input type="text" ng-model="filter.filterString" class="form-control"
|
||||
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
|
||||
ng-change="filter.save()">
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<!-- show all selected multiselectoptions -->
|
||||
<span>
|
||||
<!-- for all boolean Filters -->
|
||||
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
|
||||
ng-hide="booleanFilter.value === booleanFilter.defaultValue"
|
||||
class="pointer spacer-left-lg"
|
||||
ng-click="booleanFilter.value = booleanFilter.defaultValue; filter.save();"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ booleanFilter.value ? booleanFilter.choiceYes : booleanFilter.choiceNo | translate }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- main table -->
|
||||
<div class="row data-row" ng-mouseover="item.hover=true"
|
||||
ng-mouseleave="item.hover=false"
|
||||
ng-class="{'projected': item.isProjected().length,
|
||||
'related-projected': item.isRelatedProjected().length}"
|
||||
ng-repeat="item in itemsFiltered = (itemsSearched = (items
|
||||
| osFilter : filter.filterString : filter.getObjectQueryString)
|
||||
| filter : {closed: filter.booleanFilters.closed.value}
|
||||
| itemTypeFilter : filter.booleanFilters)
|
||||
| collapsedItemFilter
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="agenda.can_manage" class="col-xs-1 centered">
|
||||
<i class="fa text-danger pointer" ng-click="item.selected=!item.selected"
|
||||
ng-class="item.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
</div>
|
||||
<!-- projector column -->
|
||||
<div class="col-xs-1 centered projector" os-perms="core.can_manage_projector">
|
||||
<div class="btn-group" style="min-width:{{ (item.hasSubitems(items) || projectors.length > 1) ? '54' : '34' }}px;" uib-dropdown
|
||||
uib-tooltip="{{ 'Projector' | translate }} {{ item.isProjected(item.tree)[0] || '' }}"
|
||||
tooltip-enable="item.isProjected(item.tree).length">
|
||||
<button class="btn btn-default btn-sm"
|
||||
ng-click="item.project(getProjectionDefault(item), item.tree)"
|
||||
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && inArray(item.isProjected(item.tree), getProjectionDefault(item))}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm slimDropDown"
|
||||
ng-class="{ 'btn-primary': item.isProjected(item.tree).length && !inArray(item.isProjected(item.tree), getProjectionDefault(item))}"
|
||||
ng-if="item.hasSubitems(items) || projectors.length > 1"
|
||||
uib-dropdown-toggle>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
|
||||
ng-if="item.hasSubitems(items) || projectors.length > 1">
|
||||
<li role="menuitem" ng-show="item.hasSubitems(items)">
|
||||
<a href="" ng-click="changeItemTree(item); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="item.tree ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
<translate>Include all sub items</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider" ng-show="item.hasSubitems(items)"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href="" ng-click="item.project(projector.id, item.tree)"
|
||||
ng-class="{ 'projected': inArray(item.isProjected(item.tree), projector.id) }">
|
||||
<i class="fa fa-video-camera" ng-show="inArray(item.isProjected(item.tree), projector.id)"></i>
|
||||
{{ projector.name | translate }}
|
||||
<span ng-if="projector.id == getProjectionDefault(item)">(<translate>Default</translate>)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-projector-spacer" os-perms="!core.can_manage_projector"></div>
|
||||
|
||||
<!-- main content column -->
|
||||
<div class="col-xs-6 content"
|
||||
style="padding-left: calc({{ items.length === itemsSearched.length ? item.parentCount : 0 }}*25px)">
|
||||
<div class="caret-spacer" ng-if="items.length === itemsSearched.length">
|
||||
<i class="fa pointer"
|
||||
ng-style="{visibility: hasChildren(item) ? 'visible' : 'hidden'}"
|
||||
ng-class="item.hideChildren ? 'fa-caret-right' : 'fa-caret-down'"
|
||||
ng-click="item.hideChildren = !item.hideChildren"></i>
|
||||
</div>
|
||||
<div class="title-column">
|
||||
<!-- ID and title -->
|
||||
<div>
|
||||
<a class="title" ui-sref="{{ item.getContentObjectDetailState() }}" ng-show="isAllowedToSeeOpenLink(item)">
|
||||
{{ item.getListViewTitle() }}
|
||||
</a>
|
||||
<span class="title" ng-hide="isAllowedToSeeOpenLink(item)">
|
||||
{{ item.getListViewTitle() }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- hover menu -->
|
||||
<div os-perms="agenda.can_see" ng-class="{'hiddenDiv': !item.hover}">
|
||||
<small>
|
||||
<a ui-sref="agenda.item.detail({id: item.id})" translate>List of speakers</a>
|
||||
<span os-perms="agenda.can_manage"> ·
|
||||
<a href="" ng-click="edit(item)" translate>Edit</a> ·
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ item.getTitle() }}</b>"
|
||||
ng-bootbox-confirm-action="deleteRelatedItem(item)" translate>Delete</a>
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- additional content column -->
|
||||
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}">
|
||||
<div style="width: 60%;" class="optional">
|
||||
<small>
|
||||
<!-- type dropdown for managers -->
|
||||
<div os-perms="agenda.can_manage">
|
||||
<div ng-mouseover="typeHover=true" ng-mouseleave="typeHover=false">
|
||||
<span uib-dropdown>
|
||||
<span id="dropdownType{{ item.id }}" class="pointer"
|
||||
uib-dropdown-toggle uib-tooltip="{{ 'Change visibility' | translate }}"
|
||||
tooltip-class="nobr">
|
||||
<span ng-if="item.is_public && item.hover">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Public</translate>
|
||||
</span>
|
||||
<span ng-if="item.is_internal">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Internal</translate>
|
||||
</span>
|
||||
<span ng-if="item.is_hidden">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Hidden</translate>
|
||||
</span>
|
||||
<i class="fa fa-cog fa-lg spacer-left" ng-show="typeHover"></i>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownType{{ item.id }}">
|
||||
<li>
|
||||
<a href ng-click="item.type = AGENDA_ITEM; save(item);" translate>
|
||||
Public item
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href ng-click="item.type = INTERNAL_ITEM; save(item);" translate>
|
||||
Internal item
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href ng-click="item.type = HIDDEN_ITEM; save(item);" translate>
|
||||
Hidden item
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- type for non-managers -->
|
||||
<div os-perms="!agenda.can_manage">
|
||||
<span ng-if="item.is_public && item.hover">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Public</translate>
|
||||
</span>
|
||||
<span ng-if="item.is_internal">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Internal</translate>
|
||||
</span>
|
||||
<span ng-if="item.is_hidden">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Hidden</translate>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Duration -->
|
||||
<div ng-style="{'visibility': (item.duration || item.hover) ? 'visible' : 'hidden'}">
|
||||
<div class="popover-wrapper" os-perms="agenda.can_manage">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
<span editable-text="item.durationText" e-placeholder="hh:mm"
|
||||
onshow="generateDurationText(item)" onaftersave="setDurationText(item)">
|
||||
<span ng-if="!item.duration" translate>Set duration ...</span>
|
||||
<span ng-if="item.duration">
|
||||
{{ (item.duration | osMinutesToTime)}}
|
||||
<translate translate-comment="'h' means time in hours">h</translate>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div os-perms="!agenda.can_manage">
|
||||
<div os-perms="agenda.can_see_internal_items">
|
||||
<span ng-if="item.duration">
|
||||
<i class="fa fa-clock-o"></i> {{ item.duration | osMinutesToTime }}
|
||||
<translate translate-comment="'h' means time in hours">h</translate>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Comment -->
|
||||
<div ng-style="{'visibility': (item.comment || item.hover) ? 'visible' : 'hidden'}">
|
||||
<div class="popover-wrapper" os-perms="agenda.can_manage">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span editable-text="item.comment" onaftersave="save(item)">
|
||||
<span ng-if="!item.comment" translate>Set comment ...</span>
|
||||
<span ng-if="item.comment">{{ item.comment }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Number -->
|
||||
<div ng-style="{'visibility': ((item.is_public) && item.hover) ? 'visible' : 'hidden'}" os-perms="agenda.can_manage">
|
||||
<div class="popover-wrapper" os-perms="agenda.can_manage">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span editable-text="item.item_number" onaftersave="save(item)">
|
||||
<span ng-if="!item.item_number" translate>Set item number ...</span>
|
||||
<span ng-if="item.item_number"><em translate>Change item number ...</em></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<template-hook hook-name="agendaListAdditionalContentColumn"></template-hook>
|
||||
</small>
|
||||
</div>
|
||||
<div style="width: 40%;" class="pull-right">
|
||||
<div os-perms="agenda.can_manage">
|
||||
<div class="pointer nobr" ng-click="item.closed = !item.closed; save(item);" ng-show="item.hover || item.closed">
|
||||
<i class="fa" ng-class="item.closed ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
<span class="spacer-left" translate>Done</span>
|
||||
</div>
|
||||
</div>
|
||||
<div os-perms="!agenda.can_manage" >
|
||||
<div ng-show="item.closed">
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
<span class="spacer-left" translate>Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- data row -->
|
||||
|
||||
</div> <!-- container -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="itemsFiltered.length > pagination.itemsPerPage"
|
||||
total-items="itemsFiltered.length"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
first-text="«"
|
||||
last-text="»">
|
||||
</ul>
|
||||
</div> <!-- details -->
|
@ -1,48 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="agenda.item.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Back to overview</translate>
|
||||
</a>
|
||||
</div>
|
||||
<h1 translate>Sort agenda</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details agenda-sort">
|
||||
<p translate>Drag and drop items to change the order of the agenda. Your modification will be saved immediately.</p>
|
||||
|
||||
<p>
|
||||
<button class="btn btn-default btn-sm" ng-click="showInternalItems=!showInternalItems">
|
||||
<translate ng-if="showInternalItems">Hide internal items</translate>
|
||||
<translate ng-if="!showInternalItems">Show internal items</translate>
|
||||
</button>
|
||||
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
|
||||
<div ui-tree="treeOptions" id="tree-root" data-empty-placeholder-enabled="false">
|
||||
<ol ui-tree-nodes ng-model="items">
|
||||
<li ng-repeat="item in items" ui-tree-node ng-include="'nodes_renderer.html'">
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Nested node template -->
|
||||
<script type="text/ng-template" id="nodes_renderer.html">
|
||||
<div ui-tree-handle ng-if="!item.item.is_hidden">
|
||||
{{ item.item.getListViewTitle() }}
|
||||
</div>
|
||||
<div ui-tree-handle ng-if="item.item.is_hidden && showInternalItems" class="internal">
|
||||
<i class="fa fa-ban"></i>
|
||||
{{ item.item.getListViewTitle() }}
|
||||
</div>
|
||||
|
||||
<ol ui-tree-nodes="" ng-model="item.children">
|
||||
<li ng-repeat="item in item.children" ui-tree-node ng-include="'nodes_renderer.html'">
|
||||
</ol>
|
||||
</script>
|
||||
|
@ -1,132 +0,0 @@
|
||||
<div ng-if="item" class="details" ng-controller="ListOfSpeakersManagementCtrl">
|
||||
<div class="speakers-toolbar">
|
||||
<div class="pull-right">
|
||||
<span os-perms="agenda.can_manage">
|
||||
<button ng-if="item.speaker_list_closed" ng-click="closeList(false)"
|
||||
class="btn btn-sm btn-default">
|
||||
<translate>Open list of speakers</translate>
|
||||
</button>
|
||||
<button ng-if="!item.speaker_list_closed" ng-click="closeList(true)"
|
||||
class="btn btn-sm btn-default">
|
||||
<translate>Close list of speakers</translate>
|
||||
</button>
|
||||
</span>
|
||||
<span os-perms="agenda.can_manage_list_of_speakers">
|
||||
<button ng-if="isAllowed('removeAll')" class="btn btn-sm btn-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to remove all speakers from this list?'| translate }}"
|
||||
ng-bootbox-confirm-action="removeAllSpeakers()">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Remove all speakers</translate>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- text for empty list -->
|
||||
<p ng-if="speakers.length == 0" translate>
|
||||
The list of speakers is empty.
|
||||
</p>
|
||||
|
||||
<template-hook hook-name="itemDetailListOfSpeakersButtons"></template-hook>
|
||||
|
||||
<!-- Last speakers -->
|
||||
<div class="spacer-top-lg spacer-bottom">
|
||||
<button ng-if="isAllowed('showLastSpeakers')" ng-click="$parent.showOldSpeakers = !$parent.showOldSpeakers"
|
||||
class="btn btn-xs btn-default">
|
||||
<translate ng-if="!$parent.showOldSpeakers">Last speakers</translate>
|
||||
<translate ng-if="$parent.showOldSpeakers">Hide</translate>
|
||||
</button>
|
||||
<div uib-collapse="!showOldSpeakers">
|
||||
<ol class="indentation">
|
||||
<li ng-repeat="speaker in lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
<small class="grey">
|
||||
{{ getDuration(speaker) | osSecondsToTime }} <translate>minutes</translate>
|
||||
(<translate>Start time</translate>:
|
||||
{{ speaker.begin_time | date:'yyyy-MM-dd HH:mm:ss' }})
|
||||
</small>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)"
|
||||
class="btn btn-default btn-xs" title="{{ 'Remove' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<p ng-repeat="speaker in currentSpeaker" class="currentSpeaker spacer indentation">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<span os-perms="!agenda.can_manage_list_of_speakers">
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</span>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="endSpeech()"
|
||||
class="btn btn-default btn-sm" title="{{ 'End speech' | translate }}">
|
||||
<i class="fa fa-microphone-slash"></i> <translate>Stop</translate>
|
||||
</button>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="toggleMarked(speaker)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Mark speaker' | translate }}">
|
||||
<i class="fa" ng-class="speaker.marked ? 'fa-star' : 'fa-star-o'"></i>
|
||||
</button>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<!-- Next speakers -->
|
||||
<div ng-show="nextSpeakers.length > 0">
|
||||
<div ui-tree="treeOptions" data-empty-placeholder-enabled="false">
|
||||
<ol ui-tree-nodes="" ng-model="nextSpeakers">
|
||||
<li ng-repeat="speaker in nextSpeakers | orderBy: 'weight'" ui-tree-node>
|
||||
<i os-perms="agenda.can_manage_list_of_speakers" ui-tree-handle="" class="fa fa-arrows-v"></i>
|
||||
{{ $index + 1 }}.
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<span os-perms="!agenda.can_manage_list_of_speakers">
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</span>
|
||||
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="beginSpeech(speaker.id)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Begin speech' | translate }}">
|
||||
<i class="fa fa-microphone"></i> <translate>Start</translate>
|
||||
</button>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="toggleMarked(speaker)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Mark speaker' | translate }}">
|
||||
<i class="fa" ng-class="speaker.marked ? 'fa-star' : 'fa-star-o'"></i>
|
||||
</button>
|
||||
<button os-perms="agenda.can_manage_list_of_speakers" ng-click="removeSpeaker(speaker.id)"
|
||||
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select speakers form -->
|
||||
<div class="form-group spacer-top-lg">
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
<div os-perms="agenda.can_manage_list_of_speakers">
|
||||
<select chosen
|
||||
ng-model="speakerSelectBox.selected"
|
||||
ng-change="addSpeaker(speakerSelectBox.selected)"
|
||||
ng-options="user.id as user.get_full_name() for user in users"
|
||||
search-contains="true"
|
||||
placeholder-text-single="'Select or search a participant ...' | translate"
|
||||
no-results-text="'No results available ...' | translate"
|
||||
class="form-control">
|
||||
<select>
|
||||
</div>
|
||||
<p class="spacer">
|
||||
<button ng-if="isAllowed('add')" ng-click="addSpeaker()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>Add me</translate>
|
||||
</button>
|
||||
<button ng-if="isAllowed('remove')" ng-click="removeSpeaker()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-minus"></i>
|
||||
<translate>Remove me</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -1,36 +0,0 @@
|
||||
<div id="speakerbox">
|
||||
<h3 translate>List of speakers</h3>
|
||||
|
||||
<!-- Last speakers -->
|
||||
<p ng-repeat="speaker in lastSpeakers = (agendaItem.speakers
|
||||
| filter: {end_time: '!!', begin_time: '!!'})
|
||||
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
|
||||
class="lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</p>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<div ng-repeat="speaker in agendaItem.speakers | filter: {end_time: null, begin_time: '!!'} "
|
||||
class="currentSpeaker">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
<span class="pull-right" style="width:calc(100% - 30px);">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Next speakers -->
|
||||
<ol class="nextSpeakers">
|
||||
<li ng-repeat="speaker in nextSpeakers = (agendaItem.speakers
|
||||
| filter: {begin_time: null})
|
||||
| orderBy:'weight'
|
||||
| limitTo: 3">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</li>
|
||||
</ol>
|
||||
<p ng-if="nextSpeakers.length > 3" class="lastSpeakers">
|
||||
<i>+ {{ nextSpeakers.length - 3 }}</i>
|
||||
</p>
|
||||
</div>
|
@ -1,43 +0,0 @@
|
||||
<div class="content scrollcontent">
|
||||
|
||||
<!-- Title -->
|
||||
<div id="title">
|
||||
<h1 translate>List of speakers</h1>
|
||||
<h2>{{ agendaItem.getListOfSpeakersTitle() }}
|
||||
<span ng-if="(agendaItem.speakers | filter: {begin_time: null}).length > 0">
|
||||
– {{ (agendaItem.speakers | filter: {begin_time: null}).length }} <translate>speakers</translate>
|
||||
</span>
|
||||
<span ng-if="agendaItem.speaker_list_closed" class="slimlabel label label-danger" translate>Closed</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="zoomcontent">
|
||||
<!-- Last speakers -->
|
||||
<p ng-repeat="speaker in lastSpeakers = (agendaItem.speakers
|
||||
| filter: {end_time: '!!', begin_time: '!!'})
|
||||
| limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))"
|
||||
class="lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</p>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<p ng-repeat="speaker in currentspeakers = (agendaItem.speakers
|
||||
| filter: {end_time: null, begin_time: '!!'})"
|
||||
class="currentSpeaker nobr">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</p>
|
||||
|
||||
<!-- Next speakers -->
|
||||
<ol class="nextSpeakers">
|
||||
<li ng-repeat="speaker in agendaItem.speakers
|
||||
| filter: {begin_time: null}
|
||||
| orderBy:'weight'">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
@ -1,8 +0,0 @@
|
||||
<div ng-controller="SlideCurrentListOfSpeakersCtrl">
|
||||
|
||||
<ng-include src="'static/templates/agenda/partial-slide-current-list-of-speakers.html'"
|
||||
ng-if="!overlay"></ng-include>
|
||||
<ng-include src="'static/templates/agenda/partial-slide-current-list-of-speakers-overlay.html'"
|
||||
ng-if="overlay"></ng-include>
|
||||
|
||||
</div>
|
@ -1,27 +0,0 @@
|
||||
<div ng-controller="SlideItemListCtrl" class="content scrollcontent">
|
||||
<h1 ng-if="!element.id" translate>Agenda</h1>
|
||||
<h1 ng-if="element.id">{{ rootItem.getTitle() }}</h1>
|
||||
|
||||
<div class="agendalist zoomcontent" ng-class="{'spacer-left-lg': element.id}">
|
||||
<table class="agendalist-table">
|
||||
<tr ng-repeat="node in tree" ng-include="'projector_agenda_renderer.html'"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nested node template -->
|
||||
<script type="text/ng-template" id="projector_agenda_renderer.html">
|
||||
<td class="number" ng-if="!node.item.closed">
|
||||
<p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null}">
|
||||
{{ node.item.item_number }}
|
||||
</p>
|
||||
</td>
|
||||
<td ng-if="!node.item.closed">
|
||||
<p ng-class="{mainitem: node.item.parent_id === null, subitem: node.item.parent_id !== null}">
|
||||
{{ node.item.getProjectorTitle() }}
|
||||
</p>
|
||||
<table ng-if="node.children.length" class="agendalist-table">
|
||||
<tr ng-repeat="node in node.children" ng-include="'projector_agenda_renderer.html'"></tr>
|
||||
</table>
|
||||
</td>
|
||||
</script>
|
@ -1,36 +0,0 @@
|
||||
<div ng-controller="SlideListOfSpeakersCtrl" class="content scrollcontent">
|
||||
|
||||
<!-- Title -->
|
||||
<div id="title">
|
||||
<h1 translate>List of speakers</h1>
|
||||
<h2>
|
||||
{{ item.getListOfSpeakersTitle() }}
|
||||
<span ng-if="(item.speakers | filter: {begin_time: null}).length > 0">
|
||||
– {{ (item.speakers | filter: {begin_time: null}).length }} <translate>speakers</translate>
|
||||
</span>
|
||||
<span ng-if="item.speaker_list_closed" class="slimlabel label label-danger" translate>Closed</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="zoomcontent">
|
||||
<!-- Last speakers -->
|
||||
<p ng-repeat="speaker in lastSpeakers = (item.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
|
||||
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
|
||||
<!-- Current speaker -->
|
||||
<p ng-repeat="speaker in item.speakers | filter: {end_time: null, begin_time: '!!'}"
|
||||
class="currentSpeaker">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
|
||||
<!-- Next speakers -->
|
||||
<ol class="nextSpeakers">
|
||||
<li ng-repeat="speaker in item.speakers | filter: {begin_time: null} | orderBy:'weight'">
|
||||
{{ speaker.user.get_full_name() }}
|
||||
<i class="fa fa-star" ng-if="speaker.marked" title="{{ 'Marked' | translate }}"></i>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^docxtemplate/$',
|
||||
views.AgendaDocxTemplateView.as_view(),
|
||||
name='agenda_docx_template'),
|
||||
]
|
@ -15,7 +15,6 @@ from openslides.utils.rest_api import (
|
||||
detail_route,
|
||||
list_route,
|
||||
)
|
||||
from openslides.utils.views import BinaryTemplateView
|
||||
|
||||
from ..utils.auth import has_perm
|
||||
from .access_permissions import ItemAccessPermissions
|
||||
@ -340,11 +339,3 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
|
||||
inform_changed_data(items)
|
||||
return Response({'detail': _('The agenda has been sorted.')})
|
||||
|
||||
|
||||
# Special views
|
||||
class AgendaDocxTemplateView(BinaryTemplateView):
|
||||
"""
|
||||
Returns the template for motions docx export
|
||||
"""
|
||||
template_name = 'templates/docx/agenda.docx'
|
||||
|
@ -1,19 +0,0 @@
|
||||
.electionresults table {
|
||||
width: calc(100% - 230px);
|
||||
}
|
||||
|
||||
#speakerbox {
|
||||
width: 40%;
|
||||
float: right;
|
||||
margin: 20px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
background: #d3d3d3;
|
||||
border-radius: 7px;
|
||||
border: 1px solid #999;
|
||||
padding: 0px 7px 10px 19px;
|
||||
z-index: 99;
|
||||
box-shadow: 3px 3px 10px 1px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#content .col1 .ballot-tabs {
|
||||
ul {
|
||||
margin-left: 0px;
|
||||
}
|
||||
li.active a {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
.tab-content {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-width: 0px 1px 1px 1px;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
@ -1,500 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.assignments', [])
|
||||
|
||||
.factory('AssignmentPollOption', [
|
||||
'DS',
|
||||
'jsDataModel',
|
||||
'gettextCatalog',
|
||||
'Config',
|
||||
'MajorityMethods',
|
||||
function (DS, jsDataModel, gettextCatalog, Config, MajorityMethods) {
|
||||
return DS.defineResource({
|
||||
name: 'assignments/polloption',
|
||||
useClass: jsDataModel,
|
||||
// Change the stringified numbers to floats.
|
||||
beforeInject: function (resource, instance) {
|
||||
_.forEach(instance.votes, function (vote) {
|
||||
vote.weight = parseFloat(vote.weight);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getVotes: function () {
|
||||
if (!this.poll.has_votes) {
|
||||
// Return undefined if this poll has no votes.
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial values for the option
|
||||
var votes = [],
|
||||
config = Config.get('assignments_poll_100_percent_base').value;
|
||||
|
||||
var base = this.poll.getPercentBase(config);
|
||||
if (typeof base === 'object' && base !== null) {
|
||||
// this.poll.pollmethod === 'yna'
|
||||
base = base[this.id];
|
||||
}
|
||||
|
||||
_.forEach(this.votes, function (vote) {
|
||||
// Initial values for the vote
|
||||
var order = '',
|
||||
value = '',
|
||||
percentStr = '',
|
||||
percentNumber;
|
||||
|
||||
// Check for special value
|
||||
switch (vote.weight) {
|
||||
case -1:
|
||||
value = gettextCatalog.getString('majority');
|
||||
break;
|
||||
case -2:
|
||||
value = gettextCatalog.getString('undocumented');
|
||||
break;
|
||||
default:
|
||||
if (vote.weight >= 0) {
|
||||
value = vote.weight;
|
||||
} else {
|
||||
value = 0; // Vote was not defined. Set value to 0.
|
||||
}
|
||||
}
|
||||
switch (vote.value) {
|
||||
case "Yes":
|
||||
order = 1;
|
||||
break;
|
||||
case "No":
|
||||
order = 2;
|
||||
break;
|
||||
case "Abstain":
|
||||
order = 3;
|
||||
break;
|
||||
default:
|
||||
order = 0;
|
||||
}
|
||||
|
||||
// Special case where to skip percents
|
||||
var skipPercents = config === 'YES_NO' && vote.value === 'Abstain';
|
||||
|
||||
if (base && !skipPercents) {
|
||||
percentNumber = Math.round(vote.weight * 100 / base * 100) / 100;
|
||||
if (percentNumber >= 0) {
|
||||
percentStr = '(' + percentNumber + ' %)';
|
||||
}
|
||||
}
|
||||
votes.push({
|
||||
'order': order,
|
||||
'label': gettextCatalog.getString(vote.value),
|
||||
'value': value,
|
||||
'percentStr': percentStr,
|
||||
'percentNumber': percentNumber
|
||||
});
|
||||
});
|
||||
return _.sortBy(votes, 'order');
|
||||
},
|
||||
|
||||
// Returns 0 or positive integer if quorum is reached or surpassed.
|
||||
// Returns negativ integer if quorum is not reached.
|
||||
// Returns undefined if we can not calculate the quorum.
|
||||
isReached: function (method) {
|
||||
if (!this.poll.has_votes) {
|
||||
// Return undefined if this poll has no votes.
|
||||
return;
|
||||
}
|
||||
var isReached;
|
||||
var config = Config.get('assignments_poll_100_percent_base').value;
|
||||
var base = this.poll.getPercentBase(config);
|
||||
if (typeof base === 'object' && base !== null) {
|
||||
// this.poll.pollmethod === 'yna'
|
||||
base = base[this.id];
|
||||
}
|
||||
if (base) {
|
||||
// Provide result only if base is not undefined and not 0.
|
||||
isReached = MajorityMethods[method](this.getVoteYes(), base);
|
||||
}
|
||||
return isReached;
|
||||
},
|
||||
|
||||
// Returns the weight for the vote or the vote 'yes' in case of YNA poll method.
|
||||
getVoteYes: function () {
|
||||
var voteYes = 0;
|
||||
if (this.poll.pollmethod === 'yna') {
|
||||
var voteObj = _.find(this.votes, function (vote) {
|
||||
return vote.value === 'Yes';
|
||||
});
|
||||
if (voteObj) {
|
||||
voteYes = voteObj.weight;
|
||||
}
|
||||
} else {
|
||||
// pollmethod === 'votes'
|
||||
voteYes = this.votes[0].weight;
|
||||
}
|
||||
return voteYes;
|
||||
}
|
||||
},
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'assignments/poll': {
|
||||
localField: 'poll',
|
||||
localKey: 'poll_id',
|
||||
},
|
||||
'users/user': {
|
||||
localField: 'candidate',
|
||||
localKey: 'candidate_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('AssignmentPoll', [
|
||||
'$http',
|
||||
'DS',
|
||||
'jsDataModel',
|
||||
'gettextCatalog',
|
||||
'AssignmentPollOption',
|
||||
'Config',
|
||||
function ($http, DS, jsDataModel, gettextCatalog, AssignmentPollOption, Config) {
|
||||
var name = 'assignments/poll';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
// Change the stringified numbers to floats.
|
||||
beforeInject: function (resource, instance) {
|
||||
var attrs = ['votescast', 'votesinvalid', 'votesvalid', 'votesabstain', 'votesno'];
|
||||
_.forEach(attrs, function (attr) {
|
||||
if (instance[attr] !== null) {
|
||||
instance[attr] = parseFloat(instance[attr]);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
},
|
||||
|
||||
// Returns percent base. Returns undefined if calculation is not possible in general.
|
||||
getPercentBase: function (config, type) {
|
||||
var base;
|
||||
switch (config) {
|
||||
case 'CAST':
|
||||
if (this.votescast <= 0 || this.votesinvalid < 0) {
|
||||
// It would be OK to check only this.votescast < 0 because 0
|
||||
// is checked again later but this is a little bit faster.
|
||||
break;
|
||||
}
|
||||
base = this.votescast;
|
||||
/* falls through */
|
||||
case 'VALID':
|
||||
if (this.votesvalid < 0) {
|
||||
base = void 0;
|
||||
break;
|
||||
}
|
||||
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid') {
|
||||
base = this.votesvalid;
|
||||
}
|
||||
/* falls through */
|
||||
case 'YES_NO_ABSTAIN':
|
||||
case 'YES_NO':
|
||||
if (this.pollmethod === 'yna') {
|
||||
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid' && type !== 'votesvalid') {
|
||||
base = {};
|
||||
_.forEach(this.options, function (option) {
|
||||
var allVotes = option.votes;
|
||||
if (config === 'YES_NO') {
|
||||
allVotes = _.filter(allVotes, function (vote) {
|
||||
// Extract abstain votes in case of YES_NO percent base.
|
||||
// Do not extract abstain vote if it is set to majority so the predicate later
|
||||
// fails and therefor we get an undefined base. Reason: It should not be possible
|
||||
// to set abstain to majority and nevertheless calculate percents of yes and no.
|
||||
return vote.value !== 'Abstain' || vote.weight === -1;
|
||||
});
|
||||
}
|
||||
var predicate = function (vote) {
|
||||
return vote.weight < 0;
|
||||
};
|
||||
if (_.findIndex(allVotes, predicate) === -1) {
|
||||
base[option.id] = _.reduce(allVotes, function (sum, vote) {
|
||||
return sum + vote.weight;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// this.pollmethod === 'votes'
|
||||
var predicate = function (option) {
|
||||
return option.votes[0].weight < 0;
|
||||
};
|
||||
if (_.findIndex(this.options, predicate) !== -1) {
|
||||
base = void 0;
|
||||
} else {
|
||||
if (typeof base === 'undefined' && type !== 'votesabstain' &&
|
||||
type !== 'votesno' && type !== 'votescast' &&
|
||||
type !== 'votesinvalid' && type !== 'votesvalid') {
|
||||
base = _.reduce(this.options, function (sum, option) {
|
||||
return sum + option.votes[0].weight;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return base;
|
||||
},
|
||||
|
||||
// Returns object with value and percent for this poll (for votes valid/invalid/cast only).
|
||||
getVote: function (type) {
|
||||
if (!this.has_votes) {
|
||||
// Return undefined if this poll has no votes.
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial values
|
||||
var value = '',
|
||||
percentStr = '',
|
||||
percentNumber,
|
||||
vote,
|
||||
config = Config.get('assignments_poll_100_percent_base').value;
|
||||
|
||||
switch (type) {
|
||||
case 'votesabstain':
|
||||
vote = this.votesabstain;
|
||||
break;
|
||||
case 'votesno':
|
||||
vote = this.votesno;
|
||||
break;
|
||||
case 'votesinvalid':
|
||||
vote = this.votesinvalid;
|
||||
break;
|
||||
case 'votesvalid':
|
||||
vote = this.votesvalid;
|
||||
break;
|
||||
case 'votescast':
|
||||
vote = this.votescast;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check special values
|
||||
switch (vote) {
|
||||
case -1:
|
||||
value = gettextCatalog.getString('majority');
|
||||
break;
|
||||
case -2:
|
||||
value = gettextCatalog.getString('undocumented');
|
||||
break;
|
||||
default:
|
||||
if (vote >= 0) {
|
||||
value = vote;
|
||||
} else {
|
||||
value = 0; // value was not defined
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate percent value
|
||||
var base = this.getPercentBase(config, type);
|
||||
if (base) {
|
||||
percentNumber = Math.round(vote * 100 / (base) * 10) / 10;
|
||||
percentStr = '(' + percentNumber + ' %)';
|
||||
}
|
||||
return {
|
||||
'value': value,
|
||||
'percentStr': percentStr,
|
||||
'percentNumber': percentNumber,
|
||||
'display': value + ' ' + percentStr
|
||||
};
|
||||
}
|
||||
},
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'assignments/assignment': {
|
||||
localField: 'assignment',
|
||||
localKey: 'assignment_id',
|
||||
}
|
||||
},
|
||||
hasMany: {
|
||||
'assignments/polloption': {
|
||||
localField: 'options',
|
||||
foreignKey: 'poll_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.provider('AssignmentPollDecimalPlaces', [
|
||||
function () {
|
||||
this.$get = ['$q', function ($q) {
|
||||
return {
|
||||
getPlaces: function (poll, find) {
|
||||
if (find) {
|
||||
return $q(function (resolve) {
|
||||
resolve(0);
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
};
|
||||
}];
|
||||
}
|
||||
])
|
||||
|
||||
.factory('AssignmentRelatedUser', [
|
||||
'DS',
|
||||
function (DS) {
|
||||
return DS.defineResource({
|
||||
name: 'assignments/relateduser',
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'users/user': {
|
||||
localField: 'user',
|
||||
localKey: 'user_id',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Assignment', [
|
||||
'$http',
|
||||
'DS',
|
||||
'Projector',
|
||||
'ProjectHelper',
|
||||
'AssignmentRelatedUser',
|
||||
'AssignmentPoll',
|
||||
'jsDataModel',
|
||||
'gettext',
|
||||
function ($http, DS, Projector, ProjectHelper, AssignmentRelatedUser, AssignmentPoll,
|
||||
jsDataModel, gettext) {
|
||||
var name = 'assignments/assignment';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
verboseName: gettext('Election'),
|
||||
verboseNamePlural: gettext('Elections'),
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
},
|
||||
getTitle: function () {
|
||||
return this.title;
|
||||
},
|
||||
getAgendaTitle: function () {
|
||||
return this.getTitle();
|
||||
},
|
||||
// link name which is shown in search result
|
||||
getSearchResultName: function () {
|
||||
return this.getAgendaTitle();
|
||||
},
|
||||
// return true if a specific relation matches for given searchquery
|
||||
// (here: related_users/candidates)
|
||||
hasSearchResult: function (results) {
|
||||
var assignment = this;
|
||||
// search for related users (check if any user.id from already found users matches)
|
||||
return _.some(results, function(result) {
|
||||
if (result.getResourceName() === "users/user") {
|
||||
if (_.some(assignment.assignment_related_users, {'user_id': result.id})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// override project function of jsDataModel factory
|
||||
project: function (projectorId, pollId) {
|
||||
var isProjectedIds = this.isProjected(pollId);
|
||||
var requestData = {
|
||||
clear_ids: isProjectedIds,
|
||||
};
|
||||
if (_.indexOf(isProjectedIds, projectorId) == -1) {
|
||||
requestData.prune = {
|
||||
id: projectorId,
|
||||
element: {
|
||||
name: 'assignments/assignment',
|
||||
id: this.id,
|
||||
poll: pollId
|
||||
},
|
||||
};
|
||||
}
|
||||
return ProjectHelper.project(requestData);
|
||||
},
|
||||
// override isProjected function of jsDataModel factory
|
||||
isProjected: function (poll_id, anyPoll) {
|
||||
// Returns the ids of all projectors with an element
|
||||
// with the name 'assignments/assignment'. Else returns an empty list.
|
||||
// Provide a poll_id to query a specific poll or set anyPoll to true, to
|
||||
// query whether any poll (but not the assignment itself) is projected.
|
||||
var self = this;
|
||||
var predicate = function (element) {
|
||||
var value;
|
||||
if (typeof poll_id === 'undefined') {
|
||||
// Assignment detail slide without poll
|
||||
value = element.name == 'assignments/assignment' &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.id &&
|
||||
typeof element.poll === 'undefined';
|
||||
} else if (anyPoll) {
|
||||
// Assignment detail slide with any poll
|
||||
value = element.name == 'assignments/assignment' &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.id &&
|
||||
typeof element.poll !== 'undefined';
|
||||
} else {
|
||||
// Assignment detail slide with specific poll
|
||||
value = element.name == 'assignments/assignment' &&
|
||||
typeof element.id !== 'undefined' &&
|
||||
element.id == self.id &&
|
||||
typeof element.poll !== 'undefined' &&
|
||||
element.poll == poll_id;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
var isProjectedIds = [];
|
||||
Projector.getAll().forEach(function (projector) {
|
||||
if (typeof _.findKey(projector.elements, predicate) === 'string') {
|
||||
isProjectedIds.push(projector.id);
|
||||
}
|
||||
});
|
||||
return isProjectedIds;
|
||||
},
|
||||
isRelatedProjected: function () {
|
||||
var listOfSpeakers = [];
|
||||
if (this.agenda_item) {
|
||||
listOfSpeakers = this.agenda_item.isListOfSpeakersProjected();
|
||||
}
|
||||
return listOfSpeakers.concat(this.isProjected(null, true));
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'agenda/item': {
|
||||
localKey: 'agenda_item_id',
|
||||
localField: 'agenda_item',
|
||||
}
|
||||
},
|
||||
hasMany: {
|
||||
'core/tag': {
|
||||
localField: 'tags',
|
||||
localKeys: 'tags_id',
|
||||
},
|
||||
'assignments/relateduser': {
|
||||
localField: 'assignment_related_users',
|
||||
foreignKey: 'assignment_id',
|
||||
},
|
||||
'assignments/poll': {
|
||||
localField: 'polls',
|
||||
foreignKey: 'assignment_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeInject: function (resource, instance) {
|
||||
AssignmentRelatedUser.ejectAll({where: {assignment_id: {'==': instance.id}}});
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.run(['Assignment', function(Assignment) {}]);
|
||||
|
||||
}());
|
@ -1,666 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
.factory('AssignmentContentProvider', [
|
||||
'$filter',
|
||||
'HTMLValidizer',
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
'AssignmentPollDecimalPlaces',
|
||||
function($filter, HTMLValidizer, gettextCatalog, PDFLayout, AssignmentPollDecimalPlaces) {
|
||||
|
||||
var createInstance = function(assignment) {
|
||||
|
||||
// page title
|
||||
var title = PDFLayout.createTitle(assignment.title);
|
||||
var isElectedSemaphore = false;
|
||||
|
||||
// open posts
|
||||
var createPreamble = function() {
|
||||
var preambleText = gettextCatalog.getString("Number of persons to be elected") + ": ";
|
||||
var memberNumber = ""+assignment.open_posts;
|
||||
var preamble = {
|
||||
text: [
|
||||
{
|
||||
text: preambleText,
|
||||
bold: true,
|
||||
style: 'textItem'
|
||||
},
|
||||
{
|
||||
text: memberNumber,
|
||||
style: 'textItem'
|
||||
}
|
||||
]
|
||||
};
|
||||
return preamble;
|
||||
};
|
||||
|
||||
// description
|
||||
var createDescription = function() {
|
||||
if (assignment.description) {
|
||||
var html = HTMLValidizer.validize(assignment.description);
|
||||
var descriptionText = gettextCatalog.getString("Description") + ":";
|
||||
var description = [
|
||||
{
|
||||
text: descriptionText,
|
||||
bold: true,
|
||||
style: 'textItem'
|
||||
},
|
||||
{
|
||||
text: $(html).text(),
|
||||
style: 'textItem',
|
||||
margin: [10, 0, 0, 0]
|
||||
}
|
||||
];
|
||||
return description;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// show candidate list (if assignment phase is not 'finished')
|
||||
var createCandidateList = function() {
|
||||
if (assignment.phase != 2) {
|
||||
var candidates = $filter('orderBy')(assignment.assignment_related_users, 'weight');
|
||||
var candidatesText = gettextCatalog.getString("Candidates") + ": ";
|
||||
var userList = [];
|
||||
|
||||
_.forEach(candidates, function(assignmentsRelatedUser) {
|
||||
userList.push({
|
||||
text: assignmentsRelatedUser.user.get_full_name(),
|
||||
margin: [0, 0, 0, 10],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
var cadidateList = {
|
||||
columns: [
|
||||
{
|
||||
text: candidatesText,
|
||||
bold: true,
|
||||
width: "25%",
|
||||
style: 'textItem'
|
||||
},
|
||||
{
|
||||
ul: userList,
|
||||
style: 'textItem'
|
||||
}
|
||||
]
|
||||
};
|
||||
return cadidateList;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// handles the case if a candidate is elected or not
|
||||
var electedCandidateLine = function(candidateName, pollOption, pollTableBody) {
|
||||
if (pollOption.is_elected) {
|
||||
isElectedSemaphore = true;
|
||||
return {
|
||||
text: candidateName + "*",
|
||||
bold: true,
|
||||
style: PDFLayout.flipTableRowStyle(pollTableBody.length)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: candidateName,
|
||||
style: PDFLayout.flipTableRowStyle(pollTableBody.length)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//creates the voting string for the result table and differentiates between special values
|
||||
var parseVoteValue = function(voteObject, printLabel, precision) {
|
||||
var voteVal = '';
|
||||
if (voteObject) {
|
||||
if (printLabel) {
|
||||
voteVal += voteObject.label + ': ';
|
||||
}
|
||||
voteVal += $filter('number')(voteObject.value, precision);
|
||||
|
||||
if (voteObject.percentStr) {
|
||||
voteVal += ' ' + voteObject.percentStr;
|
||||
}
|
||||
}
|
||||
voteVal += '\n';
|
||||
return voteVal;
|
||||
};
|
||||
|
||||
// creates the election result table
|
||||
var createPollResultTable = function() {
|
||||
var resultBody = [];
|
||||
_.forEach(assignment.polls, function(poll, pollIndex) {
|
||||
if (poll.published) {
|
||||
var pollTableBody = [];
|
||||
var precision = AssignmentPollDecimalPlaces.getPlaces(poll);
|
||||
|
||||
resultBody.push({
|
||||
text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1),
|
||||
bold: true,
|
||||
style: 'textItem',
|
||||
margin: [0, 15, 0, 0]
|
||||
});
|
||||
|
||||
pollTableBody.push([
|
||||
{
|
||||
text: gettextCatalog.getString('Candidates'),
|
||||
style: 'tableHeader',
|
||||
},
|
||||
{
|
||||
text: gettextCatalog.getString('Votes'),
|
||||
style: 'tableHeader',
|
||||
}
|
||||
]);
|
||||
|
||||
_.forEach(poll.options, function(pollOption, optionIndex) {
|
||||
var candidateName = pollOption.candidate.get_full_name();
|
||||
var votes = pollOption.getVotes(); // 0 = yes, 1 = no, 2 = abstain
|
||||
var tableLine = [];
|
||||
|
||||
tableLine.push(electedCandidateLine(candidateName, pollOption, pollTableBody));
|
||||
if (poll.pollmethod == 'votes') {
|
||||
tableLine.push(
|
||||
{
|
||||
text: parseVoteValue(votes[0], false, precision),
|
||||
style: PDFLayout.flipTableRowStyle(pollTableBody.length)
|
||||
}
|
||||
);
|
||||
} else {
|
||||
var resultBlock = [];
|
||||
_.forEach(votes, function(vote) {
|
||||
resultBlock.push(parseVoteValue(vote, true, precision));
|
||||
});
|
||||
tableLine.push({
|
||||
text: resultBlock,
|
||||
style: PDFLayout.flipTableRowStyle(pollTableBody.length)
|
||||
}
|
||||
);
|
||||
}
|
||||
pollTableBody.push(tableLine);
|
||||
});
|
||||
|
||||
var pushConcludeRow = function (title, fieldName) {
|
||||
if (poll[fieldName]) {
|
||||
pollTableBody.push([
|
||||
{
|
||||
text: gettextCatalog.getString(title),
|
||||
style: 'tableConclude'
|
||||
},
|
||||
{
|
||||
text: parseVoteValue(poll.getVote(fieldName), false, precision),
|
||||
style: 'tableConclude'
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
pushConcludeRow('Abstain', 'votesabstain');
|
||||
pushConcludeRow('No', 'votesno');
|
||||
pushConcludeRow('Valid ballots', 'votesvalid');
|
||||
pushConcludeRow('Invalid ballots', 'votesinvalid');
|
||||
pushConcludeRow('Casted ballots', 'votescast');
|
||||
|
||||
var resultTableJsonSting = {
|
||||
table: {
|
||||
widths: ['64%','33%'],
|
||||
headerRows: 1,
|
||||
body: pollTableBody,
|
||||
},
|
||||
layout: 'headerLineOnly',
|
||||
};
|
||||
|
||||
resultBody.push(resultTableJsonSting);
|
||||
}
|
||||
});
|
||||
|
||||
// add the legend to the result body
|
||||
if (assignment.polls.length > 0 && isElectedSemaphore) {
|
||||
resultBody.push({
|
||||
text: '* = ' + gettextCatalog.getString('is elected'),
|
||||
margin: [0, 5, 0, 0],
|
||||
});
|
||||
}
|
||||
|
||||
return resultBody;
|
||||
};
|
||||
|
||||
var getContent = function() {
|
||||
return [
|
||||
title,
|
||||
createPreamble(),
|
||||
createDescription(),
|
||||
createCandidateList(),
|
||||
createPollResultTable()
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
getContent: getContent,
|
||||
title: assignment.title
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('BallotContentProvider', [
|
||||
'$q',
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
'Config',
|
||||
'User',
|
||||
'ImageConverter',
|
||||
function($q, $filter, gettextCatalog, PDFLayout, Config, User, ImageConverter) {
|
||||
var createInstance = function(assignment, poll, pollNumber) {
|
||||
|
||||
var logoBallotPaperUrl = Config.get('logo_pdf_ballot_paper').value.path;
|
||||
var imageMap = {};
|
||||
|
||||
// PDF header
|
||||
var header = function() {
|
||||
var columns = [];
|
||||
|
||||
// logo
|
||||
if (logoBallotPaperUrl) {
|
||||
columns.push({
|
||||
image: logoBallotPaperUrl,
|
||||
fit: [90,20],
|
||||
width: '20%'
|
||||
});
|
||||
}
|
||||
var text = Config.get('general_event_name').value;
|
||||
columns.push({
|
||||
text: text,
|
||||
fontSize: 8,
|
||||
alignment: 'right',
|
||||
});
|
||||
|
||||
return {
|
||||
color: '#555',
|
||||
margin: [30, 10, 10, -10], // [left, top, right, bottom]
|
||||
columns: columns,
|
||||
columnGap: 10
|
||||
};
|
||||
};
|
||||
|
||||
// page title
|
||||
var createTitle = function() {
|
||||
return {
|
||||
text: assignment.title,
|
||||
style: 'title',
|
||||
};
|
||||
};
|
||||
|
||||
// poll description
|
||||
var createPollHint = function() {
|
||||
var description = poll.description ? ': ' + poll.description : '';
|
||||
return {
|
||||
text: gettextCatalog.getString("Ballot") + " " + pollNumber + description,
|
||||
style: 'description',
|
||||
};
|
||||
};
|
||||
|
||||
// election entries
|
||||
var createYNBallotEntry = function(decision) {
|
||||
var YNColumn = [
|
||||
{
|
||||
width: "auto",
|
||||
stack: [
|
||||
PDFLayout.createBallotEntry(gettextCatalog.getString("Yes"))
|
||||
]
|
||||
},
|
||||
{
|
||||
width: "auto",
|
||||
stack: [
|
||||
PDFLayout.createBallotEntry(gettextCatalog.getString("No"))
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
if (poll.pollmethod == 'yna') {
|
||||
YNColumn.push({
|
||||
width: "auto",
|
||||
stack: [
|
||||
PDFLayout.createBallotEntry(gettextCatalog.getString("Abstain"))
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
text: decision,
|
||||
margin: [40, 10, 0, 0],
|
||||
},
|
||||
{
|
||||
columns: YNColumn
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
var createSelectionField = function() {
|
||||
var candidates = $filter('orderBy')(poll.options, 'weight');
|
||||
var candidateBallotList = [];
|
||||
|
||||
if (poll.pollmethod == 'votes') {
|
||||
_.forEach(candidates, function(option) {
|
||||
var candidate = option.candidate.get_full_name();
|
||||
candidateBallotList.push(PDFLayout.createBallotEntry(candidate));
|
||||
});
|
||||
// Add 'no' option
|
||||
var no = gettextCatalog.getString('No');
|
||||
var ballotEntry = PDFLayout.createBallotEntry(no);
|
||||
ballotEntry.margin[1] = 25; // top margin
|
||||
candidateBallotList.push(ballotEntry);
|
||||
} else {
|
||||
_.forEach(candidates, function(option) {
|
||||
var candidate;
|
||||
if (option.candidate) {
|
||||
candidate = option.candidate.get_full_name();
|
||||
}
|
||||
candidateBallotList.push(createYNBallotEntry(candidate));
|
||||
});
|
||||
}
|
||||
return candidateBallotList;
|
||||
};
|
||||
|
||||
var createSection = function(marginTop) {
|
||||
|
||||
// since it is not possible to give a column a fixed height, we draw an "empty" column
|
||||
// with a one px width and a fixed top-margin
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
width: 1,
|
||||
margin: [0, marginTop],
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
width: '*',
|
||||
stack: [
|
||||
header(),
|
||||
createTitle(),
|
||||
createPollHint(),
|
||||
createSelectionField(),
|
||||
],
|
||||
},
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
var createTableBody = function(numberOfRows, sheetend, maxballots) {
|
||||
var ballotstoprint = numberOfRows * 2;
|
||||
if (Number.isInteger(maxballots) && maxballots > 0 && maxballots < ballotstoprint) {
|
||||
ballotstoprint = maxballots;
|
||||
}
|
||||
var tableBody = [];
|
||||
while (ballotstoprint > 1){
|
||||
tableBody.push([createSection(sheetend), createSection(sheetend)]);
|
||||
ballotstoprint -= 2;
|
||||
}
|
||||
if (ballotstoprint == 1) {
|
||||
tableBody.push([createSection(sheetend), '']);
|
||||
}
|
||||
return tableBody;
|
||||
};
|
||||
|
||||
var createContentTable = function() {
|
||||
// first, determine how many ballots we need
|
||||
var amount;
|
||||
var amount_method = Config.get('assignments_pdf_ballot_papers_selection').value;
|
||||
switch (amount_method) {
|
||||
case 'NUMBER_OF_ALL_PARTICIPANTS':
|
||||
amount = User.getAll().length;
|
||||
break;
|
||||
case 'NUMBER_OF_DELEGATES':
|
||||
//TODO: assumption that DELEGATES is always group id 2. This may not be true
|
||||
var group_id = 2;
|
||||
amount = User.filter({where: {'groups_id': {contains:group_id} }}).length;
|
||||
break;
|
||||
case 'CUSTOM_NUMBER':
|
||||
amount = Config.get('assignments_pdf_ballot_papers_number').value;
|
||||
break;
|
||||
default:
|
||||
// should not happen.
|
||||
amount = 0;
|
||||
}
|
||||
var tabledContent = [];
|
||||
var rowsperpage;
|
||||
var sheetend;
|
||||
if (poll.pollmethod == 'votes') {
|
||||
if (poll.options.length <= 4) {
|
||||
sheetend = 105;
|
||||
rowsperpage = 4;
|
||||
} else if (poll.options.length <= 8) {
|
||||
sheetend = 140;
|
||||
rowsperpage = 3;
|
||||
} else if (poll.options.length <= 12) {
|
||||
sheetend = 210;
|
||||
rowsperpage = 2;
|
||||
}
|
||||
else { //works untill ~30 people
|
||||
sheetend = 418;
|
||||
rowsperpage = 1;
|
||||
}
|
||||
} else {
|
||||
if (poll.options.length <= 2) {
|
||||
sheetend = 105;
|
||||
rowsperpage = 4;
|
||||
} else if (poll.options.length <= 4) {
|
||||
sheetend = 140;
|
||||
rowsperpage = 3;
|
||||
} else if (poll.options.length <= 6) {
|
||||
sheetend = 210;
|
||||
rowsperpage = 2;
|
||||
} else {
|
||||
sheetend = 418;
|
||||
rowsperpage = 1;
|
||||
}
|
||||
}
|
||||
var page_entries = rowsperpage * 2;
|
||||
var fullpages = Math.floor(amount / page_entries);
|
||||
for (var i=0; i < fullpages; i++) {
|
||||
tabledContent.push({
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ['50%', '50%'],
|
||||
body: createTableBody(rowsperpage, sheetend),
|
||||
pageBreak: 'after'
|
||||
},
|
||||
layout: PDFLayout.getBallotLayoutLines(),
|
||||
rowsperpage: rowsperpage
|
||||
});
|
||||
}
|
||||
// fill the last page only partially
|
||||
var lastpage_ballots = amount - (fullpages * page_entries);
|
||||
if (lastpage_ballots < page_entries && lastpage_ballots > 0){
|
||||
var partialpage = createTableBody(rowsperpage, sheetend, lastpage_ballots);
|
||||
tabledContent.push({
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ['50%', '50%'],
|
||||
body: partialpage
|
||||
},
|
||||
layout: PDFLayout.getBallotLayoutLines(),
|
||||
rowsperpage: rowsperpage
|
||||
});
|
||||
}
|
||||
return tabledContent;
|
||||
};
|
||||
|
||||
var getContent = function() {
|
||||
return createContentTable();
|
||||
};
|
||||
|
||||
var getImageMap = function () {
|
||||
return imageMap;
|
||||
};
|
||||
|
||||
return $q(function (resolve, reject) {
|
||||
var imageSources = [
|
||||
logoBallotPaperUrl,
|
||||
];
|
||||
ImageConverter.toBase64(imageSources).then(function (_imageMap) {
|
||||
imageMap = _imageMap;
|
||||
resolve({
|
||||
getContent: getContent,
|
||||
getImageMap: getImageMap,
|
||||
});
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('AssignmentCatalogContentProvider', [
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
'Config',
|
||||
function(gettextCatalog, PDFLayout, Config) {
|
||||
|
||||
var createInstance = function(allAssignments) {
|
||||
|
||||
var title = PDFLayout.createTitle(
|
||||
Config.translate(Config.get('assignments_pdf_title').value)
|
||||
);
|
||||
|
||||
var createPreamble = function() {
|
||||
var preambleText = Config.get('assignments_pdf_preamble').value;
|
||||
if (preambleText) {
|
||||
return {
|
||||
text: preambleText,
|
||||
style: "preamble"
|
||||
};
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
var createTOContent = function(assignmentTitles) {
|
||||
var heading = {
|
||||
text: gettextCatalog.getString("Table of contents"),
|
||||
style: "heading2",
|
||||
};
|
||||
|
||||
var toc = [];
|
||||
_.forEach(assignmentTitles, function(title) {
|
||||
toc.push({
|
||||
text: title,
|
||||
style: "tableofcontent"
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
heading,
|
||||
toc,
|
||||
PDFLayout.addPageBreak()
|
||||
];
|
||||
};
|
||||
|
||||
var getContent = function() {
|
||||
var content = [];
|
||||
var assignmentContent = [];
|
||||
var assignmentTitles = [];
|
||||
|
||||
_.forEach(allAssignments, function(assignment, key) {
|
||||
assignmentTitles.push(assignment.title);
|
||||
assignmentContent.push(assignment.getContent());
|
||||
if (key < allAssignments.length - 1) {
|
||||
assignmentContent.push(PDFLayout.addPageBreak());
|
||||
}
|
||||
});
|
||||
|
||||
content.push(title);
|
||||
content.push(createPreamble());
|
||||
content.push(createTOContent(assignmentTitles));
|
||||
content.push(assignmentContent);
|
||||
return content;
|
||||
};
|
||||
|
||||
return {
|
||||
getContent: getContent
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('AssignmentPdfExport', [
|
||||
'gettextCatalog',
|
||||
'AssignmentContentProvider',
|
||||
'AssignmentCatalogContentProvider',
|
||||
'PdfMakeDocumentProvider',
|
||||
'BallotContentProvider',
|
||||
'PdfMakeBallotPaperProvider',
|
||||
'PdfCreate',
|
||||
'Messaging',
|
||||
function (gettextCatalog, AssignmentContentProvider, AssignmentCatalogContentProvider,
|
||||
PdfMakeDocumentProvider, BallotContentProvider, PdfMakeBallotPaperProvider, PdfCreate,
|
||||
Messaging) {
|
||||
return {
|
||||
export: function (assignments, singleAssignment) {
|
||||
var filename = singleAssignment ?
|
||||
gettextCatalog.getString('Election') + '_' + assignments.title :
|
||||
gettextCatalog.getString('Elections');
|
||||
filename += '.pdf';
|
||||
if (singleAssignment) {
|
||||
assignments = [assignments];
|
||||
}
|
||||
|
||||
// Convert the assignments to content providers
|
||||
var assignmentContentProviderArray = _.map(assignments, function (assignment) {
|
||||
return AssignmentContentProvider.createInstance(assignment);
|
||||
});
|
||||
|
||||
var documentProviderPromise;
|
||||
if (singleAssignment) {
|
||||
documentProviderPromise =
|
||||
PdfMakeDocumentProvider.createInstance(assignmentContentProviderArray[0]);
|
||||
} else {
|
||||
var assignmentCatalogContentProvider =
|
||||
AssignmentCatalogContentProvider.createInstance(assignmentContentProviderArray);
|
||||
documentProviderPromise =
|
||||
PdfMakeDocumentProvider.createInstance(assignmentCatalogContentProvider);
|
||||
}
|
||||
documentProviderPromise.then(function (documentProvider) {
|
||||
PdfCreate.download(documentProvider, filename);
|
||||
}, function (error) {
|
||||
Messaging.addMessage(error.msg, 'error');
|
||||
});
|
||||
},
|
||||
createBallotPdf: function (assignment, pollId) {
|
||||
var thePoll;
|
||||
var pollNumber;
|
||||
_.forEach(assignment.polls, function(poll, pollIndex) {
|
||||
if (poll.id == pollId) {
|
||||
thePoll = poll;
|
||||
pollNumber = pollIndex+1;
|
||||
}
|
||||
});
|
||||
var filename = gettextCatalog.getString('Ballot') + '_' + pollNumber + '_' + assignment.title + '.pdf';
|
||||
BallotContentProvider.createInstance(assignment, thePoll, pollNumber).then(function (ballotContentProvider) {
|
||||
var documentProvider = PdfMakeBallotPaperProvider.createInstance(ballotContentProvider);
|
||||
PdfCreate.download(documentProvider, filename);
|
||||
}, function (error) {
|
||||
Messaging.addMessage(error.msg, 'error');
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,46 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignments'])
|
||||
|
||||
.config([
|
||||
'slidesProvider',
|
||||
function(slidesProvider) {
|
||||
slidesProvider.registerSlide('assignments/assignment', {
|
||||
template: 'static/templates/assignments/slide_assignment.html',
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideAssignmentCtrl', [
|
||||
'$scope',
|
||||
'Assignment',
|
||||
'AssignmentPoll',
|
||||
'AssignmentPhases',
|
||||
'AssignmentPollDecimalPlaces',
|
||||
'User',
|
||||
function($scope, Assignment, AssignmentPoll, AssignmentPhases, AssignmentPollDecimalPlaces, User) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
$scope.showResult = $scope.element.poll;
|
||||
|
||||
if ($scope.showResult) {
|
||||
var poll = AssignmentPoll.get($scope.showResult);
|
||||
$scope.votesPrecision = 0;
|
||||
if (poll) {
|
||||
AssignmentPollDecimalPlaces.getPlaces(poll, true).then(function (decimalPlaces) {
|
||||
$scope.votesPrecision = decimalPlaces;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Assignment.bindOne(id, $scope, 'assignment');
|
||||
$scope.phases = AssignmentPhases;
|
||||
User.bindAll({}, $scope, 'users');
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,941 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.assignments.site', [
|
||||
'OpenSlidesApp.assignments',
|
||||
'OpenSlidesApp.core.pdf',
|
||||
'OpenSlidesApp.assignments.pdf',
|
||||
'OpenSlidesApp.poll.majority'
|
||||
])
|
||||
|
||||
.config([
|
||||
'mainMenuProvider',
|
||||
'gettext',
|
||||
function (mainMenuProvider, gettext) {
|
||||
mainMenuProvider.register({
|
||||
'ui_sref': 'assignments.assignment.list',
|
||||
'img_class': 'pie-chart',
|
||||
'title': gettext('Elections'),
|
||||
'weight': 400,
|
||||
'perm': 'assignments.can_see'
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.config([
|
||||
'SearchProvider',
|
||||
'gettext',
|
||||
function (SearchProvider, gettext) {
|
||||
SearchProvider.register({
|
||||
'verboseName': gettext('Elections'),
|
||||
'collectionName': 'assignments/assignment',
|
||||
'urlDetailState': 'assignments.assignment.detail',
|
||||
'weight': 400,
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.config([
|
||||
'$stateProvider',
|
||||
'gettext',
|
||||
function($stateProvider, gettext) {
|
||||
$stateProvider
|
||||
.state('assignments', {
|
||||
url: '/assignments',
|
||||
abstract: true,
|
||||
template: "<ui-view/>",
|
||||
data: {
|
||||
title: gettext('Elections'),
|
||||
basePerm: 'assignments.can_see',
|
||||
},
|
||||
})
|
||||
.state('assignments.assignment', {
|
||||
abstract: true,
|
||||
template: "<ui-view/>",
|
||||
})
|
||||
.state('assignments.assignment.list', {})
|
||||
.state('assignments.assignment.detail', {
|
||||
controller: 'AssignmentDetailCtrl',
|
||||
resolve: {
|
||||
assignmentId: ['$stateParams', function($stateParams) {
|
||||
return $stateParams.id;
|
||||
}],
|
||||
}
|
||||
})
|
||||
// redirects to assignment detail and opens assignment edit form dialog, uses edit url,
|
||||
// used by ui-sref links from agenda only
|
||||
// (from assignment controller use AssignmentForm factory instead to open dialog in front
|
||||
// of current view without redirect)
|
||||
.state('assignments.assignment.detail.update', {
|
||||
onEnter: ['$stateParams', '$state', 'ngDialog',
|
||||
function($stateParams, $state, ngDialog) {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/assignments/assignment-form.html',
|
||||
controller: 'AssignmentUpdateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
assignmentId: function() {
|
||||
return $stateParams.id;
|
||||
},
|
||||
},
|
||||
preCloseCallback: function() {
|
||||
$state.go('assignments.assignment.detail', {assignment: $stateParams.id});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Service for generic assignment form (create and update)
|
||||
.factory('AssignmentForm', [
|
||||
'gettextCatalog',
|
||||
'operator',
|
||||
'Editor',
|
||||
'Mediafile',
|
||||
'Tag',
|
||||
'Assignment',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
'ShowAsAgendaItemField',
|
||||
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree, ShowAsAgendaItemField) {
|
||||
return {
|
||||
// ngDialog for assignment form
|
||||
getDialog: function (assignment) {
|
||||
return {
|
||||
template: 'static/templates/assignments/assignment-form.html',
|
||||
controller: (assignment) ? 'AssignmentUpdateCtrl' : 'AssignmentCreateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
assignmentId: function () {return assignment ? assignment.id : void 0;}
|
||||
},
|
||||
};
|
||||
},
|
||||
// angular-formly fields for assignment form
|
||||
getFormFields: function (isCreateForm) {
|
||||
var images = Mediafile.getAllImages();
|
||||
var formFields = [
|
||||
{
|
||||
key: 'title',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Title'),
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Description')
|
||||
},
|
||||
data: {
|
||||
ckeditorOptions: Editor.getOptions(images)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'open_posts',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Number of persons to be elected'),
|
||||
type: 'number',
|
||||
min: 1,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'poll_description_default',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Default comment on the ballot paper')
|
||||
}
|
||||
}];
|
||||
|
||||
// show as agenda item + parent item
|
||||
if (isCreateForm) {
|
||||
formFields.push(ShowAsAgendaItemField('assignments.can_manage'));
|
||||
formFields.push({
|
||||
key: 'agenda_parent_id',
|
||||
type: 'select-single',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Parent item'),
|
||||
options: AgendaTree.getFlatTree(Agenda.getAll()),
|
||||
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
|
||||
placeholder: gettextCatalog.getString('Select a parent item ...')
|
||||
},
|
||||
hide: !operator.hasPerms('agenda.can_manage')
|
||||
});
|
||||
}
|
||||
// more (with tags field)
|
||||
if (Tag.getAll().length > 0) {
|
||||
formFields.push(
|
||||
{
|
||||
key: 'more',
|
||||
type: 'checkbox',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Show extended fields')
|
||||
},
|
||||
hide: !operator.hasPerms('assignments.can_manage')
|
||||
},
|
||||
{
|
||||
template: '<hr class="smallhr">',
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'tags_id',
|
||||
type: 'select-multiple',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Tags'),
|
||||
options: Tag.getAll(),
|
||||
ngOptions: 'option.id as option.name for option in to.options',
|
||||
placeholder: gettextCatalog.getString('Select or search a tag ...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return formFields;
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Cache for AssignmentPollDetailCtrl so that users choices are keeped during user actions (e. g. save poll form).
|
||||
.value('AssignmentPollDetailCtrlCache', {})
|
||||
|
||||
// Child controller of AssignmentDetailCtrl for each single poll.
|
||||
.controller('AssignmentPollDetailCtrl', [
|
||||
'$scope',
|
||||
'MajorityMethodChoices',
|
||||
'Config',
|
||||
'AssignmentPollDetailCtrlCache',
|
||||
'AssignmentPoll',
|
||||
'AssignmentPollDecimalPlaces',
|
||||
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache,
|
||||
AssignmentPoll, AssignmentPollDecimalPlaces) {
|
||||
// Define choices.
|
||||
$scope.methodChoices = MajorityMethodChoices;
|
||||
// TODO: Get $scope.baseChoices from config_variables.py without copying them.
|
||||
|
||||
$scope.votesPrecision = AssignmentPollDecimalPlaces.getPlaces($scope.poll);
|
||||
|
||||
// Setup empty cache with default values.
|
||||
if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
|
||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||
method: $scope.config('assignments_poll_default_majority_method'),
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch users choices from cache.
|
||||
$scope.method = AssignmentPollDetailCtrlCache[$scope.poll.id].method;
|
||||
|
||||
$scope.recalculateMajorities = function (method) {
|
||||
$scope.method = method;
|
||||
_.forEach($scope.poll.options, function (option) {
|
||||
option.majorityReached = option.isReached(method);
|
||||
});
|
||||
};
|
||||
$scope.recalculateMajorities($scope.method);
|
||||
|
||||
$scope.saveDescriptionChange = function (poll) {
|
||||
AssignmentPoll.save(poll);
|
||||
};
|
||||
|
||||
// Save current values to cache on destroy of this controller.
|
||||
$scope.$on('$destroy', function() {
|
||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||
method: $scope.method,
|
||||
};
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentListCtrl', [
|
||||
'$scope',
|
||||
'ngDialog',
|
||||
'AssignmentForm',
|
||||
'Assignment',
|
||||
'Tag',
|
||||
'Agenda',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
'gettextCatalog',
|
||||
'User',
|
||||
'osTableFilter',
|
||||
'osTableSort',
|
||||
'osTablePagination',
|
||||
'gettext',
|
||||
'AssignmentPhases',
|
||||
'AssignmentPdfExport',
|
||||
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector,
|
||||
ProjectionDefault, gettextCatalog, User, osTableFilter, osTableSort, osTablePagination,
|
||||
gettext, AssignmentPhases, AssignmentPdfExport) {
|
||||
$scope.$watch(function () {
|
||||
return Assignment.lastModified();
|
||||
}, function () {
|
||||
$scope.assignments = _.orderBy(Assignment.getAll(), ['title']);
|
||||
});
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
|
||||
if (projectiondefault) {
|
||||
$scope.defaultProjectorId = projectiondefault.projector_id;
|
||||
}
|
||||
});
|
||||
$scope.phases = AssignmentPhases;
|
||||
$scope.alert = {};
|
||||
|
||||
// Filtering
|
||||
$scope.filter = osTableFilter.createInstance('AssignmentTableFilter');
|
||||
|
||||
if (!$scope.filter.existsStorageEntry()) {
|
||||
$scope.filter.multiselectFilters = {
|
||||
tag: [],
|
||||
phase: [],
|
||||
};
|
||||
}
|
||||
$scope.filter.propertyList = ['title', 'description'];
|
||||
$scope.filter.propertyFunctionList = [
|
||||
function (assignment) {
|
||||
return gettextCatalog.getString($scope.phases[assignment.phase].display_name);
|
||||
},
|
||||
];
|
||||
$scope.filter.propertyDict = {
|
||||
'assignment_related_users': function (candidate) {
|
||||
return candidate.user.get_short_name();
|
||||
},
|
||||
'tags': function (tag) {
|
||||
return tag.name;
|
||||
},
|
||||
};
|
||||
$scope.getItemId = {
|
||||
tag: function (assignment) {return assignment.tags_id;},
|
||||
phase: function (assignment) {return assignment.phase;},
|
||||
};
|
||||
|
||||
// Sorting
|
||||
$scope.sort = osTableSort.createInstance('AssignmentTableSort');
|
||||
if (!$scope.sort.column) {
|
||||
$scope.sort.column = 'title';
|
||||
}
|
||||
$scope.sortOptions = [
|
||||
{name: 'agenda_item.getItemNumberWithAncestors()',
|
||||
display_name: gettext('Item')},
|
||||
{name: 'title',
|
||||
display_name: gettext('Title')},
|
||||
{name: 'phase',
|
||||
display_name: gettext('Phase')},
|
||||
{name: 'assignment_related_users.length',
|
||||
display_name: gettext('Number of candidates')},
|
||||
];
|
||||
$scope.hasTag = function (assignment, tag) {
|
||||
return _.indexOf(assignment.tags_id, tag.id) > -1;
|
||||
};
|
||||
$scope.toggleTag = function (assignment, tag) {
|
||||
if ($scope.hasTag(assignment, tag)) {
|
||||
assignment.tags_id = _.filter(assignment.tags_id, function (tag_id){
|
||||
return tag_id != tag.id;
|
||||
});
|
||||
} else {
|
||||
assignment.tags_id.push(tag.id);
|
||||
}
|
||||
Assignment.save(assignment);
|
||||
};
|
||||
|
||||
// Pagination
|
||||
$scope.pagination = osTablePagination.createInstance('AssignmentTablePagination');
|
||||
|
||||
// update phase
|
||||
$scope.updatePhase = function (assignment, phase_id) {
|
||||
assignment.phase = phase_id;
|
||||
Assignment.save(assignment);
|
||||
};
|
||||
// open new/edit dialog
|
||||
$scope.openDialog = function (assignment) {
|
||||
ngDialog.open(AssignmentForm.getDialog(assignment));
|
||||
};
|
||||
// *** select mode functions ***
|
||||
$scope.isSelectMode = false;
|
||||
// check all checkboxes
|
||||
$scope.checkAll = function () {
|
||||
$scope.selectedAll = !$scope.selectedAll;
|
||||
angular.forEach($scope.assignments, function (assignment) {
|
||||
assignment.selected = $scope.selectedAll;
|
||||
});
|
||||
};
|
||||
// uncheck all checkboxes if isSelectMode is closed
|
||||
$scope.uncheckAll = function () {
|
||||
if (!$scope.isSelectMode) {
|
||||
$scope.selectedAll = false;
|
||||
angular.forEach($scope.assignments, function (assignment) {
|
||||
assignment.selected = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
// delete all selected assignments
|
||||
$scope.deleteMultiple = function () {
|
||||
angular.forEach($scope.assignments, function (assignment) {
|
||||
if (assignment.selected)
|
||||
Assignment.destroy(assignment.id);
|
||||
});
|
||||
$scope.isSelectMode = false;
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
// delete single assignment
|
||||
$scope.delete = function (assignment) {
|
||||
Assignment.destroy(assignment.id);
|
||||
};
|
||||
// create the PDF List
|
||||
$scope.pdfExport = function () {
|
||||
AssignmentPdfExport.export($scope.assignmentsFiltered);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentDetailCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$filter',
|
||||
'$timeout',
|
||||
'filterFilter',
|
||||
'gettext',
|
||||
'ngDialog',
|
||||
'AssignmentForm',
|
||||
'operator',
|
||||
'Assignment',
|
||||
'User',
|
||||
'assignmentId',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
'gettextCatalog',
|
||||
'AssignmentPhases',
|
||||
'AssignmentPdfExport',
|
||||
'WebpageTitle',
|
||||
'ErrorMessage',
|
||||
function($scope, $http, $filter, $timeout, filterFilter, gettext, ngDialog, AssignmentForm, operator,
|
||||
Assignment, User, assignmentId, Projector, ProjectionDefault, gettextCatalog, AssignmentPhases,
|
||||
AssignmentPdfExport, WebpageTitle, ErrorMessage) {
|
||||
User.bindAll({}, $scope, 'users');
|
||||
var assignment = Assignment.get(assignmentId);
|
||||
Assignment.loadRelations(assignment, 'agenda_item');
|
||||
// This flag is for setting 'activeTab' to recently added (last) ballot tab.
|
||||
// Set this flag, if ballots are added/removed. When the next autoupdate comes
|
||||
// in, the tabset will be updated.
|
||||
var updateBallotTabsFlag = true;
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
|
||||
if (projectiondefault) {
|
||||
$scope.defaultProjectorId = projectiondefault.projector_id;
|
||||
}
|
||||
});
|
||||
$scope.$watch(function () {
|
||||
return Assignment.lastModified(assignmentId);
|
||||
}, function () {
|
||||
// setup sorting of candidates
|
||||
$scope.relatedUsersSorted = $filter('orderBy')(assignment.assignment_related_users, 'weight');
|
||||
$scope.assignment = Assignment.get(assignment.id);
|
||||
if (updateBallotTabsFlag) {
|
||||
$scope.activeTab = $scope.assignment.polls.length - 1;
|
||||
updateBallotTabsFlag = false;
|
||||
}
|
||||
WebpageTitle.updateTitle(gettextCatalog.getString('Election') + ' ' + $scope.assignment.title);
|
||||
});
|
||||
$scope.candidateSelectBox = {};
|
||||
$scope.phases = AssignmentPhases;
|
||||
$scope.alert = {};
|
||||
|
||||
// open edit dialog
|
||||
$scope.openDialog = function () {
|
||||
ngDialog.open(AssignmentForm.getDialog($scope.assignment));
|
||||
};
|
||||
// add (nominate) candidate
|
||||
$scope.addCandidate = function (userId) {
|
||||
$http.post('/rest/assignments/assignment/' + assignmentId + '/candidature_other/', {'user': userId})
|
||||
.then(function (success){
|
||||
$scope.alert.show = false;
|
||||
$scope.candidateSelectBox = {};
|
||||
}, function (error){
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
$scope.candidateSelectBox = {};
|
||||
});
|
||||
};
|
||||
// remove candidate
|
||||
$scope.removeCandidate = function (userId) {
|
||||
$http.delete('/rest/assignments/assignment/' + assignmentId + '/candidature_other/',
|
||||
{headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({user: userId})})
|
||||
.then(function (success) {},
|
||||
function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
// add me (nominate self as candidate)
|
||||
$scope.addMe = function () {
|
||||
$http.post('/rest/assignments/assignment/' + assignmentId + '/candidature_self/', {}).then(
|
||||
function (success) {
|
||||
$scope.alert.show = false;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
// remove me (withdraw own candidature)
|
||||
$scope.removeMe = function () {
|
||||
$http.delete('/rest/assignments/assignment/' + assignmentId + '/candidature_self/').then(
|
||||
function (success) {
|
||||
$scope.alert.show = false;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
// check if current user is already a candidate (elected==false)
|
||||
$scope.isCandidate = function () {
|
||||
var check = $scope.assignment.assignment_related_users.map(function(candidate) {
|
||||
if (!candidate.elected) {
|
||||
return candidate.user_id;
|
||||
}
|
||||
}).indexOf(operator.user.id);
|
||||
if (check > -1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// Sort all candidates
|
||||
$scope.treeOptions = {
|
||||
dropped: function () {
|
||||
var sortedCandidates = [];
|
||||
_.forEach($scope.relatedUsersSorted, function (user) {
|
||||
sortedCandidates.push(user.id);
|
||||
});
|
||||
$http.post('/rest/assignments/assignment/' + assignmentId + '/sort_related_users/',
|
||||
{related_users: sortedCandidates}
|
||||
);
|
||||
}
|
||||
};
|
||||
// update phase
|
||||
$scope.updatePhase = function (phase_id) {
|
||||
$scope.assignment.phase = phase_id;
|
||||
Assignment.save($scope.assignment);
|
||||
};
|
||||
// create new ballot
|
||||
$scope.createBallot = function () {
|
||||
$http.post('/rest/assignments/assignment/' + assignmentId + '/create_poll/').then(
|
||||
function (success) {
|
||||
$scope.alert.show = false;
|
||||
if (assignment.phase === 0) {
|
||||
$scope.updatePhase(1);
|
||||
}
|
||||
updateBallotTabsFlag = true;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
// delete ballot
|
||||
$scope.deleteBallot = function (poll) {
|
||||
poll.DSDestroy().then(
|
||||
function (success) {
|
||||
$scope.activeTab = $scope.activeTab - 1;
|
||||
updateBallotTabsFlag = true;
|
||||
}
|
||||
);
|
||||
};
|
||||
// edit poll dialog
|
||||
$scope.editPollDialog = function (poll, ballot) {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/assignments/assignmentpoll-form.html',
|
||||
controller: 'AssignmentPollUpdateCtrl',
|
||||
className: 'ngdialog-theme-default',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
assignmentpollId: function () {return poll.id;},
|
||||
ballot: function () {return ballot;},
|
||||
}
|
||||
});
|
||||
};
|
||||
// publish ballot
|
||||
$scope.togglePublishBallot = function (poll) {
|
||||
poll.DSUpdate({
|
||||
assignment_id: assignmentId,
|
||||
published: !poll.published,
|
||||
})
|
||||
.then(function (success) {
|
||||
$scope.alert.show = false;
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
// mark candidate as (not) elected
|
||||
$scope.markElected = function (user, reverse) {
|
||||
if (reverse) {
|
||||
$http.delete(
|
||||
'/rest/assignments/assignment/' + assignmentId + '/mark_elected/', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({user: user})
|
||||
});
|
||||
} else {
|
||||
$http.post('/rest/assignments/assignment/' + assignmentId + '/mark_elected/', {'user': user});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Creates the document as pdf
|
||||
$scope.pdfExport = function() {
|
||||
AssignmentPdfExport.export($scope.assignment, true);
|
||||
};
|
||||
// Creates the ballotpaper as pdf
|
||||
$scope.ballotpaperExport = function(pollId) {
|
||||
AssignmentPdfExport.createBallotPdf($scope.assignment, pollId);
|
||||
};
|
||||
|
||||
// Just mark some vote value strings for translation.
|
||||
gettext('Yes');
|
||||
gettext('No');
|
||||
gettext('Abstain');
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentCreateCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'Assignment',
|
||||
'AssignmentForm',
|
||||
'Agenda',
|
||||
'Config',
|
||||
'ErrorMessage',
|
||||
function($scope, $state, Assignment, AssignmentForm, Agenda, Config, ErrorMessage) {
|
||||
$scope.model = {
|
||||
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
|
||||
};
|
||||
// set default value for open posts form field
|
||||
$scope.model.open_posts = 1;
|
||||
// get all form fields
|
||||
$scope.formFields = AssignmentForm.getFormFields(true);
|
||||
// save assignment
|
||||
$scope.save = function(assignment, gotoDetailView) {
|
||||
Assignment.create(assignment).then(
|
||||
function (success) {
|
||||
if (gotoDetailView) {
|
||||
$state.go('assignments.assignment.detail', {id: success.id});
|
||||
}
|
||||
$scope.closeThisDialog();
|
||||
},
|
||||
function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentUpdateCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'Assignment',
|
||||
'AssignmentForm',
|
||||
'Agenda',
|
||||
'assignmentId',
|
||||
'ErrorMessage',
|
||||
function($scope, $state, Assignment, AssignmentForm, Agenda, assignmentId, ErrorMessage) {
|
||||
var assignment = Assignment.get(assignmentId);
|
||||
$scope.alert = {};
|
||||
// set initial values for form model by create deep copy of assignment object
|
||||
// so list/detail view is not updated while editing
|
||||
$scope.model = angular.copy(assignment);
|
||||
// get all form fields
|
||||
$scope.formFields = AssignmentForm.getFormFields();
|
||||
|
||||
// save assignment
|
||||
$scope.save = function (assignment, gotoDetailView) {
|
||||
// inject the changed assignment (copy) object back into DS store
|
||||
Assignment.inject(assignment);
|
||||
// save changed assignment object on server
|
||||
Assignment.save(assignment).then(
|
||||
function(success) {
|
||||
if (gotoDetailView) {
|
||||
$state.go('assignments.assignment.detail', {id: success.id});
|
||||
}
|
||||
$scope.closeThisDialog();
|
||||
},
|
||||
function (error) {
|
||||
// save error: revert all changes by restore
|
||||
// (refresh) original assignment object from server
|
||||
Assignment.refresh(assignment);
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentPollUpdateCtrl', [
|
||||
'$scope',
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'AssignmentPoll',
|
||||
'assignmentpollId',
|
||||
'AssignmentPollDecimalPlaces',
|
||||
'ballot',
|
||||
'ErrorMessage',
|
||||
function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId,
|
||||
AssignmentPollDecimalPlaces, ballot, ErrorMessage) {
|
||||
// set initial values for form model by create deep copy of assignmentpoll object
|
||||
// so detail view is not updated while editing poll
|
||||
var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId));
|
||||
$scope.model = assignmentpoll;
|
||||
$scope.ballot = ballot;
|
||||
$scope.formFields = [];
|
||||
$scope.alert = {};
|
||||
|
||||
// For number inputs
|
||||
var step = Math.pow(10, -AssignmentPollDecimalPlaces.getPlaces(assignmentpoll));
|
||||
|
||||
// add dynamic form fields
|
||||
var options = $filter('orderBy')(assignmentpoll.options, 'weight');
|
||||
_.forEach(options, function(option) {
|
||||
var defaultValue;
|
||||
if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') {
|
||||
defaultValue = {};
|
||||
_.forEach(option.votes, function (vote) {
|
||||
defaultValue[vote.value.toLowerCase()] = vote.weight;
|
||||
});
|
||||
|
||||
var columnClass = (assignmentpoll.pollmethod === 'yna') ? 'col-xs-4' : 'col-xs-6';
|
||||
var columns = [
|
||||
{
|
||||
key: 'yes_' + option.candidate_id,
|
||||
type: 'input',
|
||||
className: columnClass + ' no-padding-left',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Yes'),
|
||||
type: 'number',
|
||||
min: -2,
|
||||
step: step,
|
||||
required: true
|
||||
},
|
||||
defaultValue: defaultValue.yes
|
||||
},
|
||||
{
|
||||
key: 'no_' + option.candidate_id,
|
||||
type: 'input',
|
||||
className: columnClass + ' no-padding-left' +
|
||||
(assignmentpoll.pollmethod === 'yn' ? ' no-padding-right' : ''),
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('No'),
|
||||
type: 'number',
|
||||
min: -2,
|
||||
step: step,
|
||||
required: true
|
||||
},
|
||||
defaultValue: defaultValue.no
|
||||
}
|
||||
];
|
||||
if (assignmentpoll.pollmethod == 'yna'){
|
||||
columns.push({
|
||||
key:'abstain_' + option.candidate_id,
|
||||
type: 'input',
|
||||
className: columnClass + ' no-padding-left no-padding-right',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Abstain'),
|
||||
type: 'number',
|
||||
min: -2,
|
||||
step: step,
|
||||
required: true
|
||||
},
|
||||
defaultValue: defaultValue.abstain
|
||||
});
|
||||
}
|
||||
$scope.formFields.push({
|
||||
noFormControl: true,
|
||||
template: '<strong>' + option.candidate.get_full_name() + '</strong>'
|
||||
},
|
||||
{
|
||||
className: 'row',
|
||||
fieldGroup: columns,
|
||||
});
|
||||
} else { // votes method
|
||||
if (option.votes.length) {
|
||||
defaultValue = option.votes[0].weight;
|
||||
}
|
||||
$scope.formFields.push({
|
||||
key: 'vote_' + option.candidate_id,
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: option.candidate.get_full_name(),
|
||||
type: 'number',
|
||||
min: -2,
|
||||
step: step,
|
||||
required: true,
|
||||
},
|
||||
defaultValue: defaultValue
|
||||
});
|
||||
}
|
||||
});
|
||||
if (assignmentpoll.pollmethod == 'votes'){
|
||||
$scope.formFields.push(
|
||||
{
|
||||
key: 'votesabstain',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Abstain'),
|
||||
type: 'number',
|
||||
step: step,
|
||||
min: -2,
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'votesno',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('No'),
|
||||
type: 'number',
|
||||
step: step,
|
||||
min: -2,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// add general form fields
|
||||
$scope.formFields.push(
|
||||
{
|
||||
template: '<hr class="smallhr">',
|
||||
},
|
||||
{
|
||||
key: 'votesvalid',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Valid ballots'),
|
||||
type: 'number',
|
||||
step: step,
|
||||
min: -2,
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'votesinvalid',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Invalid ballots'),
|
||||
type: 'number',
|
||||
step: step,
|
||||
min: -2,
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'votescast',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Casted ballots'),
|
||||
type: 'number',
|
||||
step: step,
|
||||
min: -2,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// save assignmentpoll
|
||||
$scope.save = function (poll) {
|
||||
var votes = [];
|
||||
if (assignmentpoll.pollmethod == 'yna') {
|
||||
assignmentpoll.options.forEach(function(option) {
|
||||
votes.push({
|
||||
'Yes': poll['yes_' + option.candidate_id],
|
||||
'No': poll['no_' + option.candidate_id],
|
||||
'Abstain': poll['abstain_' + option.candidate_id]
|
||||
});
|
||||
});
|
||||
} else if (assignmentpoll.pollmethod == 'yn') {
|
||||
assignmentpoll.options.forEach(function(option) {
|
||||
votes.push({
|
||||
'Yes': poll['yes_' + option.candidate_id],
|
||||
'No': poll['no_' + option.candidate_id]
|
||||
});
|
||||
});
|
||||
} else {
|
||||
assignmentpoll.options.forEach(function(option) {
|
||||
votes.push({
|
||||
'Votes': poll['vote_' + option.candidate_id],
|
||||
});
|
||||
});
|
||||
}
|
||||
// save change poll object on server
|
||||
poll.DSUpdate({
|
||||
assignment_id: poll.assignment_id,
|
||||
votes: votes,
|
||||
votesabstain: poll.votesabstain,
|
||||
votesno: poll.votesno,
|
||||
votesvalid: poll.votesvalid,
|
||||
votesinvalid: poll.votesinvalid,
|
||||
votescast: poll.votescast,
|
||||
})
|
||||
.then(function(success) {
|
||||
$scope.alert.show = false;
|
||||
$scope.closeThisDialog();
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
//mark all assignment config strings for translation with Javascript
|
||||
.config([
|
||||
'gettext',
|
||||
function (gettext) {
|
||||
gettext('Election method');
|
||||
gettext('Automatic assign of method');
|
||||
gettext('Always one option per candidate');
|
||||
gettext('Always Yes-No-Abstain per candidate');
|
||||
gettext('Always Yes/No per candidate');
|
||||
gettext('Elections');
|
||||
gettext('Ballot and ballot papers');
|
||||
gettext('The 100-%-base of an election result consists of');
|
||||
gettext('For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base ' +
|
||||
'depends on the election method: If there is only one option per candidate, ' +
|
||||
'the sum of all votes of all candidates is 100 %. Otherwise for each ' +
|
||||
'candidate the sum of all votes is 100 %.');
|
||||
gettext('Yes/No/Abstain per candidate');
|
||||
gettext('Yes/No per candidate');
|
||||
gettext('All valid ballots');
|
||||
gettext('All casted ballots');
|
||||
gettext('Disabled (no percents)');
|
||||
gettext('Number of ballot papers (selection)');
|
||||
gettext('Number of all delegates');
|
||||
gettext('Number of all participants');
|
||||
gettext('Use the following custom number');
|
||||
gettext('Custom number of ballot papers');
|
||||
gettext('Required majority');
|
||||
gettext('Default method to check whether a candidate has reached the required majority.');
|
||||
gettext('Simple majority');
|
||||
gettext('Two-thirds majority');
|
||||
gettext('Three-quarters majority');
|
||||
gettext('Disabled');
|
||||
gettext('Put all candidates on the list of speakers');
|
||||
gettext('Title for PDF document (all elections)');
|
||||
gettext('Preamble text for PDF document (all elections)');
|
||||
//other translations
|
||||
gettext('Searching for candidates');
|
||||
gettext('Voting');
|
||||
gettext('Finished');
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,299 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="assignments.assignment.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Back to overview</translate>
|
||||
</a>
|
||||
<!-- List of speakers -->
|
||||
<a ui-sref="agenda.item.detail({id: assignment.agenda_item_id})"
|
||||
os-perms="agenda.can_see" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-microphone fa-lg"></i>
|
||||
<translate>List of speakers</translate>
|
||||
</a>
|
||||
<!-- project -->
|
||||
<projector-button model="assignment", default-projector-id="defaultProjectorId"
|
||||
title="{{ 'Project' | translate }}">
|
||||
</projector-button>
|
||||
<!-- edit -->
|
||||
<a os-perms="assignments.can_manage" ng-click="openDialog()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<!-- pdf -->
|
||||
<a ng-click="pdfExport()" target="_blank" class="btn btn-default btn-sm"
|
||||
title="{{ 'Export as PDF' | translate}}">
|
||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||
<translate>PDF</translate>
|
||||
</a>
|
||||
</div>
|
||||
<h1>{{ assignment.agenda_item.getTitle() || assignment.title }}</h1>
|
||||
<h2>
|
||||
<translate>Election</translate>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div class="title" ng-click="isMeta = !isMeta">
|
||||
<div class="name">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<translate>Meta information</translate>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fa fa-lg" ng-class="isMeta ? 'fa-angle-down' : 'fa-angle-up'"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail" uib-collapse="isMeta">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- posts -->
|
||||
<h3 translate>Number of persons to be elected</h3>
|
||||
{{ assignment.open_posts }}<br>
|
||||
<!-- Tags -->
|
||||
<h3 ng-if="assignment.tags.length > 0" translate>Tags</h3>
|
||||
<span ng-repeat="tag in assignment.tags">
|
||||
{{ tag.name }}{{$last ? '' : ', '}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- phase -->
|
||||
<h3 translate>Phase</h3>
|
||||
<span class="label" ng-class="{'label-primary': assignment.phase == 0,
|
||||
'label-warning': assignment.phase == 1,
|
||||
'label-success': assignment.phase == 2 }">
|
||||
{{ phases[assignment.phase].display_name | translate }}
|
||||
</span>
|
||||
<div os-perms="assignments.can_manage" class="spacer">
|
||||
<select ng-model="phaseSelect" class="form-control" ng-change="updatePhase(phaseSelect)">
|
||||
<option value="" translate>--- Set phase ---</option>
|
||||
<option ng-repeat="phase in phases" value="{{ phase.value }}">{{ phase.display_name | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div ng-if="assignment.description" class="clearfix">
|
||||
<h3 translate>Description</h3>
|
||||
<div ng-bind-html="assignment.description | trusted"></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="assignment.phase !== 2">
|
||||
<h3 translate>Candidates</h3>
|
||||
<div ui-tree="treeOptions" ng-if="assignment.assignment_related_users.length"
|
||||
data-empty-placeholder-enabled="false">
|
||||
<ol ui-tree-nodes="" ng-model="relatedUsersSorted">
|
||||
<li ui-tree-node ng-repeat="related_user in assignment.assignment_related_users | orderBy:'weight'">
|
||||
<i ui-tree-handle="" class="fa fa-arrows-v spacer-right" os-perms="assignments.can_manage"></i>
|
||||
<a class="spacer-right" ui-sref="users.user.detail({id: related_user.user_id})">
|
||||
{{ related_user.user.get_full_name() }}
|
||||
</a>
|
||||
<i ng-if="related_user.elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
<a href os-perms="assignments.can_manage" ng-click="removeCandidate(related_user.user_id)" class="btn btn-default btn-xs">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="form-group spacer-top-lg">
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
<div os-perms="assignments.can_nominate_other">
|
||||
<select chosen ng-model="candidateSelectBox.selected" ng-change="addCandidate(candidateSelectBox.selected)"
|
||||
ng-options="user.id as user.get_full_name() for user in users"
|
||||
search-contains="true"
|
||||
placeholder-text-single="'Select or search a participant ...' | translate"
|
||||
no-results-text="'No results available ...' | translate"
|
||||
class="form-control">
|
||||
<select>
|
||||
</div>
|
||||
<p os-perms="assignments.can_nominate_self" class="spacer">
|
||||
<button ng-if="!isCandidate()" ng-click="addMe()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>Add me</translate>
|
||||
</button>
|
||||
<button ng-if="isCandidate()" ng-click="removeMe()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-minus"></i>
|
||||
<translate>Remove me</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 translate>Election result</h3>
|
||||
<template-hook hook-name="assignmentPollVotingHeader"></template-hook>
|
||||
<template-hook hook-name="assignmentPollNewBallotButton">
|
||||
<button os-perms="assignments.can_manage" ng-show="assignment.phase !== 2" ng-click="createBallot()"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-bar-chart fa-lg"></i>
|
||||
<translate>New ballot</translate>
|
||||
</button>
|
||||
</template-hook>
|
||||
|
||||
<uib-tabset ng-if="assignment.polls.length > 0" class="spacer ballot-tabs" active="$parent.activeTab">
|
||||
<uib-tab ng-repeat="poll in assignment.polls | orderBy:'id'"
|
||||
index="$index" heading="{{ 'Ballot' | translate }} {{ $index + 1 }}">
|
||||
<div ng-controller="AssignmentPollDetailCtrl">
|
||||
<!-- action buttons -->
|
||||
<div class="pull-right" os-perms="assignments.can_manage" ng-show="assignment.phase !== 2">
|
||||
<span class="spacer-right" editable-text="poll.description" onaftersave="saveDescriptionChange(poll)"
|
||||
uib-tooltip="{{ 'Hint on the ballot paper.' | translate }}">
|
||||
<span ng-if="!poll.description" translate>Set hint for ballot paper ...</span>
|
||||
<span ng-if="poll.description">{{ poll.description }}</span>
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
<!-- delete -->
|
||||
<a class="btn btn-danger btn-xs"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
|
||||
ng-bootbox-confirm-action="deleteBallot(poll)">
|
||||
<i class="fa fa-trash"></i>
|
||||
<translate>Delete</translate>
|
||||
</a>
|
||||
</div>
|
||||
<div os-perms="assignments.can_manage" class="spacer" role="group">
|
||||
<!-- angular requires to open the link in new tab with "target='_blank'".
|
||||
Otherwise the pdf url can't be open in same window; angular redirects to "/". -->
|
||||
<!-- PDF -->
|
||||
<a ng-click="ballotpaperExport(poll.id)" target="_blank" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<translate>Print ballot paper</translate>
|
||||
</a>
|
||||
<!-- Edit -->
|
||||
<button ng-click="assignment.phase !== 2 && editPollDialog(poll, $index+1)"
|
||||
ng-class="{ 'disabled': assignment.phase === 2 }"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<translate>Enter votes</translate>
|
||||
</button>
|
||||
<!-- Publish -->
|
||||
<button ng-click="togglePublishBallot(poll)"
|
||||
ng-class="poll.published ? 'btn-primary' : 'btn-default'"
|
||||
class="btn btn-sm">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Publish</translate>
|
||||
</button>
|
||||
<!-- Project -->
|
||||
<projector-button model="assignment" default-projector-id="defaultProjectorId"
|
||||
arg="poll.id" content="{{ 'Project' | translate }}">
|
||||
</projector-button>
|
||||
</div>
|
||||
|
||||
<!-- template hook for assignment poll small buttons -->
|
||||
<template-hook hook-name="assignmentPollSmallButtons"></template-hook>
|
||||
|
||||
<div class="results spacer-top-lg">
|
||||
|
||||
<!-- list of candidates of selected poll (without election result) -->
|
||||
<div ng-if="!poll.has_votes">
|
||||
<strong translate>Candidates</strong>
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
||||
{{ option.candidate.get_full_name() }}
|
||||
</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Settings for majority calculations -->
|
||||
<div os-perms="assignments.can_manage" ng-show="poll.has_votes" ng-cloak>
|
||||
<div class="input-group">
|
||||
<span><translate>Required majority</translate>: </span>
|
||||
<select ng-model="method"
|
||||
ng-change="recalculateMajorities(method)"
|
||||
ng-options="option.value as option.display_name | translate for option in methodChoices" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- election result of poll -->
|
||||
<table ng-if="poll.has_votes" class="table table-bordered table-striped minimumTable spacer">
|
||||
<tr>
|
||||
<th translate>Candidates
|
||||
<th translate>Votes
|
||||
<th translate ng-hide="method === 'disabled'">Quorum
|
||||
</th>
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
<!-- candidate name -->
|
||||
<td>
|
||||
<span os-perms="assignments.can_manage">
|
||||
<i ng-if="option.is_elected" class="fa fa-check-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is elected' | translate }}"></i>
|
||||
<i ng-if="!option.is_elected" class="fa fa-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is not elected' | translate }}"></i>
|
||||
|
||||
</span>
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
|
||||
|
||||
<!-- votes -->
|
||||
<td>
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-repeat="vote in votes">
|
||||
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<div ng-if="vote.percentNumber >= 0">
|
||||
<uib-progressbar ng-if="$index == 0" value="vote.percentNumber" type="success"></uib-progressbar>
|
||||
<uib-progressbar ng-if="$index == 1" value="vote.percentNumber" type="danger"></uib-progressbar>
|
||||
<uib-progressbar ng-if="$index == 2" value="vote.percentNumber" type="warning"></uib-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<td ng-hide="method === 'disabled'">
|
||||
<span ng-if="option.majorityReached >= 0" class="text-success" translate>
|
||||
Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) reached.
|
||||
</span>
|
||||
<span ng-if="option.majorityReached < 0" class="text-danger" translate>
|
||||
Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) not reached.
|
||||
</span>
|
||||
|
||||
<!-- total votes (valid/invalid/casts) -->
|
||||
<tr ng-if="poll.pollmethod === 'votes'">
|
||||
<td>
|
||||
<translate>Abstain</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesabstain').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesabstain').percentStr }}
|
||||
<tr ng-if="poll.pollmethod === 'votes'">
|
||||
<td>
|
||||
<translate>No</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesno').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesno').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesvalid').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesvalid').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesinvalid').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesinvalid').percentStr }}
|
||||
<tr class="total bg-info">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votescast').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votescast').percentStr }}
|
||||
</table>
|
||||
|
||||
<strong translate>Election method</strong><br>
|
||||
<span ng-if="poll.pollmethod == 'votes'" translate>One vote per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yna'" translate>Yes/No/Abstain per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yn'" translate>Yes/No per candidate</span>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
|
||||
<!-- Workaround to prevent page scrolling up after autoupdate. -->
|
||||
<div style="height: 700px"></div>
|
||||
</div>
|
||||
|
||||
<template-hook hook-name="assignmentDetailViewDetailContainer"></template-hook>
|
@ -1,22 +0,0 @@
|
||||
<h1 ng-if="model.id" translate>Edit election</h1>
|
||||
<h1 ng-if="!model.id" translate>New election</h1>
|
||||
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
|
||||
<form name="assignmentForm" ng-submit="save(model, gotoDetailView)">
|
||||
<formly-form model="model" fields="formFields">
|
||||
<hr class="smallhr">
|
||||
<div class="checkbox pointer" ng-click="$parent.$parent.gotoDetailView = !$parent.$parent.gotoDetailView">
|
||||
<i class="fa" ng-class="$parent.$parent.gotoDetailView ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
<translate>Open election detail view after save.</translate>
|
||||
</div>
|
||||
<button type="submit" ng-disabled="assignmentForm.$invalid" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</formly-form>
|
||||
</form>
|
@ -1,348 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ng-click="openDialog()" os-perms="assignments.can_manage" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</a>
|
||||
<a ui-sref="core.tag.list" os-perms="core.can_manage_tags" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-tags fa-lg"></i>
|
||||
<translate>Tags</translate>
|
||||
</a>
|
||||
<template-hook hook-name="assignmentListMenuButton"></template-hook>
|
||||
</div>
|
||||
<h1 translate>Elections</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div class="row form-group">
|
||||
<div class="col-sm-12">
|
||||
<!-- select mode -->
|
||||
<button os-perms="assignments.can_manage" class="btn btn-sm"
|
||||
ng-class="$parent.isSelectMode ? 'btn-primary' : 'btn-default'"
|
||||
ng-click="$parent.isSelectMode = !$parent.isSelectMode; uncheckAll()">
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
<translate>Select ...</translate>
|
||||
</button>
|
||||
<!-- export button -->
|
||||
<button type="button" class="btn btn-default btn-sm pull-right" ng-click="pdfExport()">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<span ng-if="assignmentsFiltered.length == assignments.length" translate>
|
||||
Export all
|
||||
</span>
|
||||
<span ng-if="assignmentsFiltered.length != assignments.length" translate>
|
||||
Export filtered
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div uib-collapse="!isFilterOpen" class="row">
|
||||
<div class="col-sm-6 text-right"></div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<!-- phase filter -->
|
||||
<select ng-model="phaseFilter" class="form-control" id="phaseFilter">
|
||||
<option value="" translate>--- Select phase ---</option>
|
||||
<option ng-repeat="phase in phases" value="{{ phase.value }}">{{ phase.display_name | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div uib-collapse="!isSelectMode" class="row spacer">
|
||||
<div class="col-sm-12 text-left">
|
||||
<!-- delete button -->
|
||||
<a ng-show="isSelectMode" os-perms="assignments.can_manage"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected elections?' | translate }}"
|
||||
ng-bootbox-confirm-action="deleteMultiple()"
|
||||
class="btn btn-default btn-sm btn-danger">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Delete selected elections</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer-top-lg italic row">
|
||||
<div class="col-md-6">
|
||||
{{ assignmentsFiltered.length }} /
|
||||
{{ assignments.length }} {{ "elections" | translate }}<span ng-if="(assignments|filter:{selected:true}).length > 0">,
|
||||
{{(assignments|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</div>
|
||||
<div class="col-md-6" ng-show="assignmentsFiltered.length > pagination.itemsPerPage">
|
||||
<span class="pull-right">
|
||||
<a href="" class="pagination-arrow" ng-click="pagination.prevPage()"
|
||||
ng-if="pagination.showPrevPageArrow()">
|
||||
«
|
||||
</a>
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ pagination.getPageCount(assignmentsFiltered) }}
|
||||
<a href="" class="pagination-arrow" ng-click="pagination.nextPage(assignmentsFiltered)"
|
||||
ng-if="pagination.showNextPageArrow(assignmentsFiltered)">
|
||||
»
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="os-table container-fluid">
|
||||
<div class="row header-row">
|
||||
<div class="col-xs-1 centered" ng-show="isSelectMode">
|
||||
<i class="fa text-danger pointer" ng-class="selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
|
||||
ng-click="checkAll()"></i>
|
||||
</div>
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- clear all filters -->
|
||||
<span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
|
||||
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-window-close"></i>
|
||||
<translate>Filter</translate>
|
||||
</span>
|
||||
<!-- Tag filter -->
|
||||
<span uib-dropdown ng-if="tags.length > 0">
|
||||
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
|
||||
ng-class="{'bold': filter.multiselectFilters.tag.length > 0, 'disabled': isSelectMode}"
|
||||
ng-disabled="isSelectMode">
|
||||
<translate>Tag</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right"
|
||||
aria-labelledby="dropdownTag">
|
||||
<li ng-repeat="tag in tags">
|
||||
<a href ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)">
|
||||
<i class="fa fa-check" ng-if="filter.MultiselectFilters.tag.indexOf(tag.id) > -1"></i>
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)">
|
||||
<i class="fa fa-check" ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1"></i>
|
||||
<translate>No tag set</translate>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- Phase filter -->
|
||||
<span uib-dropdown>
|
||||
<span class="pointer" id="dropdownPhase" uib-dropdown-toggle
|
||||
ng-class="{'bold': filter.multiselectFilters.phase.length > 0, 'disabled': isSelectMode}"
|
||||
ng-disabled="isSelectMode">
|
||||
<translate>Phase</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right"
|
||||
aria-labelledby="dropdownPhase">
|
||||
<li ng-repeat="phase in phases">
|
||||
<a href ng-click="filter.operateMultiselectFilter('phase', phase.value, isSelectMode)">
|
||||
<i class="fa fa-check" ng-if="filter.multiselectFilters.phase.indexOf(phase.value) > -1"></i>
|
||||
{{ phase.display_name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- dropdown sort -->
|
||||
<span uib-dropdown>
|
||||
<span class="pointer" id="dropdownSort" uib-dropdown-toggle
|
||||
ng-class="{'disabled': isSelectMode}"
|
||||
ng-disabled="isSelectMode">
|
||||
<translate>Sort</translate>
|
||||
<span class="caret"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownSort">
|
||||
<!-- all sortOptions -->
|
||||
<li ng-repeat="option in sortOptions">
|
||||
<a ng-click="sort.toggle(option.name)">
|
||||
<span ng-style="{'font-weight': sort.column === option.name ? 'bold' : 'normal'}">
|
||||
{{ option.display_name | translate }}
|
||||
</span>
|
||||
<span class="spacer-right pull-right"></span>
|
||||
<i class="pull-right fa"
|
||||
ng-style="{'visibility': sort.column === option.name ? 'visible' : 'hidden'}"
|
||||
ng-class="sort.reverse ? 'fa-sort-amount-desc' : 'fa-sort-amount-asc'">
|
||||
</i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<!-- search field -->
|
||||
<span class="form-group">
|
||||
<span class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
<input type="text" ng-model="filter.filterString" class="form-control"
|
||||
placeholder="{{ 'Search' | translate}}" ng-disable="isSelectMode"
|
||||
ng-change="filter.save()">
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
<!-- show all selected multiselectoptions -->
|
||||
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
|
||||
ng-if="filter.multiselectFilters.tag.indexOf(tag.id) > -1"
|
||||
ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1" class="pointer spacer-left-lg"
|
||||
ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
<translate>No tag set</translate>
|
||||
</span>
|
||||
<span ng-repeat="phase in phases" class="pointer spacer-left-lg"
|
||||
ng-if="filter.multiselectFilters.phase.indexOf(phase.value) > -1"
|
||||
ng-click="filter.operateMultiselectFilter('phase', phase.value, isSelectMode)"
|
||||
ng-class="{'disabled': isSelectMode}">
|
||||
<span class="nobr">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ phase.display_name | translate }}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- main table -->
|
||||
<div class="row data-row" ng-mouseover="assignment.hover=true"
|
||||
ng-mouseleave="assignment.hover=false"
|
||||
ng-class="{'projected': assignment.isProjected().length,
|
||||
'related-projected': assignment.isRelatedProjected().length}"
|
||||
ng-repeat="assignment in assignmentsFiltered = (assignments
|
||||
| osFilter: filter.filterString : filter.getObjectQueryString
|
||||
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
|
||||
| MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase
|
||||
| orderByEmptyLast: sort.column : sort.reverse)
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="assignments.can_manage" class="col-xs-1 centered">
|
||||
<i class="fa text-danger pointer" ng-click="assignment.selected=!assignment.selected"
|
||||
ng-class="assignment.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
</div>
|
||||
|
||||
<!-- projector column -->
|
||||
<div class="col-xs-1 centered projector" os-perms="core.can_manage_projector">
|
||||
<projector-button model="assignment", default-projector-id="defaultProjectorId">
|
||||
</projector-button>
|
||||
</div>
|
||||
<div class="no-projector-spacer" os-perms="!core.can_manage_projector"></div>
|
||||
|
||||
<!-- main content column -->
|
||||
<div class="col-xs-6 content">
|
||||
<div class="title-col">
|
||||
<!-- title and phase -->
|
||||
<div>
|
||||
<a class="title" ui-sref="assignments.assignment.detail({id: assignment.id})">
|
||||
{{ assignment.title }}
|
||||
</a>
|
||||
<span style="padding: 5px;" ng-mouseover="assignment.phaseHover=true" ng-mouseleave="assignment.phaseHover=false">
|
||||
<span class="label" ng-class="{'label-primary': assignment.phase == 0,
|
||||
'label-warning': assignment.phase == 1,
|
||||
'label-success': assignment.phase == 2 }">
|
||||
{{ phases[assignment.phase].display_name | translate }}
|
||||
</span>
|
||||
<span os-perms="assignments.can_manage" ng-class="{'hiddenDiv': !assignment.phaseHover}" uib-dropdown>
|
||||
<i class="fa fa-cog pointer" uib-dropdown-toggle id="phaseDropdown{{ assignment.id }}"></i>
|
||||
<ul uib-dropdown-menu aria-labelledby="phaseDropdown{{ assignment.id }}"
|
||||
class="dropdown-menu">
|
||||
<li ng-repeat="phase in phases">
|
||||
<a href>
|
||||
<i class="fa fa-check" ng-if="assignment.phase == phase.value"></i>
|
||||
<span class="pointer" ng-click="updatePhase(assignment, phase.value)">{{ phase.display_name | translate }}</a>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- hover menu -->
|
||||
<div os-perms="assignments.can_manage" ng-class="{'hiddenDiv': !assignment.hover}">
|
||||
<small>
|
||||
<a href="" ng-click="openDialog(assignment)" translate>Edit</a> ·
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ assignment.title }}</b>"
|
||||
ng-bootbox-confirm-action="delete(assignment)" translate>Delete</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- additional content column -->
|
||||
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}">
|
||||
<div style="width: 60%;" class="optional">
|
||||
<small>
|
||||
<!-- Tag dropdown for manage user -->
|
||||
<div os-perms="assignments.can_manage" ng-show="tags.length > 0"
|
||||
ng-mouseover="assignment.tagHover=true"
|
||||
ng-mouseleave="assignment.tagHover=false">
|
||||
<span uib-dropdown>
|
||||
<span id="dropdownTags{{ assignment.id }}" class="pointer"
|
||||
uib-dropdown-toggle uib-tooltip="{{ 'Add a tag' | translate }}"
|
||||
tooltip-class="nobr">
|
||||
<span ng-if="assignment.tags.length == 0" ng-show="assignment.hover">
|
||||
<i class="fa fa-tags"></i>
|
||||
<i class="fa fa-plus"></i>
|
||||
</span>
|
||||
<span ng-if="assignment.tags.length > 0">
|
||||
<i class="fa fa-tags"></i>
|
||||
<span ng-repeat="tag in assignment.tags">
|
||||
{{ tag.name }}<span ng-if="!$last">,</span>
|
||||
</span>
|
||||
<i class="fa fa-cog fa-lg spacer-left" ng-show="assignment.tagHover"></i>
|
||||
</span>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownTags{{ assignment.id }}">
|
||||
<li ng-repeat="tag in tags">
|
||||
<a href ng-click="toggleTag(assignment, tag)">
|
||||
<i class="fa fa-check" ng-if="hasTag(assignment, tag)"></i>
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Tag string for normal user -->
|
||||
<div os-perms="!assignments.can_manage" ng-show="assignment.tags.length > 0">
|
||||
<i class="fa fa-tags spacer-right"></i>
|
||||
<span ng-repeat="tag in assignment.tags">
|
||||
{{ tag.name }}<span ng-if="!$last">,</span>
|
||||
</span>
|
||||
</div>
|
||||
<template-hook hook-name="assignmentListAdditionalContentColumn"></template-hook>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="width: 20%;" class="pull-right nobr optional centered">
|
||||
<span class="badge"
|
||||
uib-tooltip="{{ assignment.assignment_related_users.length }} {{ 'Candidates' | translate }}"
|
||||
tooltip-class="nobr"
|
||||
ng-class="{'badge-info': assignment.assignment_related_users.length < assignment.open_posts}">
|
||||
{{ assignment.assignment_related_users.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="width: 20%;" class="pull-right nobr">
|
||||
<div class="centered">{{ assignment.agenda_item.getItemNumberWithAncestors() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- main table -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="assignmentsFiltered.length > pagination.itemsPerPage"
|
||||
total-items="assignmentsFiltered.length"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
first-text="«"
|
||||
last-text="»">
|
||||
</ul>
|
||||
|
||||
</div> <!-- end container -->
|
||||
</div>
|
@ -1,22 +0,0 @@
|
||||
<h1><translate>Ballot</translate> {{ ballot }}</h1>
|
||||
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<translate>Special values</translate>:
|
||||
<span class="badge badge-success">-1</span> = <translate>majority</translate>
|
||||
<span class="badge">-2</span> = <translate>undocumented</translate>
|
||||
|
||||
<form name="assignmentPollForm" ng-submit="save(model)">
|
||||
<formly-form model="model" fields="formFields">
|
||||
<template-hook hook-name="assignmentPollFormButtons"></template-hook>
|
||||
<button type="submit" ng-disabled="assignmentPollForm.$invalid" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</formly-form>
|
||||
</form>
|
@ -1,101 +0,0 @@
|
||||
<div ng-controller="SlideAssignmentCtrl" class="content scrollcontent">
|
||||
|
||||
<!-- Title -->
|
||||
<div id="title">
|
||||
<h1>{{ assignment.agenda_item.getTitle() || assignment.title }}</h1>
|
||||
<h2>
|
||||
<translate>Election</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div ng-if="!showResult && assignment.description !== ''"
|
||||
ng-bind-html="assignment.description | trusted"
|
||||
class="zoomcontent clearfix">
|
||||
</div>
|
||||
|
||||
<!-- Candidates -->
|
||||
<div class="zoomcontent" ng-if="!showResult">
|
||||
<h3 translate>Candidates</h3>
|
||||
<ul>
|
||||
<li ng-repeat="related_user in assignment.assignment_related_users | orderBy:'weight'">
|
||||
{{ related_user.user.get_full_name() }}
|
||||
<i ng-if="related_user.elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- vote results -->
|
||||
<div class="zoomcontent" ng-show="showResult">
|
||||
<div ng-if="(assignment.polls | filter: {id: element.poll}).length == 0" translate>
|
||||
Waiting for results ...
|
||||
</div>
|
||||
|
||||
<div ng-repeat="poll in assignment.polls | filter: {id: element.poll}" class="electionresults spacer">
|
||||
<table class="table table-bordered table-striped minimumTable">
|
||||
<tr>
|
||||
<th translate>Candidates
|
||||
<th ng-if="poll.has_votes" class="col-sm-5" translate>Votes</th>
|
||||
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
|
||||
<!-- candidate name -->
|
||||
<td class="bold">
|
||||
<i ng-if="option.is_elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
{{ option.candidate.get_full_name() }}
|
||||
|
||||
<!-- votes -->
|
||||
<td ng-if="poll.has_votes" class="bold">
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
|
||||
<span ng-show="poll.pollmethod == 'yna'">
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}<br>
|
||||
{{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }}</span>
|
||||
<span ng-show="poll.pollmethod == 'yn'">
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}</span>
|
||||
</div>
|
||||
<div ng-show="poll.pollmethod == 'votes'">
|
||||
<div ng-repeat="vote in votes">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<div style="float:right; width:200px;" ng-if="vote.percentNumber >= 0">
|
||||
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- total votes (abstain/no/valid/invalid/casts) -->
|
||||
<tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesabstain').value !== null">
|
||||
<td>
|
||||
<translate>Abstain</translate>
|
||||
<td ng-init="vote = poll.getVote('votesabstain')">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesno').value !== null">
|
||||
<td>
|
||||
<translate>No</translate>
|
||||
<td ng-init="vote = poll.getVote('votesno')">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null">
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votesvalid')">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
|
||||
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesinvalid').value !== null">
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votesinvalid')">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
|
||||
<tr class="total bg-info" ng-if="poll.has_votes && poll.getVote('votescast').value !== null">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votescast')">
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,66 +0,0 @@
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.management.commands.collectstatic import (
|
||||
Command as CollectStatic,
|
||||
)
|
||||
from django.contrib.staticfiles.utils import matches_patterns
|
||||
from django.core.management.base import CommandError
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
from ...views import WebclientJavaScriptView
|
||||
|
||||
|
||||
class Command(CollectStatic):
|
||||
"""
|
||||
Custom collectstatic command.
|
||||
"""
|
||||
realms = ['site', 'projector']
|
||||
js_filename = 'webclient-{}.js'
|
||||
|
||||
def handle(self, **options: Any) -> str:
|
||||
if options['link']:
|
||||
raise CommandError("Option 'link' is not supported.")
|
||||
try:
|
||||
self.view = WebclientJavaScriptView()
|
||||
except OperationalError:
|
||||
raise CommandError('You have to run OpenSlides first to create a ' +
|
||||
'database before collecting staticfiles.')
|
||||
return super().handle(**options)
|
||||
|
||||
def collect(self) -> Dict[str, Any]:
|
||||
result = super().collect()
|
||||
|
||||
try:
|
||||
destination_dir = os.path.join(settings.STATIC_ROOT, 'js')
|
||||
except IndexError:
|
||||
# If the user does not want do have staticfiles, he should not get
|
||||
# the webclient files either.
|
||||
pass
|
||||
else:
|
||||
if self.dry_run:
|
||||
self.log('Pretending to write WebclientJavaScriptView for all realms.', level=1)
|
||||
else:
|
||||
if not os.path.exists(destination_dir):
|
||||
os.makedirs(destination_dir)
|
||||
|
||||
for realm in self.realms:
|
||||
filename = self.js_filename.format(realm)
|
||||
# Matches only the basename.
|
||||
if matches_patterns(filename, self.ignore_patterns):
|
||||
continue
|
||||
path = os.path.join(destination_dir, filename)
|
||||
if matches_patterns(path, self.ignore_patterns):
|
||||
continue
|
||||
|
||||
content = self.view.get(realm=realm).content
|
||||
with open(path, 'wb+') as f:
|
||||
f.write(content)
|
||||
message = "Written WebclientJavaScriptView for realm {} to '{}'".format(
|
||||
realm,
|
||||
path)
|
||||
self.log(message, level=1)
|
||||
result['modified'].append(path)
|
||||
|
||||
return result
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Font definitions for OpenSlides site
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: $font;
|
||||
src: $font-src;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: $font-medium;
|
||||
src: $font-medium-src;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: $font-condensed;
|
||||
src: $font-condensed-src;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: $font-condensed-light;
|
||||
src: $font-condensed-light-src;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
/** General helper classes **/
|
||||
.disabled {
|
||||
color: #555;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-left: 0px !important;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.slimDropDown {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #317796;
|
||||
}
|
||||
|
||||
.btn-slim {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.spacer, .spacer-top {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.spacer-top-lg {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.spacer-bottom {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.spacer-right {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.spacer-left {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.spacer-left-lg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.lead-div {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.lead {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hoverActions {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.hiddenDiv {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.vcenter {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.white-space-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.slimlabel.label {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
.indentation, .indentation20 {
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
|
||||
.indentation-lg {
|
||||
margin-left: 35px !important;
|
||||
}
|
||||
|
||||
.halfWidth {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #5bb85b;
|
||||
}
|
||||
|
||||
.smallhr {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
.nobr {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rotate-45-deg-right {
|
||||
filter: "progid: DXImageTransform.Microsoft.BasicImage(rotation=0.5)";
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.optional { /* show optional column */
|
||||
display: auto;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.loginForm .input-group-addon i {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-padding-left {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.no-padding-right {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.scroll-x-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.btn-group > label > input[type=radio] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* tables */
|
||||
.topictext table {
|
||||
border-spacing: 10px 0;
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
.minimum {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.minimumTable {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.deleteColumn {
|
||||
text-align: center;
|
||||
background-color: #ff9999 !important;
|
||||
}
|
||||
|
||||
th.sortable:hover, tr.pointer:hover, .pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/** background colors for table rows **/
|
||||
tr.hiddenrow td {
|
||||
background-color: #F5DCDC;
|
||||
}
|
||||
|
||||
tr.activeline td, li.activeline, .projected {
|
||||
background-color: #bed4de !important;
|
||||
}
|
||||
|
||||
.related-projected {
|
||||
background-color: #e1ecfe !important;
|
||||
}
|
||||
|
||||
tr.selected td {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
font-size: 85%;
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.pagination-arrow {
|
||||
font-size: 150%;
|
||||
padding: 3px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #222;
|
||||
background-color: #bed4de;
|
||||
border-color: #46b8da;
|
||||
}
|
||||
.error {
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color: #ebccd1;
|
||||
}
|
||||
.success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
.warning {
|
||||
color: #725523;
|
||||
background-color: #fcf8e3;
|
||||
border-color: #faebcc;
|
||||
}
|
||||
.close {
|
||||
opacity: 0.3 !important;
|
||||
}
|
||||
.close:hover {
|
||||
opacity: 0.6 !important;
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/*////////////////////////////////////////
|
||||
=MEDIA QUERIES (RESPONSIVE DESIGN)
|
||||
////////////////////////////////////////*/
|
||||
|
||||
/* display for 900 to 1200px resolutions */
|
||||
@media only screen and (max-width: 1200px) {
|
||||
|
||||
body { font-size: 12px; }
|
||||
|
||||
h1 { font-size: 30px; padding-bottom: 6px; }
|
||||
|
||||
h2 { font-size: 22px; padding-bottom: 10px; }
|
||||
|
||||
h3 { font-size: 18px; padding-bottom: 10px; }
|
||||
|
||||
.col-md-4 {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.motion-text.line-numbers-outside .os-line-number:after {
|
||||
font-size: 10px;
|
||||
top: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* display for resolutions smaller that 900px */
|
||||
@media only screen and (max-width: 900px) {
|
||||
|
||||
#nav .navbar li a { padding: 24px 5px; }
|
||||
|
||||
#multi-table .info-head {
|
||||
width: 200px;
|
||||
&.small {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/* hide text in multi-table earlier */
|
||||
#multi-table .optional { display: none; }
|
||||
|
||||
/* show replacement elements, if any */
|
||||
#multi-table .optional-show { display: block !important; }
|
||||
|
||||
/* hide searchbar input */
|
||||
#nav .searchbar input { display: none !important; }
|
||||
#nav .searchbar .btn {
|
||||
border-top-left-radius: 4px !important;
|
||||
border-bottom-left-radius: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* display for resolutions smaller that 760px */
|
||||
@media only screen and (max-width: 760px) {
|
||||
|
||||
h1 { font-size: 22px; padding-bottom: 4px; }
|
||||
|
||||
h2 { font-size: 16px; padding-bottom: 4px; }
|
||||
|
||||
h3 { font-size: 14px; padding-bottom: 4px; }
|
||||
|
||||
.user i { font-size: 16px; padding: 3px; }
|
||||
|
||||
#nav .navbar { box-shadow: none; padding-right: 60px !important; }
|
||||
#nav .navbar ul li a { padding: 10px 15px; }
|
||||
#nav .searchbar { margin: 15px -53px 0 0 !important; }
|
||||
|
||||
#chatbox { width: 100%; top: 40px; }
|
||||
|
||||
.badge { font-size: 10px; }
|
||||
|
||||
/* show replacement elements, if any */
|
||||
.optional-show { display: block !important; }
|
||||
|
||||
/* hide marked element / column */
|
||||
.optional, .hide-sm { display: none !important; }
|
||||
}
|
||||
|
||||
/* display for resolutions smaller that 560px */
|
||||
@media only screen and (max-width: 560px) {
|
||||
#content .containerOS { padding: 0; }
|
||||
|
||||
#content .col2 { width: 100%; }
|
||||
|
||||
#content .col2.sidebar-max, #content .col2.sidebar-min,
|
||||
#content .col1.sidebar-min, #content .col1.sidebar-max {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#sidebar-xs {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
#sidebar-xs .projector_full {
|
||||
margin-left: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#multi-table .info-head {
|
||||
width: 150px;
|
||||
&.small {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.personalNoteFixed {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
#wrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
|
||||
#startup-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
#spinner-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 340px;
|
||||
z-index: 1000;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 56px;
|
||||
text-align: center;
|
||||
color: #317796;
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
/* override booststrap's label class to fix linebreak and add spacing */
|
||||
.label {
|
||||
display: inline-block;
|
||||
padding: .4em .6em;
|
||||
margin-right: .2em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Custom OpenSlides slider classes */
|
||||
.os-slider {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.rzslider {
|
||||
.rz-bar {
|
||||
background: #317796;
|
||||
opacity: 0.3;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.rz-selection {
|
||||
background: #317796;
|
||||
}
|
||||
|
||||
.rz-pointer {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
top: -7px;
|
||||
bottom: 0;
|
||||
background-color: #317796;
|
||||
border-radius: 7px;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ngDialog: override ngdialog-theme-default */
|
||||
.ngdialog.ngdialog-theme-default {
|
||||
padding-top: 40px;
|
||||
|
||||
.ngdialog-content {
|
||||
font-family: $font, sans-serif !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: $font-medium;
|
||||
font-family: $font-medium, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.row:after, .row:before {
|
||||
display: table-cell !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ngdialog.ngdialog-theme-default.wide-form .ngdialog-content {
|
||||
width: 750px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
/* Bootbox: override z-index, that bootboxes are higher than ngDialogs */
|
||||
.bootbox {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
/* angular-chosen: override default width of select fields in quickmode */
|
||||
.chosen-container {
|
||||
width: 100% !important;
|
||||
font-size: 14px;
|
||||
|
||||
ul.chosen-results {
|
||||
margin: 0 !important;
|
||||
|
||||
li.active-result {
|
||||
margin: 6px 5px 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container-active .chosen-choices {
|
||||
border-color: rgba(82,168,236,.8);
|
||||
box-shadow: 0 0 8px rgba(82,168,236,.6);
|
||||
}
|
||||
|
||||
/* angular-ui-tree style */
|
||||
.angular-ui-tree-handle {
|
||||
background: none repeat scroll 0 0 #f8faff;
|
||||
border: 1px solid #dae2ea;
|
||||
color: #7c9eb2;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
background: none repeat scroll 0 0 #f4f6f7;
|
||||
border-color: #dce2e8;
|
||||
color: #438eb9;
|
||||
}
|
||||
}
|
||||
|
||||
.angular-ui-tree-placeholder {
|
||||
background: none repeat scroll 0 0 #f0f9ff;
|
||||
border: 2px dashed #bed2db;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ngAnimate classes */
|
||||
.animate-item.ng-enter {
|
||||
-webkit-animation: fade-in 0.5s linear;
|
||||
animation: fade-in 0.5s linear;
|
||||
}
|
||||
|
||||
.animate-item.ng-leave {
|
||||
-webkit-animation: fade-out 0.25s linear;
|
||||
animation: fade-out 0.25s linear;
|
||||
}
|
||||
|
||||
/* xeditable */
|
||||
.editable-click {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #555;
|
||||
|
||||
&:hover {
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-wrapper {
|
||||
.editable-hide {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.small-form {
|
||||
input{
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
button{
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; background: none; }
|
||||
100% { opacity: 0; background: none; }
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; background: none; }
|
||||
100% { opacity: 1; background: none; }
|
||||
}
|
||||
|
||||
/* Angular formly */
|
||||
.checkbox label {
|
||||
padding-left: 0;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
/** Fonts **/
|
||||
/* Note: The font naming has to be consistent to the projector.html */
|
||||
$font: 'OSFont';
|
||||
$font-src: url('../fonts/Roboto-Regular.woff') format('woff');
|
||||
$font-medium: 'OSFont Medium';
|
||||
$font-medium-src: url('../fonts/Roboto-Medium.woff') format('woff');
|
||||
$font-condensed: 'OSFont Condensed';
|
||||
$font-condensed-src: url('../fonts/Roboto-Condensed-Regular.woff') format('woff');
|
||||
$font-condensed-light: 'OSFont Condensed Light';
|
||||
$font-condensed-light-src: url('../fonts/Roboto-Condensed-Light.woff') format('woff');
|
@ -1,19 +0,0 @@
|
||||
/* Chatbox */
|
||||
#chatbox {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
width: 40%;
|
||||
border-color: #dddddd;
|
||||
border-width: 1px;
|
||||
box-shadow: -5px 5px 5px rgba(0, 0, 0, 0.2);
|
||||
height: 234px;
|
||||
padding: 0 10px 10px 10px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#chatbox-text {
|
||||
overflow-y: scroll;
|
||||
height: 190px;
|
||||
color: #222;
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/** Config **/
|
||||
#content .col1 #config {
|
||||
.comments > div {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.config-checkbox {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
.custom-translations {
|
||||
> div {
|
||||
margin-bottom: 5px;
|
||||
padding-right: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
input {
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 6%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/** Countdown fullscreen mode **/
|
||||
#countdownWrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
background-color: white;
|
||||
}
|
||||
#countdownWrapper > div {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
#countdown {
|
||||
display: inline-block;
|
||||
padding: 20px 50px;
|
||||
border-radius: 8px;
|
||||
font-size: 30vw;
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
#countdown .description {
|
||||
font-weight: normal;
|
||||
font-size: 5vw;
|
||||
}
|
||||
.warning_time {
|
||||
color: #ed940d;
|
||||
}
|
||||
.negative {
|
||||
color: #CC0000;
|
||||
}
|
28
openslides/core/static/css/core/_csv-import.scss
vendored
@ -1,28 +0,0 @@
|
||||
/* for csv import form */
|
||||
#content .col1 #csv-import {
|
||||
margin-left: 15px;
|
||||
width: 35%;
|
||||
|
||||
.file-select {
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-file {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.help-block-big {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/** Goto top link **/
|
||||
#goto-top {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
right: 30px;
|
||||
padding: 10px 30px;
|
||||
background: white;
|
||||
opacity: 0.8;
|
||||
transition: opacity 250ms ease-out;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/** manage-projectors **/
|
||||
#manage-projectors {
|
||||
> div {
|
||||
display: inline-table;
|
||||
width: 256px;
|
||||
margin: 10px 20px 35px 10px;
|
||||
|
||||
> div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.middle {
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resolution {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/** Messaging status bar **/
|
||||
#messaging {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
#messaging-container {
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 0px 20px;
|
||||
max-width: 1400px;
|
||||
|
||||
> div {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/* multi list */
|
||||
#multi-table {
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
|
||||
thead tr th {
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-head {
|
||||
width: 300px;
|
||||
&.small {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
tbody tr.bg-grey {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
tbody tr td .no-overflow{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody tr td div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.optional-show { /* hide optional-show column */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editable-click {
|
||||
color: #000;
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/** OS-Table **/
|
||||
#content .col1 .os-table {
|
||||
font-weight: normal;
|
||||
|
||||
small {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.row {
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0px;
|
||||
|
||||
.col-xs-1 {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.col-xs-4 {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.col-space {
|
||||
padding: 5px 7px 5px 7px;
|
||||
}
|
||||
|
||||
// TODO: Isn't this defined in the _helper.scss?
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.data-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.data-row > div {
|
||||
padding: 10px 0px 10px 0px;
|
||||
}
|
||||
|
||||
.id-col {
|
||||
width: 50px;
|
||||
min-height: 1px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.id-col-space {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.no-projector-spacer {
|
||||
margin-right: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header-row > div {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
width: calc(100% - 50px);
|
||||
float: right;
|
||||
|
||||
.form-inline {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.content > div { /* horizontal blocks */
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content > div > div { /* vertival blocks */
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.content > div > div:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.projector {
|
||||
width: 70px !important;
|
||||
}
|
||||
|
||||
|
||||
.header-row .dropdown > span, .sort-spacer {
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: $font-medium;
|
||||
font-family: $font-medium, sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 120%;
|
||||
margin-right: 3px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dropdown-hover-space {
|
||||
padding: 5px 5px 5px 0;
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/* ProjectorContainer */
|
||||
.pContainer {
|
||||
background-color: #222;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: table;
|
||||
|
||||
& > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#iframe {
|
||||
-moz-transform-origin: 0 0;
|
||||
-webkit-transform-origin: 0 0;
|
||||
-o-transform-origin: 0 0;
|
||||
transform-origin: 0 0 0;
|
||||
}
|
||||
|
||||
#iframewrapper, .error {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.error > p {
|
||||
color: #f00;
|
||||
font-size: 150%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#iframeoverlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/* Pojector sidebar */
|
||||
.col2 .projectorSelector {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
& > div {
|
||||
margin-bottom: 10px;
|
||||
|
||||
& > div {
|
||||
width: 65%;
|
||||
padding-right: 5px;
|
||||
float: left;
|
||||
margin-bottom: 10px;
|
||||
|
||||
& > button {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manageBtn {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: table;
|
||||
}
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
@import "projector-container";
|
||||
|
||||
/* Template styles for OpenSlides projector */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $font, sans-serif;
|
||||
font-size: 20px !important;
|
||||
line-height: 24px !important;
|
||||
overflow: hidden;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-family: $font-condensed, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25em;
|
||||
line-height: 1.1em;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
margin-top: 15px;
|
||||
display: block;
|
||||
color: #9a9898;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #222;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
font-family: $font-condensed-light;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 0 7px rgba(0,0,0,0.6);
|
||||
height: 70px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#logo {
|
||||
position: relative;
|
||||
left: 50px;
|
||||
top: 10px;
|
||||
height: 50px;
|
||||
margin-right: 25px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#eventdata {
|
||||
position: relative;
|
||||
left: 50px;
|
||||
top: 12px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
|
||||
&.titleonly {
|
||||
position: relative;
|
||||
top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
#currentTime {
|
||||
font-family: $font-condensed-light;
|
||||
border: 0 solid #000000;
|
||||
font-size: 24px;
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
top: 23px;
|
||||
right: 50px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
font-family: $font-condensed-light;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 35px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
padding-top: 5px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
z-index: -1;
|
||||
line-height: 1.5em;
|
||||
|
||||
img {
|
||||
max-width: 65%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollcontent {
|
||||
transition-property: margin;
|
||||
transition-duration: 1s;
|
||||
}
|
||||
|
||||
.zoomcontent {
|
||||
transition-property: font-size;
|
||||
transition-duration: 1s;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0 !important;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: black;
|
||||
|
||||
canvas {
|
||||
margin: auto;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
border-bottom: 5px solid #d3d3d3;
|
||||
margin-bottom: 40px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Overlays: countdown and message */
|
||||
.countdown {
|
||||
min-width: 260px;
|
||||
position: relative;
|
||||
margin: 0 0 10px 10px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
padding: 26px 45px 7px 19px;
|
||||
min-height: 72px;
|
||||
font-family: $font-condensed, sans-serif;
|
||||
font-size: 3.7em;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
border-radius: 7px 7px 7px 7px;
|
||||
z-index: 320;
|
||||
|
||||
.description {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin-top: 20px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.message_background {
|
||||
background-color: #777777;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 300;
|
||||
}
|
||||
.message {
|
||||
font-family: $font-condensed, sans-serif;
|
||||
font-weight: normal;
|
||||
position: fixed;
|
||||
top: 35%;
|
||||
left: 10%;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border-radius: 0.2em;
|
||||
background: #FFFFFF;
|
||||
font-size: 2.75em;
|
||||
padding: 0.2em 0;
|
||||
line-height: normal !important;
|
||||
z-index: 350;
|
||||
}
|
||||
.identify {
|
||||
background-color: #D9F8C4;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
/* Table style */
|
||||
.table {
|
||||
border-collapse:collapse;
|
||||
border-color:#CCCCCC -moz-use-text-color #CCCCCC #CCCCCC;
|
||||
border-style:solid none solid solid;
|
||||
border-width:1px medium 1px 1px;
|
||||
margin:0;
|
||||
border-spacing:0px;
|
||||
|
||||
th {
|
||||
border-right:1px solid #CCCCCC;
|
||||
color:#333333;
|
||||
font-weight:normal;
|
||||
padding:10px 10px 10px 10px;
|
||||
text-align:left;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
|
||||
tr.odd td {
|
||||
background:none repeat scroll 0 0 #F1F1F1;
|
||||
}
|
||||
|
||||
td {
|
||||
background:none repeat scroll 0 0 #F7F7F7;
|
||||
border-right:1px solid #CCCCCC;
|
||||
line-height:120%;
|
||||
padding: 10px 10px;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
tr.total td {
|
||||
border-top: 1px solid #333333;
|
||||
background-color: #e3e3e3;
|
||||
}
|
||||
|
||||
tr.elected td {
|
||||
background-color: #BED4DE !important;
|
||||
}
|
||||
}
|
||||
|
||||
.transparentTable td {
|
||||
background-color: transparent;
|
||||
padding-right: 10px;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
/* search results */
|
||||
.searchresults li {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.searchresults h3 {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.searchresults .hits {
|
||||
margin-bottom: 10px;
|
||||
color: #999999;
|
||||
font-size: 85%;
|
||||
}
|
@ -1,612 +0,0 @@
|
||||
/** Template styles for OpenSlides site **/
|
||||
|
||||
@import "config";
|
||||
@import "search";
|
||||
@import "os-table";
|
||||
@import "multi-table";
|
||||
@import "csv-import";
|
||||
@import "chatbox";
|
||||
@import "countdown";
|
||||
@import "goto-top";
|
||||
@import "messaging";
|
||||
|
||||
@import "projector-sidebar";
|
||||
@import "manage-projectors";
|
||||
|
||||
* { /* set margin/padding for all (block) elements to null */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
background: #e8eaed;
|
||||
font-family: $font, sans-serif !important;
|
||||
}
|
||||
|
||||
div {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #2b6883;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.1;
|
||||
font-family: $font-condensed, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
color: #317796;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
color: #9a9898;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 22px;
|
||||
color: #222;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
strong, b, th {
|
||||
font-family: $font-medium;
|
||||
font-family: $font-medium, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.heading {
|
||||
a {
|
||||
color: #444;
|
||||
|
||||
&:hover {
|
||||
color: #2b6883;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Header **/
|
||||
#header {
|
||||
float: left;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #002c42;
|
||||
color: #999;
|
||||
|
||||
a.headerlink {
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a.headerlink:hover, a.headerlink:active, a.headerlink.active {
|
||||
color: #e8eaed;
|
||||
}
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
margin: 2px 0 0 -5px;
|
||||
color: #fff;
|
||||
font-family: $font-condensed, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.user {
|
||||
float: right;
|
||||
margin: 5px 0 0 0;
|
||||
height: 100%;
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nav {
|
||||
float: left;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: #317796;
|
||||
color: #fff;
|
||||
overflow-y: hidden;
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
padding: 5px 0;
|
||||
z-index: 2;
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: #fff;
|
||||
padding: 22px 15px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background: url('/static/img/nav_dark-bg.png');
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
li.active {
|
||||
background: url('/static/img/nav_dark-bg.png');
|
||||
|
||||
a {
|
||||
background: url('/static/img/nav_active.png') no-repeat bottom;
|
||||
}
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchbar {
|
||||
float: right;
|
||||
margin-top: 33px;
|
||||
margin-bottom: 32px;
|
||||
display: inline-table;
|
||||
|
||||
input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #e8eaed;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#header .containerOS, #nav .containerOS, #content .containerOS {
|
||||
max-width: 1300px;
|
||||
height: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
#content .containerOSExpanded {
|
||||
height: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/** Content **/
|
||||
#content {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
|
||||
.containerOS {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Main column **/
|
||||
#content .col1 {
|
||||
float: left;
|
||||
|
||||
&.sidebar-max { /*with maximized sidebar*/
|
||||
width: calc(100% - 325px);
|
||||
}
|
||||
|
||||
&.sidebar-min { /*with minimized sidebar*/
|
||||
width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
&.sidebar-none { /*without sidebar*/
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
border: 1px solid #d3d3d3;
|
||||
|
||||
.submenu {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.meta {
|
||||
.heading, h3 {
|
||||
font-family: $font-condensed-light;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
font-weight: 300;
|
||||
color: #444;
|
||||
padding-bottom: 0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
color: #fff;
|
||||
background: #317796;
|
||||
padding: 5px 20px 0 20px;
|
||||
|
||||
&:hover {
|
||||
color: #d3d3d3;
|
||||
}
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 10px 20px;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
background: #d3d3d3;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.heading .drop-down-name {
|
||||
font-family: $font-condensed-light;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 20px;
|
||||
width: auto;
|
||||
margin-top: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #d3d3d3;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Projector sidebar column **/
|
||||
#sidebar-xs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#content .col2 {
|
||||
float: right;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
z-index: 3;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.sidebar-max {
|
||||
width: 325px;
|
||||
}
|
||||
|
||||
&.sidebar-min {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
&.sidebar-none {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #444;
|
||||
font-family: $font-condensed-light;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
a:hover h4 {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.projector_min {
|
||||
background: url('/static/img/nav_projector_sidebar_min.png') no-repeat left 17px;
|
||||
width: 50px;
|
||||
margin-left: 20px;
|
||||
padding-left: 8px;
|
||||
float: right;
|
||||
|
||||
.icon {
|
||||
float: left;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
padding: 7px 0;
|
||||
background: #317796;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
display: block;
|
||||
|
||||
i {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.projector_full {
|
||||
margin-left: 30px;
|
||||
width: auto;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
height: 50px;
|
||||
background: #317796;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #d3d3d3;
|
||||
}
|
||||
|
||||
a, a:hover {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
padding: 8px 0 0 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
float: right;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
padding-top: 7px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.details {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
background: #d3d3d3;
|
||||
|
||||
.section {
|
||||
padding: 1px 20px;
|
||||
width: auto;
|
||||
border-bottom: 1px solid #c2c2c2;
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.in.collapse {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 20px;
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* countdown and message controls */
|
||||
.countdown {
|
||||
&.panel {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.icons {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
&.panel {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
projector-button {
|
||||
float: left;
|
||||
width: auto;
|
||||
margin: 5px 10px 5px 0px;
|
||||
}
|
||||
|
||||
.innermessage {
|
||||
float: left;
|
||||
width: 180px;
|
||||
max-width: 170px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-input {
|
||||
width: 228px;
|
||||
float: left;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.editicon {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown_timer {
|
||||
font-size: 2.2em;
|
||||
font-weight: bold;
|
||||
|
||||
&.warning {
|
||||
color: #ed940d;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #CC0000;
|
||||
}
|
||||
}
|
||||
|
||||
.notNull {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* iframe for live view */
|
||||
.iframe {
|
||||
-moz-transform-origin: 0 0;
|
||||
-webkit-transform-origin: 0 0;
|
||||
-o-transform-origin: 0 0;
|
||||
transform-origin: 0 0 0;
|
||||
}
|
||||
|
||||
.iframewrapper {
|
||||
width: 258px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid #D5D5D5;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.iframeoverlay {
|
||||
width: 256px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/** Footer **/
|
||||
#footer {
|
||||
float: left;
|
||||
height: 50px;
|
||||
padding-top: 15px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.details h1 {
|
||||
font-size: 20px;
|
||||
color: #000;
|
||||
}
|
||||
.details h2 {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
}
|
||||
.details h3 {
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/** Config **/
|
||||
#config .panel-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
#config.details h3 {
|
||||
font-size: 22px;
|
||||
padding-top: 30px;
|
||||
margin-top: 0;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/* Main projector CSS file. Just import all files needed for the site here. */
|
||||
|
||||
/* General */
|
||||
@import "variables";
|
||||
@import "helper";
|
||||
@import "ui-override";
|
||||
@import "core/countdown";
|
||||
|
||||
/* Apps */
|
||||
@import "core/projector";
|
||||
@import "../../../agenda/static/css/agenda/projector";
|
||||
@import "../../../assignments/static/css/assignments/projector";
|
||||
@import "../../../motions/static/css/motions/projector";
|
||||
@import "../../../mediafiles/static/css/mediafiles/projector";
|
@ -1,17 +0,0 @@
|
||||
/* Main site CSS file. Just import all files needed for the site here. */
|
||||
|
||||
/* General */
|
||||
@import "variables"; // TODO: Add colors, ...
|
||||
@import "fonts";
|
||||
@import "helper";
|
||||
@import "mediaqueries";
|
||||
@import "ui-override";
|
||||
@import "startup-spinner";
|
||||
|
||||
/* Apps */
|
||||
@import "core/site";
|
||||
@import "../../../agenda/static/css/agenda/site";
|
||||
@import "../../../assignments/static/css/assignments/site";
|
||||
@import "../../../motions/static/css/motions/site";
|
||||
@import "../../../users/static/css/users/site";
|
||||
@import "../../../mediafiles/static/css/mediafiles/site";
|
Before Width: | Height: | Size: 581 B |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 925 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 6.0 KiB |
3
openslides/core/static/index.html
Normal file
@ -0,0 +1,3 @@
|
||||
<h1>Hello user!</h1>
|
||||
|
||||
Use <a href="http://localhost:4200">This url</a> to get to the client. For further information see the DEVELOPMENT.rst
|
@ -1,23 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.core.csv', [])
|
||||
|
||||
.factory('CsvDownload', [
|
||||
'Config',
|
||||
'FileSaver',
|
||||
function (Config, FileSaver) {
|
||||
var utf8_BOM = decodeURIComponent('%EF%BB%BF');
|
||||
return function (contentRows, filename) {
|
||||
var separator = Config.get('general_csv_separator').value;
|
||||
var rows = _.map(contentRows, function (row) {
|
||||
return row.join(separator);
|
||||
});
|
||||
var blob = new Blob([utf8_BOM + rows.join('\n')]);
|
||||
FileSaver.saveAs(blob, filename);
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,356 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.core.docx', [])
|
||||
|
||||
.factory('Html2DocxConverter', [
|
||||
'$q',
|
||||
'ImageConverter',
|
||||
function ($q, ImageConverter) {
|
||||
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
|
||||
|
||||
var createInstance = function () {
|
||||
var converter = {
|
||||
imageMap: {},
|
||||
documentImages: [],
|
||||
relationships: [],
|
||||
contentTypes: [],
|
||||
};
|
||||
|
||||
var html2docx = function (html) {
|
||||
var docx = '';
|
||||
var tagStack = [];
|
||||
|
||||
// With this variable, we keep track, if we are currently inside or outside of a paragraph.
|
||||
var inParagraph = true;
|
||||
// the text may not begin with a paragraph. If so, append one because word needs it.
|
||||
var skipFirstParagraphClosing = true;
|
||||
|
||||
var handleTag = function (tag) {
|
||||
if (tag.charAt(0) == "/") { // A closing tag
|
||||
// remove from stack
|
||||
tagStack.pop();
|
||||
|
||||
// Special: end paragraphs
|
||||
if (tag.indexOf('/p') === 0) {
|
||||
docx += '</w:p>';
|
||||
inParagraph = false;
|
||||
}
|
||||
} else { // now all other tags
|
||||
var tagname = tag.split(' ')[0];
|
||||
handleNamedTag(tagname, tag);
|
||||
}
|
||||
return docx;
|
||||
};
|
||||
var handleNamedTag = function (tagname, fullTag) {
|
||||
var tag = {
|
||||
tag: tagname,
|
||||
attrs: {},
|
||||
};
|
||||
switch (tagname) {
|
||||
case 'p':
|
||||
if (inParagraph && !skipFirstParagraphClosing) {
|
||||
// End the paragrapth, if there is one
|
||||
docx += '</w:p>';
|
||||
}
|
||||
skipFirstParagraphClosing = false;
|
||||
docx += '<w:p>';
|
||||
inParagraph = true;
|
||||
break;
|
||||
case 'span':
|
||||
var styleRegex = /(?:\"|\;\s?)([a-zA-z\-]+)\:\s?([a-zA-Z0-9\-\#]+)/g, matchSpan;
|
||||
while ((matchSpan = styleRegex.exec(fullTag)) !== null) {
|
||||
switch (matchSpan[1]) {
|
||||
case 'color':
|
||||
tag.attrs.color = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'background-color':
|
||||
tag.attrs.backgroundColor = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'text-decoration':
|
||||
if (matchSpan[2] === 'underline') {
|
||||
tag.attrs.underline = true;
|
||||
} else if (matchSpan[2] === 'line-through') {
|
||||
tag.attrs.strike = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'a':
|
||||
var hrefRegex = /href="([^"]+)"/g;
|
||||
var href = hrefRegex.exec(fullTag)[1];
|
||||
tag.href = href;
|
||||
break;
|
||||
case 'img':
|
||||
imageTag(tag, fullTag);
|
||||
break;
|
||||
}
|
||||
if (tagname !== 'img' && tagname !== 'p') {
|
||||
tagStack.push(tag);
|
||||
}
|
||||
};
|
||||
var imageTag = function (tag, fullTag) {
|
||||
// images has to be placed instantly, so there is no use of 'tag'.
|
||||
var image = {};
|
||||
var attributeRegex = /(\w+)=\"([^\"]*)\"/g, attributeMatch;
|
||||
while ((attributeMatch = attributeRegex.exec(fullTag)) !== null) {
|
||||
image[attributeMatch[1]] = attributeMatch[2];
|
||||
}
|
||||
if (image.src && converter.imageMap[image.src]) {
|
||||
image.width = converter.imageMap[image.src].width;
|
||||
image.height = converter.imageMap[image.src].height;
|
||||
|
||||
var rrId = converter.relationships.length + 1;
|
||||
var imageId = converter.documentImages.length + 1;
|
||||
|
||||
// set name ('pic.jpg'), title, ext ('jpg'), mime ('image/jpeg')
|
||||
image.name = _.last(image.src.split('/'));
|
||||
|
||||
var tmp = image.name.split('.');
|
||||
image.ext = tmp.splice(-1);
|
||||
|
||||
// set name without extension as title if there isn't a title
|
||||
if (!image.title) {
|
||||
image.title = tmp.join('.');
|
||||
}
|
||||
|
||||
image.mime = 'image/' + image.ext;
|
||||
if (image.ext == 'jpe' || image.ext == 'jpg') {
|
||||
image.mime = 'image/jpeg';
|
||||
}
|
||||
|
||||
// x and y for the container and picture size in EMU (assuming 96dpi)!
|
||||
var x = image.width * 914400 / 96;
|
||||
var y = image.height * 914400 / 96;
|
||||
|
||||
// the image does not belong into a paragraph in ooxml
|
||||
if (inParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
docx += '<w:p><w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extend cx="' + x +'" cy="' + y + '"/><wp:effectExtent l="0" t="0" r="0" b="0"/>' +
|
||||
'<wp:docPr id="' + imageId + '" name="' + image.name + '" title="' + image.title + '" descr="' + image.title + '"/><wp:cNvGraphicFramePr>' +
|
||||
'<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>' +
|
||||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">' +
|
||||
'<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="' + imageId + '" name="' +
|
||||
image.name + '" title="' + image.title + '" descr="' + image.title + '"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rrId' + rrId + '"/><a:stretch>' +
|
||||
'<a:fillRect/></a:stretch></pic:blipFill><pic:spPr bwMode="auto"><a:xfrm><a:off x="0" y="0"/><a:ext cx="' + x + '" cy="' + y + '"/></a:xfrm>' +
|
||||
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>';
|
||||
|
||||
// inParagraph stays untouched, the documents paragraph state is restored here
|
||||
if (inParagraph) {
|
||||
docx += '<w:p>';
|
||||
}
|
||||
|
||||
// entries in documentImages, relationships and contentTypes
|
||||
converter.documentImages.push({
|
||||
src: image.src,
|
||||
zipPath: 'word/media/' + image.name
|
||||
});
|
||||
converter.relationships.push({
|
||||
Id: 'rrId' + rrId,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
|
||||
Target: 'media/' + image.name
|
||||
});
|
||||
converter.contentTypes.push({
|
||||
PartName: '/word/media/' + image.name,
|
||||
ContentType: image.mime
|
||||
});
|
||||
}
|
||||
};
|
||||
var handleText = function (text) {
|
||||
// Start a new paragraph, if only loose text is there
|
||||
if (!inParagraph) {
|
||||
docx += '<w:p>';
|
||||
inParagraph = true;
|
||||
}
|
||||
var docxPart = '<w:r><w:rPr>';
|
||||
var hyperlink = false;
|
||||
tagStack.forEach(function (tag) {
|
||||
switch (tag.tag) {
|
||||
case 'b':
|
||||
case 'strong':
|
||||
docxPart += '<w:b/><w:bCs/>';
|
||||
break;
|
||||
case 'em':
|
||||
case 'i':
|
||||
docxPart += '<w:i/><w:iCs/>';
|
||||
break;
|
||||
case 'span':
|
||||
for (var key in tag.attrs) {
|
||||
switch (key) {
|
||||
case 'color':
|
||||
docxPart += '<w:color w:val="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'backgroundColor':
|
||||
docxPart += '<w:shd w:fill="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'underline':
|
||||
docxPart += '<w:u w:val="single"/>';
|
||||
break;
|
||||
case 'strike':
|
||||
docxPart += '<w:strike/>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'u':
|
||||
docxPart += '<w:u w:val="single"/>';
|
||||
break;
|
||||
case 'strike':
|
||||
docxPart += '<w:strike/>';
|
||||
break;
|
||||
case 'a':
|
||||
var id = converter.relationships.length + 1;
|
||||
docxPart = '<w:hyperlink r:id="rrId' + id + '">' + docxPart;
|
||||
docxPart += '<w:rStyle w:val="Internetlink"/>';
|
||||
converter.relationships.push({
|
||||
Id: 'rrId' + id,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
|
||||
Target: tag.href,
|
||||
TargetMode: 'External'
|
||||
});
|
||||
hyperlink = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
docxPart += '</w:rPr><w:t>' + text + '</w:t></w:r>';
|
||||
if (hyperlink) {
|
||||
docxPart += '</w:hyperlink>';
|
||||
}
|
||||
|
||||
// append to docx
|
||||
docx += docxPart;
|
||||
return docx;
|
||||
};
|
||||
|
||||
var replaceEntities = function () {
|
||||
// replacing of special symbols:
|
||||
docx = docx.replace(new RegExp('\ä\;', 'g'), 'ä');
|
||||
docx = docx.replace(new RegExp('\ü\;', 'g'), 'ü');
|
||||
docx = docx.replace(new RegExp('\ö\;', 'g'), 'ö');
|
||||
docx = docx.replace(new RegExp('\Ä\;', 'g'), 'Ä');
|
||||
docx = docx.replace(new RegExp('\Ü\;', 'g'), 'Ü');
|
||||
docx = docx.replace(new RegExp('\Ö\;', 'g'), 'Ö');
|
||||
docx = docx.replace(new RegExp('\ß\;', 'g'), 'ß');
|
||||
docx = docx.replace(new RegExp('\ \;', 'g'), ' ');
|
||||
docx = docx.replace(new RegExp('\§\;', 'g'), '§');
|
||||
|
||||
// remove all entities except gt, lt and amp
|
||||
var entityRegex = /\&(?!gt|lt|amp)\w+\;/g, matchEntry, indexes = [];
|
||||
while ((matchEntry = entityRegex.exec(docx)) !== null) {
|
||||
indexes.push({
|
||||
startId: matchEntry.index,
|
||||
stopId: matchEntry.index + matchEntry[0].length
|
||||
});
|
||||
}
|
||||
for (var i = indexes.length - 1; i>=0; i--) {
|
||||
docx = docx.substring(0, indexes[i].startId) + docx.substring(indexes[i].stopId, docx.length);
|
||||
}
|
||||
};
|
||||
|
||||
var parse = function () {
|
||||
if (html.substring(0,3) != '<p>') {
|
||||
docx += '<w:p>';
|
||||
skipFirstParagraphClosing = false;
|
||||
}
|
||||
html = html.split(/(<|>)/g);
|
||||
// remove whitespaces and > brackets. Leave < brackets in there to check, whether
|
||||
// the following string is a tag or text.
|
||||
html = _.filter(html, function (part) {
|
||||
var skippedCharsRegex = new RegExp('^([\s\n\r]|>)*$', 'g');
|
||||
return !skippedCharsRegex.test(part);
|
||||
});
|
||||
|
||||
for (var i = 0; i < html.length; i++) {
|
||||
if (html[i] === '<') {
|
||||
i++;
|
||||
handleTag(html[i]);
|
||||
} else {
|
||||
handleText(html[i]);
|
||||
}
|
||||
}
|
||||
// for finishing close the last paragraph (if open)
|
||||
if (inParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
|
||||
replaceEntities();
|
||||
|
||||
return docx;
|
||||
};
|
||||
|
||||
return parse();
|
||||
};
|
||||
|
||||
// return a wrapper function for html2docx, that fetches all the images.
|
||||
converter.html2docx = function (html) {
|
||||
var imageSources = _.map($(html).find('img'), function (element) {
|
||||
return element.getAttribute('src');
|
||||
});
|
||||
// Don't get images multiple times; just if the converter has not seen them befor.
|
||||
imageSources = _.filter(imageSources, function (src) {
|
||||
return !converter.imageMap[src];
|
||||
});
|
||||
return $q(function (resolve) {
|
||||
ImageConverter.toBase64(imageSources).then(function (_imageMap) {
|
||||
_.forEach(_imageMap, function (value, key) {
|
||||
converter.imageMap[key] = value;
|
||||
});
|
||||
var docx = html2docx(html);
|
||||
resolve(docx);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
converter.updateZipFile = function (zip) {
|
||||
var updateRelationships = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
_.forEach(converter.relationships, function (relationship) {
|
||||
content[1] += '<Relationship';
|
||||
_.forEach(relationship, function (value, key) {
|
||||
content[1] += ' ' + key + '="' + value + '"';
|
||||
});
|
||||
content[1] += '/>';
|
||||
});
|
||||
return content.join('\n');
|
||||
};
|
||||
var updateContentTypes = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
_.forEach(converter.contentTypes, function (type) {
|
||||
content[1] += '<Override';
|
||||
_.forEach(type, function (value, key) {
|
||||
content[1] += ' ' + key + '="' + value + '"';
|
||||
});
|
||||
content[1] += '/>';
|
||||
});
|
||||
return content.join('\n');
|
||||
};
|
||||
// update relationships from 'relationships'
|
||||
var relationships = updateRelationships(zip.file('word/_rels/document.xml.rels').asText());
|
||||
zip.file('word/_rels/document.xml.rels', relationships);
|
||||
|
||||
// update content type from 'contentTypes'
|
||||
var contentTypes = updateContentTypes(zip.file('[Content_Types].xml').asText());
|
||||
zip.file('[Content_Types].xml', contentTypes);
|
||||
|
||||
converter.documentImages = _.uniqBy(converter.documentImages, 'src');
|
||||
_.forEach(converter.documentImages, function (image) {
|
||||
var dataUrl = converter.imageMap[image.src].data;
|
||||
var base64 = dataUrl.split(',')[1];
|
||||
zip.file(image.zipPath, base64, {base64: true});
|
||||
});
|
||||
return zip;
|
||||
};
|
||||
|
||||
return converter;
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance,
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
})();
|
@ -1,124 +0,0 @@
|
||||
/* Worker for creating PDFs in a separate thread. The creation of larger PDFs
|
||||
* needs (currently) a lot of time and we don't want to block the UI.
|
||||
*/
|
||||
|
||||
// Setup fake environment for pdfMake
|
||||
var document = {
|
||||
'createElementNS': function () {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
var window = this;
|
||||
|
||||
// PdfMake
|
||||
importScripts('/static/js/workers/pdf-worker-libs.js');
|
||||
|
||||
// Set default font family.
|
||||
// "PdfFont" and "OSFont-*" are generic names used here and in core/pdf.js. The
|
||||
// suffix after "OSFont-" has to be the same as the config value.
|
||||
pdfMake.fonts = {
|
||||
PdfFont: {
|
||||
normal: 'OSFont-regular.ttf',
|
||||
bold: 'OSFont-bold.ttf',
|
||||
italics: 'OSFont-italic.ttf',
|
||||
bolditalics: 'OSFont-bold_italic.ttf'
|
||||
}
|
||||
};
|
||||
|
||||
// Function to replace layout placeholder
|
||||
//
|
||||
// Workaround for using table layout functions.
|
||||
// TODO: Needs improvement of pdfmake's web worker support.
|
||||
// Currently only functions are allowed for 'layout'.
|
||||
// But functions cannot be passed to workers (via JSON).
|
||||
var replacePlaceholder = function (content) {
|
||||
for (var i = 0; i < content.length; i++) {
|
||||
if (typeof content[i] === 'object') {
|
||||
|
||||
// motion meta table border lines
|
||||
if (content[i].layout === "{{motion-placeholder-to-insert-functions-here}}") {
|
||||
content[i].layout = {
|
||||
hLineWidth: function(i, node) {
|
||||
return (i === 0 || i === node.table.body.length) ? 0 : 0.5;
|
||||
},
|
||||
vLineWidth: function() {
|
||||
return 0;
|
||||
},
|
||||
hLineColor: function() {
|
||||
return 'white';
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// ballot paper crop marks
|
||||
if (content[i].layout === "{{ballot-placeholder-to-insert-functions-here}}") {
|
||||
content[i].layout = {
|
||||
hLineWidth: function(i, node) {
|
||||
if (i === 0){
|
||||
return 0;
|
||||
} else {
|
||||
return 0.5;
|
||||
}
|
||||
},
|
||||
vLineWidth: function(i, node) {
|
||||
return (i === 0 || i === node.table.widths.length) ? 0 : 0.5;
|
||||
},
|
||||
hLineColor: function() {
|
||||
return 'gray';
|
||||
},
|
||||
vLineColor: function() {
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
replacePlaceholder(content[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Workaround for using dynamic footer with page number.
|
||||
// TODO: Needs improvement of pdfmake's web worker support.
|
||||
// see https://github.com/bpampuch/pdfmake/issues/38
|
||||
var replaceFooter = function (doc) {
|
||||
if (doc.footerTpl) {
|
||||
doc.footer = function (currentPage, pageCount) {
|
||||
// One way to clone arrays/objects in js, that are serilizeable.
|
||||
var columns = JSON.parse(JSON.stringify(doc.footerTpl.columns));
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
if (columns[i].text) {
|
||||
columns[i].text = columns[i].text
|
||||
.replace('{{currentPage}}', currentPage)
|
||||
.replace('{{pageCount}}', pageCount);
|
||||
}
|
||||
}
|
||||
return {
|
||||
columns: columns,
|
||||
margin: doc.footerTpl.margin,
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Create PDF on message and return the base64 decoded document
|
||||
self.addEventListener('message', function(e) {
|
||||
var data = JSON.parse(e.data);
|
||||
pdfMake.vfs = data.vfs; // Set custom fonts.
|
||||
|
||||
var doc = data.pdfDocument;
|
||||
replaceFooter(doc);
|
||||
replacePlaceholder(doc.content);
|
||||
|
||||
var pdf = pdfMake.createPdf(doc);
|
||||
pdf.getBase64(function (base64) {
|
||||
if (data.filename) {
|
||||
self.postMessage(JSON.stringify({
|
||||
filename: data.filename,
|
||||
base64: base64
|
||||
}));
|
||||
} else {
|
||||
self.postMessage(base64);
|
||||
}
|
||||
});
|
||||
}, false);
|
@ -1,409 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
// The core module for the OpenSlides projector
|
||||
angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
||||
|
||||
// Can be used to find out if the projector or the side is used
|
||||
.constant('REALM', 'projector')
|
||||
|
||||
.run([
|
||||
'$http',
|
||||
'autoupdate',
|
||||
'DS',
|
||||
function ($http, autoupdate, DS) {
|
||||
autoupdate.newConnect();
|
||||
|
||||
// If the connection aborts, we try to ping the server with whoami requests. If
|
||||
// the server is flushed, we clear the datastore, so the message 'this projector
|
||||
// cannot be shown' will be displayed. Otherwise establish the websocket connection.
|
||||
autoupdate.registerRetryConnectCallback(function () {
|
||||
return $http.get('/users/whoami').then(function (success) {
|
||||
if (success.data.user_id === null && !success.data.guest_enabled) {
|
||||
DS.clear();
|
||||
} else {
|
||||
autoupdate.newConnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Provider to register slides in a .config() statement.
|
||||
.provider('slides', [
|
||||
function() {
|
||||
var slidesMap = {};
|
||||
|
||||
this.registerSlide = function(name, config) {
|
||||
slidesMap[name] = config;
|
||||
return this;
|
||||
};
|
||||
|
||||
this.$get = function($templateRequest, $q) {
|
||||
var self = this;
|
||||
return {
|
||||
getElements: function(projector) {
|
||||
var elements = [];
|
||||
var factory = this;
|
||||
_.forEach(projector.elements, function(element) {
|
||||
if (element.name in slidesMap) {
|
||||
element.template = slidesMap[element.name].template;
|
||||
elements.push(element);
|
||||
} else {
|
||||
console.error("Unknown slide: " + element.name);
|
||||
}
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.config([
|
||||
'slidesProvider',
|
||||
function(slidesProvider) {
|
||||
slidesProvider.registerSlide('core/clock', {
|
||||
template: 'static/templates/core/slide_clock.html',
|
||||
});
|
||||
|
||||
slidesProvider.registerSlide('core/countdown', {
|
||||
template: 'static/templates/core/slide_countdown.html',
|
||||
});
|
||||
|
||||
slidesProvider.registerSlide('core/projector-message', {
|
||||
template: 'static/templates/core/slide_message.html',
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('LanguageAndFontCtrl', [
|
||||
'$scope',
|
||||
'Languages',
|
||||
'Config',
|
||||
'Projector',
|
||||
'ProjectorID',
|
||||
'Fonts',
|
||||
function ($scope, Languages, Config, Projector, ProjectorID, Fonts) {
|
||||
// for the dynamic title
|
||||
$scope.projectorId = ProjectorID();
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified($scope.projectorId);
|
||||
}, function () {
|
||||
var projector = Projector.get($scope.projectorId);
|
||||
if (projector) {
|
||||
$scope.projectorName = projector.name;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('projector_language');
|
||||
}, function () {
|
||||
var lang = Config.get('projector_language');
|
||||
if (!lang || lang.value == 'browser') {
|
||||
$scope.selectedLanguage = Languages.getBrowserLanguage();
|
||||
} else {
|
||||
$scope.selectedLanguage = lang.value;
|
||||
}
|
||||
Languages.setCurrentLanguage($scope.selectedLanguage);
|
||||
});
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('font_regular') +
|
||||
Config.lastModified('font_italic') +
|
||||
Config.lastModified('font_bold') +
|
||||
Config.lastModified('font_bold_italic');
|
||||
}, function () {
|
||||
$scope.font = Fonts.getForCss('font_regular');
|
||||
$scope.font_medium = Fonts.getForCss('font_italic');
|
||||
$scope.font_condensed = Fonts.getForCss('font_bold');
|
||||
$scope.font_condensed_light = Fonts.getForCss('font_bold_italic');
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Projector Container Controller
|
||||
.controller('ProjectorContainerCtrl', [
|
||||
'$scope',
|
||||
'$timeout',
|
||||
'$location',
|
||||
'gettext',
|
||||
'Projector',
|
||||
function($scope, $timeout, $location, gettext, Projector) {
|
||||
$scope.showError = true;
|
||||
|
||||
// watch for changes in Projector
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified($scope.projectorId);
|
||||
}, function () {
|
||||
var projector = Projector.get($scope.projectorId);
|
||||
if (projector) {
|
||||
$scope.showError = false;
|
||||
$scope.projectorWidth = projector.width;
|
||||
$scope.projectorHeight = projector.height;
|
||||
$scope.recalculateIframe();
|
||||
} else {
|
||||
$scope.showError = true;
|
||||
// delay displaying the error message, because with a slow internet
|
||||
// connection, the autoupdate with the projector may be delayed. We
|
||||
// de not want to irritate the user by showing this error to early.
|
||||
$scope.error = '';
|
||||
$timeout(function () {
|
||||
if ($scope.showError) {
|
||||
$scope.error = gettext('Can not open the projector.');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// recalculate the actual Iframesize and scale
|
||||
$scope.recalculateIframe = function () {
|
||||
var scale_width = window.innerWidth / $scope.projectorWidth;
|
||||
var scale_height = window.innerHeight / $scope.projectorHeight;
|
||||
|
||||
// Iframe has to be scaled down or saceUp is activated
|
||||
if (scale_width <= scale_height) {
|
||||
// width is the reference
|
||||
$scope.iframeWidth = window.innerWidth;
|
||||
$scope.scale = scale_width;
|
||||
$scope.iframeHeight = $scope.projectorHeight * scale_width;
|
||||
} else {
|
||||
// height is the reference
|
||||
$scope.iframeHeight = window.innerHeight;
|
||||
$scope.scale = scale_height;
|
||||
$scope.iframeWidth = $scope.projectorWidth * scale_height;
|
||||
}
|
||||
};
|
||||
|
||||
// watch for changes in the windowsize
|
||||
$(window).on("resize.doResize", function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.recalculateIframe();
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$on("$destroy",function (){
|
||||
$(window).off("resize.doResize");
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('ProjectorCtrl', [
|
||||
'$scope',
|
||||
'$location',
|
||||
'$timeout',
|
||||
'Projector',
|
||||
'slides',
|
||||
'Config',
|
||||
'ProjectorID',
|
||||
'Logos',
|
||||
function($scope, $location, $timeout, Projector, slides, Config, ProjectorID, Logos) {
|
||||
var projectorId = ProjectorID();
|
||||
|
||||
$scope.broadcast = 0;
|
||||
|
||||
var setElements = function (projector) {
|
||||
// Get all elements, that should be projected.
|
||||
var newElements = [];
|
||||
var enable_clock = Config.get('projector_enable_clock');
|
||||
enable_clock = enable_clock ? enable_clock.value : true;
|
||||
_.forEach(slides.getElements(projector), function (element) {
|
||||
if (!element.error) {
|
||||
// Exclude the clock if it should be disabled.
|
||||
if (enable_clock || element.name !== 'core/clock') {
|
||||
newElements.push(element);
|
||||
}
|
||||
} else {
|
||||
console.error("Error for slide " + element.name + ": " + element.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Now we have to align $scope.elements to newElements:
|
||||
// We cannot just assign them, because the ng-repeat would reload every
|
||||
// element. This should be prevented (see #3259). To change $scope.elements:
|
||||
// 1) remove all elements from scope, that are not in newElements (compared by the uuid)
|
||||
// 2) Every new element in newElements, that is not in $scope.elements, get inserted there.
|
||||
// 3) If there is the same element in newElements and $scope.elements every changed property
|
||||
// is copied from the new element to the scope element.
|
||||
|
||||
$scope.elements = _.filter($scope.elements, function (element) {
|
||||
return _.some(newElements, function (newElement) {
|
||||
return element.uuid === newElement.uuid;
|
||||
});
|
||||
});
|
||||
|
||||
_.forEach(newElements, function (newElement) {
|
||||
var matchingElement = _.find($scope.elements, function (element) {
|
||||
return element.uuid === newElement.uuid;
|
||||
});
|
||||
if (matchingElement) {
|
||||
// copy all changed properties.
|
||||
_.forEach(newElement, function (value, key) {
|
||||
// key has own property and does not start with a '$'.
|
||||
if (newElement.hasOwnProperty(key) && key.indexOf('$') != 0) {
|
||||
if (typeof matchingElement[key] === 'undefined' || matchingElement[key] !== value) {
|
||||
matchingElement[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$scope.elements.push(newElement);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.scroll = 0;
|
||||
var setScroll = function (scroll) {
|
||||
$scope.scroll = -250 * scroll;
|
||||
};
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified(projectorId);
|
||||
}, function () {
|
||||
$scope.projector = Projector.get(projectorId);
|
||||
if ($scope.projector) {
|
||||
if ($scope.broadcast === 0) {
|
||||
setElements($scope.projector);
|
||||
$scope.blank = $scope.projector.blank;
|
||||
}
|
||||
setScroll($scope.projector.scroll);
|
||||
} else {
|
||||
// Blank projector on error
|
||||
$scope.elements = [];
|
||||
$scope.projector = {
|
||||
scale: 0,
|
||||
blank: true
|
||||
};
|
||||
setScroll(0);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('projector_broadcast');
|
||||
}, function () {
|
||||
var bc = Config.get('projector_broadcast');
|
||||
if (bc) {
|
||||
if ($scope.broadcast != bc.value) {
|
||||
$scope.broadcast = bc.value;
|
||||
if ($scope.broadcastDeregister) {
|
||||
// revert to original $scope.projector
|
||||
$scope.broadcastDeregister();
|
||||
$scope.broadcastDeregister = null;
|
||||
setElements($scope.projector);
|
||||
$scope.blank = $scope.projector.blank;
|
||||
}
|
||||
}
|
||||
if ($scope.broadcast > 0) {
|
||||
// get elements and blank from broadcast projector
|
||||
$scope.broadcastDeregister = $scope.$watch(function () {
|
||||
return Projector.lastModified($scope.broadcast);
|
||||
}, function () {
|
||||
if ($scope.broadcast > 0) {
|
||||
var broadcast_projector = Projector.get($scope.broadcast);
|
||||
if (broadcast_projector) {
|
||||
setElements(broadcast_projector);
|
||||
$scope.blank = broadcast_projector.blank;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified('projector_enable_clock');
|
||||
}, function () {
|
||||
setElements($scope.projector);
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
if ($scope.broadcastDeregister) {
|
||||
$scope.broadcastDeregister();
|
||||
$scope.broadcastDeregister = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideClockCtrl', [
|
||||
'$scope',
|
||||
'$interval',
|
||||
function($scope, $interval) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
$scope.servertime = ( Date.now() / 1000 - $scope.serverOffset ) * 1000;
|
||||
var interval = $interval(function () {
|
||||
$scope.servertime = ( Date.now() / 1000 - $scope.serverOffset ) * 1000;
|
||||
}, 30000); // Update the clock every 30 seconds
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
if (interval) {
|
||||
$interval.cancel(interval);
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideCountdownCtrl', [
|
||||
'$scope',
|
||||
'$interval',
|
||||
'Countdown',
|
||||
function($scope, $interval, Countdown) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
var interval;
|
||||
var calculateCountdownTime = function (countdown) {
|
||||
countdown.seconds = Math.floor( $scope.countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
};
|
||||
$scope.$watch(function () {
|
||||
return Countdown.lastModified(id);
|
||||
}, function () {
|
||||
$scope.countdown = Countdown.get(id);
|
||||
if (interval) {
|
||||
$interval.cancel(interval);
|
||||
}
|
||||
if ($scope.countdown) {
|
||||
if ($scope.countdown.running) {
|
||||
calculateCountdownTime($scope.countdown);
|
||||
interval = $interval(function () { calculateCountdownTime($scope.countdown); }, 1000);
|
||||
} else {
|
||||
$scope.countdown.seconds = $scope.countdown.countdown_time;
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.$on('$destroy', function() {
|
||||
// Cancel the interval if the controller is destroyed
|
||||
if (interval) {
|
||||
$interval.cancel(interval);
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('SlideMessageCtrl', [
|
||||
'$scope',
|
||||
'ProjectorMessage',
|
||||
'Projector',
|
||||
'ProjectorID',
|
||||
'gettextCatalog',
|
||||
function($scope, ProjectorMessage, Projector, ProjectorID, gettextCatalog) {
|
||||
// Attention! Each object that is used here has to be dealt on server side.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
|
||||
if ($scope.element.identify) {
|
||||
var projector = Projector.get(ProjectorID());
|
||||
$scope.identifyMessage = gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + gettextCatalog.getString(projector.name);
|
||||
} else {
|
||||
$scope.message = ProjectorMessage.get(id);
|
||||
ProjectorMessage.bindOne(id, $scope, 'message');
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,50 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.core.remove-format-plugin', [
|
||||
'OpenSlidesApp.core',
|
||||
])
|
||||
|
||||
/*
|
||||
* Plugin for the CKEditor that hooks into the removeformat plugin
|
||||
* which is a default plugin enabled by 'cleanup' in the config
|
||||
* toolbar.
|
||||
* We change the behavior of the removeformat command here:
|
||||
* It should not remove any tags and styles, but only the
|
||||
* 'DISALLOWED_STYLES'. Removeformat traverses through the DOM
|
||||
* and calles for every element the custom filter down below.
|
||||
* We change the element and return false, so the removeformat
|
||||
* plugin does not clean it up.
|
||||
*/
|
||||
.factory('OSRemoveFormatPlugin', [
|
||||
'Editor',
|
||||
'gettextCatalog',
|
||||
function (Editor, gettextCatalog) {
|
||||
var DISALLOWED_STYLES = ['color', 'background-color'];
|
||||
return {
|
||||
getPlugin: function () {
|
||||
return {
|
||||
init: function (editor) {
|
||||
editor.addRemoveFormatFilter(function (element) {
|
||||
_.forEach(DISALLOWED_STYLES, function (style) {
|
||||
element.removeStyle(style);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.run([
|
||||
'Editor',
|
||||
'OSRemoveFormatPlugin',
|
||||
function (Editor, OSRemoveFormatPlugin, gettext) {
|
||||
Editor.registerPlugin('OSRemoveFormat', OSRemoveFormatPlugin.getPlugin());
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,117 +0,0 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.core.start', [])
|
||||
|
||||
.factory('OpenSlides', [
|
||||
'$http',
|
||||
'$rootScope',
|
||||
'$state',
|
||||
'$q',
|
||||
'DS',
|
||||
'autoupdate',
|
||||
'operator',
|
||||
'Group',
|
||||
'mainMenu',
|
||||
'ngDialog',
|
||||
'LoginDialog',
|
||||
function($http, $rootScope, $state, $q, DS, autoupdate, operator, Group, mainMenu, ngDialog, LoginDialog) {
|
||||
var OpenSlides = {
|
||||
bootup: function () {
|
||||
$rootScope.openslidesBootstrapDone = false;
|
||||
$http.get('/users/whoami/').then(function (success) {
|
||||
$rootScope.guest_enabled = success.data.guest_enabled;
|
||||
if (success.data.user_id === null && !success.data.guest_enabled) {
|
||||
// Redirect to login dialog if user is not logged in.
|
||||
$state.go('login', {guest_enabled: success.data.guest_enabled});
|
||||
} else {
|
||||
autoupdate.newConnect();
|
||||
autoupdate.firstMessageDeferred.promise.then(function () {
|
||||
operator.setUser(success.data.user_id, success.data.user);
|
||||
$rootScope.operator = operator;
|
||||
mainMenu.updateMainMenu();
|
||||
$rootScope.openslidesBootstrapDone = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
shutdown: function () {
|
||||
// Close connection, clear the store and show the OS overlay.
|
||||
autoupdate.closeConnection();
|
||||
DS.clear();
|
||||
operator.setUser(null);
|
||||
$rootScope.openslidesBootstrapDone = false;
|
||||
$rootScope.operator = operator;
|
||||
// close all open dialogs (except the login dialog)
|
||||
_.forEach(ngDialog.getOpenDialogs(), function (id) {
|
||||
if (id !== LoginDialog.id) {
|
||||
ngDialog.close(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
reboot: function () {
|
||||
this.shutdown();
|
||||
this.bootup();
|
||||
},
|
||||
};
|
||||
|
||||
// We need to 'ping' the server with a get request to whoami, because then we can decide,
|
||||
// if the server is down or respond with a 403 (this cannot be differentiated with websockets)
|
||||
autoupdate.registerRetryConnectCallback(function () {
|
||||
return $http.get('/users/whoami').then(function (success) {
|
||||
if (success.data.user_id === null && !success.data.guest_enabled) {
|
||||
OpenSlides.shutdown();
|
||||
// Redirect to login dialog if user is not logged in.
|
||||
$state.go('login', {guest_enabled: success.data.guest_enabled});
|
||||
} else {
|
||||
autoupdate.newConnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return OpenSlides;
|
||||
}
|
||||
])
|
||||
|
||||
.run([
|
||||
'OpenSlides',
|
||||
function (OpenSlides) {
|
||||
OpenSlides.bootup();
|
||||
}
|
||||
])
|
||||
|
||||
.run([
|
||||
'$rootScope',
|
||||
'$state',
|
||||
'operator',
|
||||
'User',
|
||||
'Group',
|
||||
'mainMenu',
|
||||
function ($rootScope, $state, operator, User, Group, mainMenu) {
|
||||
var permissionChangeCallback = function () {
|
||||
operator.reloadPerms();
|
||||
mainMenu.updateMainMenu();
|
||||
var stateData = $state.current.data;
|
||||
var basePerm = stateData ? stateData.basePerm : '';
|
||||
$rootScope.baseViewPermissionsGranted = basePerm ?
|
||||
operator.hasPerms(basePerm) : true;
|
||||
};
|
||||
|
||||
$rootScope.$watch(function () {
|
||||
return Group.lastModified();
|
||||
}, function () {
|
||||
if (Group.getAll().length) {
|
||||
permissionChangeCallback();
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$watch(function () {
|
||||
return operator.user ? User.lastModified(operator.user.id) : true;
|
||||
}, function () {
|
||||
permissionChangeCallback();
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -1,152 +0,0 @@
|
||||
<div class="form-group">
|
||||
<label>{{ label | translate }}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<!-- text/number input -->
|
||||
<input ng-if="type === 'text' || type === 'number'"
|
||||
ng-model="$parent.value"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
ng-class="{ 'form-control': type != 'checkbox' }"
|
||||
id="{{ key }}"
|
||||
type="{{ type }}">
|
||||
|
||||
<!-- checkbox -->
|
||||
<div ng-if="type === 'checkbox'" class="config-checkbox">
|
||||
<i class="fa pointer" id="{{ key }}"
|
||||
ng-click="$parent.value = !$parent.value; save(configOption, $parent.value)"
|
||||
ng-class="$parent.value ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
</div>
|
||||
|
||||
<!-- comments -->
|
||||
<div class="comments" ng-if="type === 'comments'">
|
||||
<div ng-repeat="(id, field) in ($parent.value
|
||||
| excludeSpecialCommentsFields
|
||||
| excludeDeletedAndForbiddenCommentsFields)"
|
||||
class="input-group">
|
||||
<input ng-model="field.name"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
class="form-control"
|
||||
id="{{ key }}"
|
||||
type="text">
|
||||
<span class="input-group-btn">
|
||||
<button type=button" class="btn btn-default"
|
||||
ng-click="field.public = !field.public; save(configOption, $parent.value);">
|
||||
<i class="fa" ng-class="field.public ? 'fa-unlock' : 'fa-lock'"></i>
|
||||
{{ (field.public ? 'Public' : 'Private') | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="removeComment(configOption, $parent, id)">
|
||||
<i class="fa fa-minus"></i>
|
||||
<translate>Remove</translate>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" ng-click="addComment(configOption, $parent)"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>Add new comment field</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- colorpicker -->
|
||||
<input ng-if="type === 'colorpicker'"
|
||||
colorpicker
|
||||
class="form-control"
|
||||
ng-model="$parent.value"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
type="text">
|
||||
|
||||
<!-- datetimepicker -->
|
||||
<input ng-if="type === 'datetimepicker'"
|
||||
class="form-control"
|
||||
datetime-picker="dd.MM.yyyy HH:mm"
|
||||
ng-model="$parent.value"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
is-open="datetimeOpen[key]"
|
||||
ng-focus="datetimeOpen[key]=true;"
|
||||
save-as="'number'"
|
||||
button-bar="dateTimePickerTranslatedButtons"
|
||||
default-time="10:00:00">
|
||||
|
||||
<!-- textarea -->
|
||||
<textarea ng-if="type === 'textarea'"
|
||||
ng-model="$parent.value"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
id="{{ key }}" class="form-control">
|
||||
</textarea>
|
||||
|
||||
<!-- editor -->
|
||||
<textarea ng-if="type === 'editor'"
|
||||
id="{{ configOption.key }}"
|
||||
ckeditor="ckeditorOptions"
|
||||
ng-model="$parent.value" class="form-control"
|
||||
ng-model-options="{debounce: 1000}">
|
||||
</textarea>
|
||||
|
||||
<!-- select -->
|
||||
<select ng-if="type === 'choice'"
|
||||
ng-model="$parent.value"
|
||||
ng-model-options="{debounce: 500}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
id="{{ key }}" class="form-control"
|
||||
ng-options="option.value as option.display_name | translate for option in choices">
|
||||
</select>
|
||||
|
||||
<!-- custom trnaslations -->
|
||||
<div class="custom-translations" ng-if="type === 'translations'">
|
||||
<div ng-repeat="entry in $parent.value" class="input-group">
|
||||
<div class="inputs">
|
||||
<input ng-model="entry.original"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
class="form-control"
|
||||
id="{{ key }}_original"
|
||||
type="text">
|
||||
<span class="arrow form-control"><i class="fa fa-arrow-right"></i></span>
|
||||
<input ng-model="entry.translation"
|
||||
ng-model-options="{debounce: 1000}"
|
||||
ng-change="save(configOption, $parent.value)"
|
||||
class="form-control"
|
||||
id="{{ key }}_translated"
|
||||
type="text">
|
||||
</div>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="removeTranslation(configOption, $parent, $index)">
|
||||
<i class="fa fa-minus"></i>
|
||||
<translate>Remove</translate>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" ng-click="addTranslation(configOption, $parent)"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>Add new custom translation</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="success-{{ key }}" class="input-group-addon" ng-if="configOption.success !== undefined">
|
||||
<i class="fa fa-lg fa-check-circle text-success"
|
||||
ng-if="configOption.success === true"></i>
|
||||
<i class="fa fa-lg fa-exclamation-triangle text-danger"
|
||||
ng-if="configOption.success === false"
|
||||
uib-tooltip="{{ configOption.errorMessage | translate }}"></i>
|
||||
</span>
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button ng-click="reset()" class="btn btn-default" title="{{ default_value | translate }}">
|
||||
<i class="fa fa-undo"></i>
|
||||
<translate>Reset</translate>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<p ng-if="help_text" class="help-block">{{ help_text | translate }}</p>
|
||||
</div>
|
@ -1,35 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<h1 translate>Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config" class="details">
|
||||
<div os-perms="core.can_manage_config" class="panel-group" id="accordion"
|
||||
role="tablist" aria-multiselectable="true">
|
||||
<!-- generate config groups -->
|
||||
<div ng-repeat="group in configGroups">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="heading-group{{ $index }}">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#group{{ $index }}"
|
||||
aria-expanded="false" aria-controls="group{{ $index }}">
|
||||
{{ group.name | translate }}
|
||||
</a>
|
||||
</h4>
|
||||
</div> <!-- heading -->
|
||||
<div id="group{{ $index }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-group{{ $index }}">
|
||||
<div class="panel-body">
|
||||
<div ng-repeat="subgroup in group.subgroups">
|
||||
<h3>{{ subgroup.name | translate }}</h3>
|
||||
<div ng-repeat="configOption in subgroup.items">
|
||||
<os-form-field field="configOption"></os-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- group.name -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,14 +0,0 @@
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label" ng-if="to.label">
|
||||
{{ to.label }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<label ng-repeat="option in to.options" class="btn btn-default btn-sm" uib-btn-checkbox
|
||||
ng-model="model[options.key][option.id]"
|
||||
ng-disabled="option.disabled">
|
||||
{{ option.name | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
@ -1,7 +0,0 @@
|
||||
<div class="checkbox">
|
||||
<label ng-click="model[options.key] = !model[options.key]">
|
||||
<i class="fa" ng-class="model[options.key] ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
{{to.label}}
|
||||
{{to.required ? '*' : ''}}
|
||||
</label>
|
||||
</div>
|
@ -1,16 +0,0 @@
|
||||
<div id="countdownWrapper">
|
||||
<div>
|
||||
<div id="countdown" ng-class="{
|
||||
'negative': countdown.seconds <= 0,
|
||||
'warning_time': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||
<div class="row">
|
||||
<div class="col-d-12">{{ countdown.seconds | osSecondsToTime}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 description">
|
||||
<span class="pull-right">{{ countdown.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,2 +0,0 @@
|
||||
<!-- custom angular formly template for ckeditor textarea field -->
|
||||
<textarea ckeditor="options.data.ckeditorOptions" ng-model="model[options.key]" class="form-control"></textarea>
|
@ -1,2 +0,0 @@
|
||||
<!-- custom angular formly template for uploading a file -->
|
||||
<input type="file" ngf-select ngf-change="to.change(model, $files, $event, $rejectedFiles)" class="form-control">
|
@ -1,30 +0,0 @@
|
||||
<form ng-submit="login()">
|
||||
<div class="modal-header">
|
||||
<img src="/static/img/openslides-logo.png" alt="OpenSlides" class="login-logo center-block">
|
||||
</div>
|
||||
<div class="modal-body loginForm">
|
||||
<div uib-alert ng-repeat="alert in alerts" ng-class="'alert-' + (alert.type || 'warning')" close="closeAlert($index)">
|
||||
<span ng-bind-html="alert.msg | translate"><span>
|
||||
</div>
|
||||
<div class="input-group form-group">
|
||||
<div class="input-group-addon"><i class="fa fa-user"></i></div>
|
||||
<input os-focus-me type="text" ng-model="username" class="form-control input-lg"
|
||||
placeholder="{{ 'Username' | translate }}">
|
||||
</div>
|
||||
<div class="input-group form-group">
|
||||
<div class="input-group-addon"><i class="fa fa-key"></i></div>
|
||||
<input type="password" ng-model="password" class="form-control input-lg"
|
||||
placeholder="{{ 'Password' | translate }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary" translate>
|
||||
Login
|
||||
</button>
|
||||
<button ng-if="guestAllowed" ng-click="guestLogin()" class="btn btn-default" translate>
|
||||
Continue as guest
|
||||
</button>
|
||||
<template-hook hook-name="loginFormButtons"></template-hook>
|
||||
</div>
|
||||
<a href="" ng-click="openPrivacyPolicyDialog()" translate>Privacy policy</a>
|
||||
</div>
|
||||
</form>
|
@ -1,219 +0,0 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu" os-perms="core.can_manage_projector">
|
||||
<!-- New -->
|
||||
<button class="btn btn-primary btn-sm" ng-bootbox-prompt="{{ 'Please enter a name for the new projector' | translate }}"
|
||||
ng-bootbox-prompt-action="createProjector(result)">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>New</translate>
|
||||
</button>
|
||||
<!-- Reference for current list of speakers -->
|
||||
<div class="btn-group" uib-dropdown>
|
||||
<button class="btn btn-default btn-sm" id="menuListofSpeakers" uib-dropdown-toggle
|
||||
uib-tooltip="{{ 'Select the projector to which the current list of speakers refers to.' | translate }}"
|
||||
aria-haspopup="true" aria-expanded="true">
|
||||
<i class="fa fa-microphone"></i>
|
||||
<translate>Reference for current list of speakers</translate>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu aria-labelledby="menuListOfSpeakers">
|
||||
<li ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href ng-click="setListOfSpeakers(projector)">
|
||||
<i class="fa fa-check" ng-if="projector.id == currentListOfSpeakers"></i>
|
||||
{{ projector.name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<button class="btn btn-sm" ng-click="identifyProjectors()" ng-class="identifyPromise ? 'btn-primary' : 'btn-default'"
|
||||
uib-tooltip="{{ 'Display an identifier message on all projectors with the id and the name.' | translate }}">
|
||||
<i class="fa fa-binoculars"></i>
|
||||
<translate>Identify</translate>
|
||||
</button>
|
||||
</div>
|
||||
<h1 translate>Manage projectors</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details" os-perms="core.can_manage_projector">
|
||||
<div id="manage-projectors">
|
||||
|
||||
<div ng-repeat="projector in projectors | orderBy: 'id'">
|
||||
<div>
|
||||
<a ui-sref="projector({id: projector.id})">
|
||||
{{ projector.id }}:
|
||||
<strong>{{ projector.name | translate }}</strong>
|
||||
</a>
|
||||
<span class="pull-right">
|
||||
<a href="" ng-click="toggleEditMenu(projector.id)"><i class="fa" ng-class="edit[projector.id] ? 'fa-times' : 'fa-pencil'"></i></a>
|
||||
<a href="" class="text-danger" style="padding-left: 5px;"
|
||||
ng-hide="projector.id==1 || edit[projector.id]"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ projector.name | translate }}</b>"
|
||||
ng-bootbox-confirm-action="deleteProjector(projector)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-show="edit[projector.id]" style="margin-bottom: -20px;">
|
||||
<div class="form-group">
|
||||
<label for="name{{ projector.id }}" class="control-label"><translate>Name</translate>:</label>
|
||||
<input type="text" class="form-control" id="name{{ projector.id }}"
|
||||
ng-model="projector.name" ng-change="editName(projector)"
|
||||
ng-model-options="{debounce: 2000}"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="menuProjector{{ projector.id }}" class="control-label"><translate>Projection defaults</translate>:</label>
|
||||
<div class="dropdown" uib-dropdown>
|
||||
<button class="btn btn-default btn-sm" id="menuProjector{{ projector.id }}" uib-dropdown-toggle>
|
||||
--- <translate>Please select</translate> ---
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu aria-labelledby="menuProjector{{ projector.id }}">
|
||||
<li ng-repeat="projectiondefault in projectiondefaults | orderBy:'id'">
|
||||
<a href ng-click="setProjectionDefault(projector, projectiondefault)">
|
||||
<i class="fa fa-check" ng-if="projectiondefault.projector_id === projector.id"></i>
|
||||
{{ projectiondefault.display_name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="aspectRatio{{ projector.id }}" class="control-label"><translate>Aspect ratio</translate>:</label>
|
||||
<div id="aspectRatio{{ projector.id }}">
|
||||
<div class="dropdown" uib-dropdown>
|
||||
<button class="btn btn-default btn-sm" uib-dropdown-toggle>
|
||||
{{ resolutions[projector.id].aspectRatio }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu aria-labelledby="aspectRatio{{ projector.id }}">
|
||||
<li ng-repeat="(aspectRatio, value) in aspectRatios track by $index">
|
||||
<a href ng-click="setAspectRatio(projector, aspectRatio); saveResolution(projector);">
|
||||
<i class="fa fa-check" ng-if="aspectRatio === resolutions[projector.id].aspectRatio"></i>
|
||||
{{ aspectRatio }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="resolution{{ projector.id }}" class="control-label"><translate>Scaling</translate>:</label>
|
||||
<div id="resolution{{ projector.id }}">
|
||||
<rzslider class="os-slider"
|
||||
rz-slider-model="sliders[projector.id].value"
|
||||
rz-slider-options="sliders[projector.id].options"></rzslider>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
{{ resolutions[projector.id].error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#iframe_{{ projector.id }} {
|
||||
width: {{ projector.width }}px;
|
||||
height: {{ projector.height }}px;
|
||||
-moz-transform: scale({{ projector.iframeScale }});
|
||||
-webkit-transform: scale({{ projector.iframeScale }});
|
||||
-o-transform: scale({{ projector.iframeScale }});
|
||||
transform: scale({{ projector.iframeScale }});
|
||||
/* IE8+ - must be on one line, unfortunately */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ projector.iframeScale }}, M12=0, M21=0, M22={{ projector.iframeScale }}, SizingMethod='auto expand')";
|
||||
}
|
||||
|
||||
#iframewrapper_{{ projector.id }} {
|
||||
height: {{ projector.iframeHeight + 2 }}px;
|
||||
}
|
||||
|
||||
#iframeoverlay_{{ projector.id }} {
|
||||
height: {{ projector.iframeHeight }}px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a ui-sref="projector({id: projector.id})" target="_blank">
|
||||
<div class="iframewrapper" id="iframewrapper_{{ projector.id }}">
|
||||
<iframe class="iframe" id="iframe_{{ projector.id }}" ng-src="{{ '/real-projector/' + projector.id + '/' }}" frameborder="0"></iframe>
|
||||
<div class="iframeoverlay" id="iframeoverlay_{{ projector.id }}"></div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- projector control buttons -->
|
||||
<div class="nobr">
|
||||
<!-- edit -->
|
||||
<a ng-click="editCurrentSlide(projector)"
|
||||
ng-disabled="!projector.getFormOrStateForCurrentSlide()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit current slide' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
|
||||
<!-- scale -->
|
||||
<div class="btn-group">
|
||||
<a ng-click="projector.controlProjector('scale', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Smaller' | translate}}">
|
||||
<i class="fa fa-search-minus"></i>
|
||||
</a>
|
||||
<a ng-click="projector.controlProjector('scale', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Bigger' | translate}}">
|
||||
<i class="fa fa-search-plus"></i>
|
||||
</a>
|
||||
<a ng-click="projector.controlProjector('scale', 'reset')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Reset scaling' | translate}}">
|
||||
<i class="fa fa-undo"></i>
|
||||
</a>
|
||||
</div>
|
||||
<span ng-class="{'notNull': projector.scale != 0}">{{ projector.scale }}</span>
|
||||
|
||||
<!-- scroll -->
|
||||
<div class="btn-group">
|
||||
<a ng-click="projector.controlProjector('scroll', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll up' | translate}}">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</a>
|
||||
<a ng-click="projector.controlProjector('scroll', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll down' | translate}}">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</a>
|
||||
<a ng-click="projector.controlProjector('scroll', 'reset')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Reset scrolling' | translate}}">
|
||||
<i class="fa fa-undo"></i>
|
||||
</a>
|
||||
</div>
|
||||
<span ng-class="{'notNull': projector.scroll != 0}">{{ projector.scroll }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Default, BC, Blank -->
|
||||
<div class="middle">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm" ng-class="broadcast == projector.id ? 'btn-danger' : 'btn-default'"
|
||||
ng-click="projector.toggleBroadcast(projector)"
|
||||
ng-disabled="broadcast > 0 && broadcast != projector.id"
|
||||
uib-tooltip="{{ 'Broadcast the content of this projector to all other projectors.' | translate }}"
|
||||
tooltip-placement="bottom">
|
||||
<i class="fa fa-bullhorn"></i>
|
||||
<translate>Broadcast</translate>
|
||||
</button>
|
||||
<button class="btn btn-sm" ng-class="projector.blank ? 'btn-danger' : 'btn-default'"
|
||||
ng-click="projector.toggleBlank(projector)"
|
||||
ng-disabled="broadcast > 0 && broadcast != projector.id">
|
||||
<i class="fa" ng-class="projector.blank ? 'fa-square' : 'fa-square-o'"></i>
|
||||
<translate>Blank</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1 +0,0 @@
|
||||
<input type="password" ng-model="model[options.key]" class="form-control">
|
@ -1,312 +0,0 @@
|
||||
<div class="details">
|
||||
|
||||
<div ng-controller="ProjectorControlCtrl">
|
||||
<!-- live view -->
|
||||
<div class="section" os-perms="core.can_see_projector">
|
||||
<a href="#" ng-click="isLiveViewClosed = !isLiveViewClosed">
|
||||
<i class="fa toggle-icon" ng-class="isLiveViewClosed ? 'fa-angle-down' : 'fa-angle-up'"></i>
|
||||
<h4 translate>Live view</h4>
|
||||
</a>
|
||||
<div uib-collapse="isLiveViewClosed" ng-cloak>
|
||||
<style>
|
||||
.col2 #iframe_sidebar {
|
||||
width: {{ active_projector.width }}px;
|
||||
height: {{ active_projector.height }}px;
|
||||
-moz-transform: scale({{ scale }});
|
||||
-webkit-transform: scale({{ scale }});
|
||||
-o-transform: scale({{ scale }});
|
||||
transform: scale({{ scale }});
|
||||
/* IE8+ - must be on one line, unfortunately */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')";
|
||||
}
|
||||
|
||||
.col2 #iframewrapper_sidebar {
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
|
||||
.col2 #iframeoverlay_sidebar {
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
</style>
|
||||
<div class="projectorSelector">
|
||||
<div>
|
||||
<div ng-show="projectors.length > 1" uib-dropdown>
|
||||
<button class="btn btn-default btn-sm dropdown-toggle" id="menuProjector" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="true">
|
||||
{{ active_projector.name | translate }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector">
|
||||
<li ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href ng-class="{'projected': projector === active_projector}"
|
||||
ng-click="changeProjector(projector)">
|
||||
<i ng-show="projector === active_projector" class="fa fa-check"></i>
|
||||
{{ projector.name | translate }}
|
||||
<i ng-show="projector.id == broadcast" class="fa fa-star-o spacer-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div os-perms="core.can_manage_projector">
|
||||
<button class="btn btn-sm" ng-click="active_projector.toggleBlank()" ng-hide="projectors.length > 1"
|
||||
ng-class="active_projector.blank ? 'btn-danger' : 'btn-default'">
|
||||
<i class="fa" ng-class="active_projector.blank ? 'fa-square' : 'fa-square-o'"></i>
|
||||
<translate>Blank</translate>
|
||||
</button>
|
||||
</div>
|
||||
<a os-perms="core.can_manage_projector" class="btn btn-primary btn-sm manageBtn" ui-sref="manage-projectors">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
<translate>Manage</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a ui-sref="projector({id: active_projector.id })" target="_blank">
|
||||
<div class="iframewrapper" id="iframewrapper_sidebar">
|
||||
<iframe class="iframe" id="iframe_sidebar" ng-src="{{ '/real-projector/' + active_projector.id + '/'}}" frameborder="0"></iframe>
|
||||
<div class="iframeoverlay" id="iframeoverlay_sidebar"></div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- projector control buttons -->
|
||||
<div os-perms="core.can_manage_projector" class="nobr">
|
||||
<!-- edit -->
|
||||
<a ng-click="editCurrentSlide(active_projector)"
|
||||
ng-disabled="!active_projector.getFormOrStateForCurrentSlide()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit current slide' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
|
||||
<!-- scale -->
|
||||
<div class="btn-group">
|
||||
<a ng-click="active_projector.controlProjector('scale', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Smaller' | translate}}">
|
||||
<i class="fa fa-search-minus"></i>
|
||||
</a>
|
||||
<a ng-click="active_projector.controlProjector('scale', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Bigger' | translate}}">
|
||||
<i class="fa fa-search-plus"></i>
|
||||
</a>
|
||||
<a ng-click="active_projector.controlProjector('scale', 'reset')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Reset scaling' | translate}}">
|
||||
<i class="fa fa-undo"></i>
|
||||
</a>
|
||||
</div>
|
||||
<span ng-class="{ 'notNull': active_projector.scale != 0 }">{{ active_projector.scale }}</span>
|
||||
|
||||
<!-- scroll -->
|
||||
<div class="btn-group">
|
||||
<a ng-click="active_projector.controlProjector('scroll', 'down')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll up' | translate}}">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</a>
|
||||
<a ng-click="active_projector.controlProjector('scroll', 'up')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Scroll down' | translate}}">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</a>
|
||||
<a ng-click="active_projector.controlProjector('scroll', 'reset')"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Reset scrolling' | translate}}">
|
||||
<i class="fa fa-undo"></i>
|
||||
</a>
|
||||
</div>
|
||||
<span ng-class="{ 'notNull': active_projector.scroll != 0 }">{{ active_projector.scroll }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- countdowns -->
|
||||
<div class="section" os-perms="core.can_manage_projector">
|
||||
<a href="#" ng-click="isCountdowns = !isCountdowns">
|
||||
<i class="fa toggle-icon" ng-class="isCountdowns ? 'fa-angle-up' : 'fa-angle-down'"></i>
|
||||
<h4 translate>Countdowns</h4>
|
||||
</a>
|
||||
<div uib-collapse="!isCountdowns" ng-cloak>
|
||||
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.id}}" class="countdown panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span ng-if="countdown.description">{{ countdown.description }}</span>
|
||||
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
|
||||
<!-- remove countdown button -->
|
||||
<button type="button" class="close"
|
||||
ng-click="removeCountdown(countdown)"
|
||||
title="{{ 'Remove countdown' | translate}}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<!-- edit countdown button -->
|
||||
<button type="button" class="close icons"
|
||||
ng-click="countdown.editFlag=true; countdown.new_description = countdown.description;"
|
||||
title="{{ 'Edit countdown' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<a ui-sref="core.countdown.detail({id: countdown.id})" class="close icons" target="_blank"
|
||||
uib-tooltip="{{ 'Open countdown in fullscreen' | translate }}">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': isProjected(countdown).length }">
|
||||
<projector-button model="countdown" default-projector-id="countdownDefaultProjectorId"></projector-button>
|
||||
|
||||
<!-- countdown controls -->
|
||||
<a class="btn btn-default vcenter"
|
||||
ng-click="countdown.reset()"
|
||||
ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
|
||||
title="{{ 'Reset countdown' | translate}}">
|
||||
<i class="fa fa-stop"></i>
|
||||
</a>
|
||||
<a ng-if="!countdown.running" class="btn btn-default vcenter"
|
||||
ng-click="countdown.start()"
|
||||
title="{{ 'Start' | translate}}">
|
||||
<i class="fa fa-play"></i>
|
||||
<i ng-if="countdown.running" class="fa fa-pause"></i>
|
||||
</a>
|
||||
<a ng-if="countdown.running" class="btn btn-default vcenter"
|
||||
ng-click="countdown.stop()"
|
||||
title="{{ 'Pause' | translate}}">
|
||||
<i class="fa fa-pause"></i>
|
||||
</a>
|
||||
<span ng-if="!countdown.editTime" class="countdown_timer vcenter"
|
||||
ng-class="{
|
||||
'negative': countdown.seconds <= 0,
|
||||
'warning_time': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||
{{ countdown.seconds | osSecondsToTime }}
|
||||
</span>
|
||||
<!-- edit countdown form -->
|
||||
<form ng-show="countdown.editFlag"
|
||||
ng-submit="editCountdown(countdown)">
|
||||
<div class="form-group">
|
||||
<label translate>Description</label>
|
||||
<input ng-model="countdown.new_description" type="text" class="form-control input-sm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Start time</label>
|
||||
<div class="input-group">
|
||||
<input data-ng-model="countdown.default_time" min-sec-format
|
||||
type="text" placeholder="mm:ss" class="form-control input-sm">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" title="{{ 'Save' | translate}}"
|
||||
class="btn btn-sm btn-primary">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button ng-click="countdown.editFlag=false;"
|
||||
title="{{ 'Cancel' | translate}}"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add countdown button -->
|
||||
<a ng-click="addCountdown()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Add countdown' | translate}}">
|
||||
<i class="fa fa-plus"></i> <translate>Add new countdown</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- messages -->
|
||||
<div class="section" os-perms="core.can_manage_projector">
|
||||
<a href="#" ng-click="isMessages = !isMessages">
|
||||
<i class="fa toggle-icon" ng-class="isMessages ? 'fa-angle-up' : 'fa-angle-down'"></i>
|
||||
<h4 translate>Messages</h4>
|
||||
</a>
|
||||
<div uib-collapse="!isMessages" ng-cloak>
|
||||
<div ng-repeat="message in messages" id="message{{message.id}}" class="message panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<span>{{ 'Message' | translate }} {{ $index + 1 }}</span>
|
||||
<!-- remove message button -->
|
||||
<button type="button" class="close"
|
||||
ng-click="removeMessage(message)"
|
||||
title="{{ 'Remove message' | translate}}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<button type="button" class="close editicon"
|
||||
ng-click="editMessage(message)"
|
||||
title="{{ 'Edit message' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': isProjected(message).length }">
|
||||
<projector-button model="message" default-projector-id="messageDefaultProjectorId"></projector-button>
|
||||
|
||||
<div class="innermessage" ng-bind-html="message.message | trusted"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add message button -->
|
||||
<a ng-click="addMessage()"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Add message' | translate}}">
|
||||
<i class="fa fa-plus"></i> <translate>Add new message</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- list of speakers overlay -->
|
||||
<div class="section" os-perms="core.can_manage_projector">
|
||||
<a href="#" ng-click="isSpeakerList = !isSpeakerList">
|
||||
<i class="fa toggle-icon" ng-class="isSpeakerList ? 'fa-angle-up' : 'fa-angle-down'"></i>
|
||||
<h4 translate>List of speakers</h4>
|
||||
</a>
|
||||
<div uib-collapse="!isSpeakerList" ng-cloak>
|
||||
|
||||
<!-- Current list of speakers projector button -->
|
||||
<div class="btn-group button" uib-dropdown
|
||||
uib-tooltip="{{ 'Project the current list of speakers' | translate }}"
|
||||
os-perms="core.can_manage_projector">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
ng-click="currentListOfSpeakers.project(listOfSpeakersDefaultProjectorId, currentListOfSpeakersAsOverlay)"
|
||||
ng-class="{ 'btn-primary': currentListOfSpeakers.isProjected().length && inArray(currentListOfSpeakers.isProjected(), listOfSpeakersDefaultProjectorId)}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
<translate>Current list of speakers</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm slimDropDown"
|
||||
ng-if="projectors.length > 1"
|
||||
uib-dropdown-toggle
|
||||
ng-class="{ 'btn-primary': currentListOfSpeakers.isProjected().length && !inArray(currentListOfSpeakers.isProjected(), listOfSpeakersDefaultProjectorId)}">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button" ng-if="projectors.length > 1">
|
||||
<li role="menuitem">
|
||||
<a href="" ng-click="setOverlay(false); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="currentListOfSpeakersAsOverlay ? 'fa-circle-o' : 'fa-check-circle-o'"></i>
|
||||
<translate>Project as slide</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="" ng-click="setOverlay(true); $event.stopPropagation();">
|
||||
<i class="fa" ng-class="currentListOfSpeakersAsOverlay ? 'fa-check-circle-o' : 'fa-circle-o'"></i>
|
||||
<translate>Project as overlay</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
|
||||
<a href="" ng-click="currentListOfSpeakers.project(projector.id, currentListOfSpeakersAsOverlay)"
|
||||
ng-class="{ 'projected': inArray(currentListOfSpeakers.isProjected(), projector.id) }">
|
||||
<i class="fa fa-video-camera" ng-show="inArray(currentListOfSpeakers.isProjected(), projector.id)"></i>
|
||||
{{ projector.name | translate }}
|
||||
<span ng-if="projector.id == listOfSpeakersDefaultProjectorId">(<translate>Default</translate>)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button os-perms="agenda.can_manage"
|
||||
ng-disabled="!currentListOfSpeakersItem()"
|
||||
ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"
|
||||
uib-tooltip="{{ 'Manage the current list of speakers' | translate}}">
|
||||
<i class="fa fa-microphone"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- end div ProjectorControlCtrl -->
|
||||
|
||||
</div>
|
@ -1,12 +0,0 @@
|
||||
<h1 translate>Edit message</h1>
|
||||
|
||||
<form name="messageForm" ng-submit="save(model)" novalidate>
|
||||
<formly-form model="model" fields="formFields">
|
||||
<button type="submit" ng-disabled="motionForm.$invalid" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</formly-form>
|
||||
</form>
|
@ -1,17 +0,0 @@
|
||||
<div class="form-group">
|
||||
<div ng-if="to.label">
|
||||
<label class="control-label">
|
||||
{{ to.label | translate }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<label ng-repeat="option in to.options" class="btn btn-default btn-sm"
|
||||
ng-class="{active: (model[options.key] === option.value)}"
|
||||
ng-disabled="option.disabled">
|
||||
<input type="radio" ng-value="option.value"
|
||||
ng-model="model[options.key]" ng-checked="model[options.key] === option.value"
|
||||
ng-disabled="option.disabled" ng-change="to.change(option.value)">
|
||||
{{ option.name | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
<!-- custom angular formly template for angular-chosen multiple form field -->
|
||||
<select multiple chosen
|
||||
data-ng-model="model[options.key]"
|
||||
ng-required="{{ to.required }}"
|
||||
ng-options="{{ to.ngOptions }}"
|
||||
search-contains="true"
|
||||
placeholder-text-multiple="'{{ to.placeholder }}'"
|
||||
no-results-text="'No results available ...' | translate"
|
||||
class="form-control">
|
||||
</select>
|
@ -1,12 +0,0 @@
|
||||
<!-- custom angular formly template for angular-chosen single form field -->
|
||||
<select chosen
|
||||
data-ng-model="model[options.key]"
|
||||
ng-required="{{ to.required }}"
|
||||
ng-options="{{ to.ngOptions }}"
|
||||
allow-single-deselect="true"
|
||||
search-contains="true"
|
||||
placeholder-text-single="'{{ to.placeholder }}'"
|
||||
no-results-text="'No results available ...' | translate"
|
||||
class="form-control">
|
||||
<option value=""></option>
|
||||
</select>
|
@ -1,4 +0,0 @@
|
||||
<div ng-controller="SlideClockCtrl" id="currentTime">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
{{ servertime | date:'HH:mm' }}
|
||||
</div>
|
@ -1,9 +0,0 @@
|
||||
<div ng-controller="SlideCountdownCtrl">
|
||||
<div class="countdown well pull-right"
|
||||
ng-class="{
|
||||
'negative': countdown.seconds <= 0,
|
||||
'warning_time': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||
{{ countdown.seconds | osSecondsToTime}}
|
||||
<div class="description">{{ countdown.description }}</div>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
<div ng-controller="SlideMessageCtrl">
|
||||
<div class="message_background"></div>
|
||||
<div class="message well" ng-class="{'identify': element.identify}" ng-bind-html="(element.identify ? identifyMessage : message.message) | trusted"></div>
|
||||
</div>
|