Use tinymce instead of CKEditor.
- better integration of tinymce in bower and gulp - Improve support for html tags in reportlab's motion pdf. - Now paste from word works without problems (That was the main reason of switching to tinymce: The data loss problem with MS Word is still unfixed in CKEditor, see https://dev.ckeditor.com/ticket/13174) - The editor is now used for customslides (text), motions (text, reason) and users (about). - Use mediafile image list for tinymce. - Use own repository for tinymce-i18n: OpenSlides/tinymce-i18n
This commit is contained in:
parent
ad7653fb76
commit
16f1ad5731
13
bower.json
13
bower.json
@ -27,15 +27,22 @@
|
|||||||
"js-data": "~2.8.2",
|
"js-data": "~2.8.2",
|
||||||
"js-data-angular": "~3.1.0",
|
"js-data-angular": "~3.1.0",
|
||||||
"ng-file-upload": "~11.2.3",
|
"ng-file-upload": "~11.2.3",
|
||||||
"ckeditor": "4.5.6",
|
"angular-ui-tinymce": "~0.0.13",
|
||||||
"angular-ckeditor": "~1.0.3",
|
|
||||||
"angular-pdf": "~1.3.0",
|
"angular-pdf": "~1.3.0",
|
||||||
"roboto-condensed": "~0.3.0",
|
"roboto-condensed": "~0.3.0",
|
||||||
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1"
|
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
||||||
|
"tinymce-i18n": "OpenSlides/tinymce-i18n"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"pdfjs-dist": {
|
"pdfjs-dist": {
|
||||||
"main": "build/pdf.combined.js"
|
"main": "build/pdf.combined.js"
|
||||||
|
},
|
||||||
|
"tinymce-dist": {
|
||||||
|
"main": [
|
||||||
|
"tinymce.js",
|
||||||
|
"themes/modern/theme.js",
|
||||||
|
"plugins/*/plugin.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
28
gulpfile.js
28
gulpfile.js
@ -25,6 +25,7 @@ var argv = require('yargs').argv,
|
|||||||
jshint = require('gulp-jshint'),
|
jshint = require('gulp-jshint'),
|
||||||
mainBowerFiles = require('main-bower-files'),
|
mainBowerFiles = require('main-bower-files'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
|
rename = require('gulp-rename'),
|
||||||
through = require('through2'),
|
through = require('through2'),
|
||||||
uglify = require('gulp-uglify'),
|
uglify = require('gulp-uglify'),
|
||||||
vsprintf = require('sprintf-js').vsprintf;
|
vsprintf = require('sprintf-js').vsprintf;
|
||||||
@ -67,10 +68,27 @@ gulp.task('fonts-libs', function() {
|
|||||||
.pipe(gulp.dest(path.join(output_directory, 'fonts')));
|
.pipe(gulp.dest(path.join(output_directory, 'fonts')));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extra task only for CKEditor
|
// Catches all skins files for TinyMCE editor.
|
||||||
gulp.task('ckeditor', function () {
|
gulp.task('tinymce-skins', function () {
|
||||||
return gulp.src(path.join('bower_components', 'ckeditor', '**'))
|
return gulp.src(path.join('bower_components', 'tinymce-dist', 'skins', '**'))
|
||||||
.pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
|
.pipe(gulp.dest(path.join(output_directory, 'tinymce', 'skins')));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Catches all required i18n files for TinyMCE editor.
|
||||||
|
gulp.task('tinymce-i18n', function () {
|
||||||
|
return gulp.src([
|
||||||
|
'bower_components/tinymce-i18n/langs/cs.js',
|
||||||
|
'bower_components/tinymce-i18n/langs/de.js',
|
||||||
|
'bower_components/tinymce-i18n/langs/es.js',
|
||||||
|
'bower_components/tinymce-i18n/langs/fr_FR.js',
|
||||||
|
'bower_components/tinymce-i18n/langs/pt_PT.js',
|
||||||
|
])
|
||||||
|
.pipe(rename(function (path) {
|
||||||
|
if (path.basename === 'pt_PT') {path.basename = 'pt'}
|
||||||
|
if (path.basename === 'fr_FR') {path.basename = 'fr'}
|
||||||
|
}))
|
||||||
|
.pipe(gulpif(argv.production, uglify()))
|
||||||
|
.pipe(gulp.dest(path.join(output_directory, 'tinymce', 'i18n')));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compiles translation files (*.po) to *.json and saves them in the directory
|
// Compiles translation files (*.po) to *.json and saves them in the directory
|
||||||
@ -84,7 +102,7 @@ gulp.task('translations', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Gulp default task. Runs all other tasks before.
|
// Gulp default task. Runs all other tasks before.
|
||||||
gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'translations'], function () {});
|
gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'tinymce-skins', 'tinymce-i18n', 'translations'], function () {});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,9 +75,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
'operator',
|
'operator',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
|
'CustomslideForm',
|
||||||
'AgendaTree',
|
'AgendaTree',
|
||||||
'Projector',
|
'Projector',
|
||||||
function($scope, $http, $state, DS, operator, ngDialog, Agenda, AgendaTree, Projector) {
|
function($scope, $http, $state, DS, operator, ngDialog, Agenda, CustomslideForm, AgendaTree, Projector) {
|
||||||
// Bind agenda tree to the scope
|
// Bind agenda tree to the scope
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
return Agenda.lastModified();
|
return Agenda.lastModified();
|
||||||
@ -110,11 +111,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
|
|||||||
};
|
};
|
||||||
// open new dialog
|
// open new dialog
|
||||||
$scope.newDialog = function () {
|
$scope.newDialog = function () {
|
||||||
ngDialog.open({
|
ngDialog.open(CustomslideForm.getDialog());
|
||||||
template: 'static/templates/core/customslide-form.html',
|
|
||||||
controller: 'CustomslideCreateCtrl',
|
|
||||||
className: 'ngdialog-theme-default wide-form'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
// open edit dialog
|
// open edit dialog
|
||||||
$scope.editDialog = function (item) {
|
$scope.editDialog = function (item) {
|
||||||
|
@ -424,34 +424,15 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
/* Options for CKEditor used in various create and edit views. */
|
// mark HTML as "trusted"
|
||||||
.value('CKEditorOptions', {
|
.filter('trusted', [
|
||||||
allowedContent:
|
'$sce',
|
||||||
'h1 h2 h3 p pre b i u strike strong em blockquote;' +
|
function ($sce) {
|
||||||
'a[!href];' +
|
return function(text) {
|
||||||
'img[!src,alt]{width,height,float};' +
|
return $sce.trustAsHtml(text);
|
||||||
'table tr th td caption;' +
|
};
|
||||||
'li ol ul{list-style};' +
|
}
|
||||||
'span{color,background-color};',
|
])
|
||||||
extraPlugins: 'colorbutton',
|
|
||||||
toolbarGroups: [
|
|
||||||
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
|
|
||||||
{ name: 'editing', groups: [ 'find', 'selection', 'spellchecker', 'editing' ] },
|
|
||||||
{ name: 'forms', groups: [ 'forms' ] },
|
|
||||||
{ name: 'tools', groups: [ 'tools' ] },
|
|
||||||
{ name: 'about', groups: [ 'about' ] },
|
|
||||||
{ name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
|
|
||||||
{ name: 'others', groups: [ 'others' ] },
|
|
||||||
'/',
|
|
||||||
{ name: 'styles', groups: [ 'styles' ] },
|
|
||||||
{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
|
|
||||||
{ name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi', 'paragraph' ] },
|
|
||||||
{ name: 'links', groups: [ 'links' ] },
|
|
||||||
{ name: 'insert', groups: [ 'insert' ] },
|
|
||||||
{ name: 'colors', groups: [ 'colors' ] }
|
|
||||||
],
|
|
||||||
removeButtons: 'Anchor,SpecialChar,Subscript,Superscript,Styles,RemoveFormat,HorizontalRule'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make sure that the DS factories are loaded by making them a dependency
|
// Make sure that the DS factories are loaded by making them a dependency
|
||||||
.run([
|
.run([
|
||||||
|
@ -14,8 +14,8 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
'ngMessages',
|
'ngMessages',
|
||||||
'ngCsvImport',
|
'ngCsvImport',
|
||||||
'ui.select',
|
'ui.select',
|
||||||
|
'ui.tinymce',
|
||||||
'luegg.directives',
|
'luegg.directives',
|
||||||
'ckeditor',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// Provider to register entries for the main menu.
|
// Provider to register entries for the main menu.
|
||||||
@ -337,10 +337,16 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
.run([
|
.run([
|
||||||
'formlyConfig',
|
'formlyConfig',
|
||||||
function (formlyConfig) {
|
function (formlyConfig) {
|
||||||
// NOTE: This next line is highly recommended. Otherwise Chrome's autocomplete will appear over your options!
|
// NOTE: This next line is highly recommended. Otherwise Chrome's autocomplete
|
||||||
|
// will appear over your options!
|
||||||
formlyConfig.extras.removeChromeAutoComplete = true;
|
formlyConfig.extras.removeChromeAutoComplete = true;
|
||||||
|
|
||||||
// Configure custom types
|
// Configure custom types
|
||||||
|
formlyConfig.setType({
|
||||||
|
name: 'editor',
|
||||||
|
extends: 'textarea',
|
||||||
|
templateUrl: 'static/templates/core/editor.html',
|
||||||
|
});
|
||||||
formlyConfig.setType({
|
formlyConfig.setType({
|
||||||
name: 'ui-select-single',
|
name: 'ui-select-single',
|
||||||
extends: 'select',
|
extends: 'select',
|
||||||
@ -354,6 +360,35 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Options for TinyMCE editor used in various create and edit views.
|
||||||
|
.factory('Editor', [
|
||||||
|
'gettextCatalog',
|
||||||
|
function (gettextCatalog) {
|
||||||
|
return {
|
||||||
|
getOptions: function (images) {
|
||||||
|
return {
|
||||||
|
language_url: '/static/tinymce/i18n/' + gettextCatalog.getCurrentLanguage() + '.js',
|
||||||
|
theme_url: '/static/js/openslides-libs.js',
|
||||||
|
skin_url: '/static/tinymce/skins/lightgray/',
|
||||||
|
inline: false,
|
||||||
|
statusbar: false,
|
||||||
|
browser_spellcheck: true,
|
||||||
|
image_advtab: true,
|
||||||
|
image_list: images,
|
||||||
|
plugins: [
|
||||||
|
'lists link autolink charmap preview searchreplace code fullscreen',
|
||||||
|
'paste textcolor colorpicker image imagetools'
|
||||||
|
],
|
||||||
|
menubar: '',
|
||||||
|
toolbar: 'undo redo searchreplace | styleselect | bold italic underline strikethrough ' +
|
||||||
|
'forecolor backcolor removeformat | bullist numlist | outdent indent | ' +
|
||||||
|
'link image charmap table | code preview fullscreen'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// html-tag os-form-field to generate generic from fields
|
// html-tag os-form-field to generate generic from fields
|
||||||
// TODO: make it possible to use other fields then config fields
|
// TODO: make it possible to use other fields then config fields
|
||||||
.directive('osFormField', [
|
.directive('osFormField', [
|
||||||
@ -504,17 +539,19 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
// Provide generic customslide form fields for create and update view
|
// Provide generic customslide form fields for create and update view
|
||||||
.factory('CustomslideForm', [
|
.factory('CustomslideForm', [
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
'CKEditorOptions',
|
'Editor',
|
||||||
'Mediafile',
|
'Mediafile',
|
||||||
function (gettextCatalog, CKEditorOptions, Mediafile) {
|
function (gettextCatalog, Editor, Mediafile) {
|
||||||
return {
|
return {
|
||||||
// ngDialog for customslide form
|
// ngDialog for customslide form
|
||||||
getDialog: function (customslide) {
|
getDialog: function (customslide) {
|
||||||
|
var resolve = {};
|
||||||
if (customslide) {
|
if (customslide) {
|
||||||
var resolve = {
|
resolve = {
|
||||||
customslide: function(Customslide) {return Customslide.find(customslide.id);}
|
customslide: function(Customslide) {return Customslide.find(customslide.id);}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();}
|
||||||
return {
|
return {
|
||||||
template: 'static/templates/core/customslide-form.html',
|
template: 'static/templates/core/customslide-form.html',
|
||||||
controller: (customslide) ? 'CustomslideUpdateCtrl' : 'CustomslideCreateCtrl',
|
controller: (customslide) ? 'CustomslideUpdateCtrl' : 'CustomslideCreateCtrl',
|
||||||
@ -525,6 +562,7 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFormFields: function () {
|
getFormFields: function () {
|
||||||
|
var images = Mediafile.getAllImages();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
@ -536,11 +574,13 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'text',
|
key: 'text',
|
||||||
type: 'textarea',
|
type: 'editor',
|
||||||
templateOptions: {
|
templateOptions: {
|
||||||
label: gettextCatalog.getString('Text')
|
label: gettextCatalog.getString('Text')
|
||||||
},
|
},
|
||||||
ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
|
data: {
|
||||||
|
tinymceOption: Editor.getOptions(images)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'attachments_id',
|
key: 'attachments_id',
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div ng-bind-html="customslide.text"></div>
|
<div ng-bind-html="customslide.text | trusted"></div>
|
||||||
<h3 ng-if="customslide.attachments.length > 0" translate>Attachments</h3>
|
<h3 ng-if="customslide.attachments.length > 0" translate>Attachments</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li ng-repeat="attachment in customslide.attachments">
|
<li ng-repeat="attachment in customslide.attachments">
|
||||||
|
2
openslides/core/static/templates/core/editor.html
Normal file
2
openslides/core/static/templates/core/editor.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<!-- custom angular formly template for tinymce textarea field -->
|
||||||
|
<textarea ui-tinymce="options.data.tinymceOption" ng-model="model[options.key]" class="form-control"></textarea>
|
@ -1,4 +1,4 @@
|
|||||||
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
|
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
|
||||||
<h1>{{ customslide.agenda_item.getTitle() }}</h1>
|
<h1>{{ customslide.agenda_item.getTitle() }}</h1>
|
||||||
<div ng-bind-html="customslide.text"></div>
|
<div ng-bind-html="customslide.text | trusted"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
<link rel="stylesheet" href="static/css/app.css">
|
<link rel="stylesheet" href="static/css/app.css">
|
||||||
<link rel="icon" href="/static/img/favicon.png">
|
<link rel="icon" href="/static/img/favicon.png">
|
||||||
<script src="static/js/openslides-libs.js"></script>
|
<script src="static/js/openslides-libs.js"></script>
|
||||||
<script src="static/ckeditor/ckeditor.js"></script>
|
|
||||||
|
|
||||||
<div id="wrapper" ng-cloak>
|
<div id="wrapper" ng-cloak>
|
||||||
|
|
||||||
|
@ -12,6 +12,15 @@ angular.module('OpenSlidesApp.mediafiles', [])
|
|||||||
return DS.defineResource({
|
return DS.defineResource({
|
||||||
name: name,
|
name: name,
|
||||||
useClass: jsDataModel,
|
useClass: jsDataModel,
|
||||||
|
getAllImages: function () {
|
||||||
|
var images = []
|
||||||
|
angular.forEach(this.getAll(), function(file) {
|
||||||
|
if (file.is_image) {
|
||||||
|
images.push({title: file.title, value: file.mediafileUrl});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return images;
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getResourceName: function () {
|
getResourceName: function () {
|
||||||
return name;
|
return name;
|
||||||
@ -30,6 +39,10 @@ angular.module('OpenSlidesApp.mediafiles', [])
|
|||||||
var PRESENTABLE_FILE_TYPES = ['application/pdf'];
|
var PRESENTABLE_FILE_TYPES = ['application/pdf'];
|
||||||
return _.contains(PRESENTABLE_FILE_TYPES, filetype);
|
return _.contains(PRESENTABLE_FILE_TYPES, filetype);
|
||||||
}],
|
}],
|
||||||
|
is_image: ['filetype', function (filetype) {
|
||||||
|
var IMAGE_FILE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||||
|
return _.contains(IMAGE_FILE_TYPES, filetype);
|
||||||
|
}],
|
||||||
mediafileUrl: [function () {
|
mediafileUrl: [function () {
|
||||||
return this.media_url_prefix + this.mediafile.name;
|
return this.media_url_prefix + this.mediafile.name;
|
||||||
}],
|
}],
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import random
|
import random
|
||||||
|
import re
|
||||||
from html import escape
|
from html import escape
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
@ -153,6 +154,23 @@ def motion_to_pdf(pdf, motion):
|
|||||||
def convert_html_to_reportlab(pdf, text):
|
def convert_html_to_reportlab(pdf, text):
|
||||||
# parsing and replacing not supported html tags for reportlab...
|
# parsing and replacing not supported html tags for reportlab...
|
||||||
soup = BeautifulSoup(text, "html5lib")
|
soup = BeautifulSoup(text, "html5lib")
|
||||||
|
|
||||||
|
# number ol list elements
|
||||||
|
ols = soup.find_all('ol')
|
||||||
|
for ol in ols:
|
||||||
|
counter = 0
|
||||||
|
for li in ol.children:
|
||||||
|
if li.name == 'li':
|
||||||
|
# if start attribute is available set counter for first list element
|
||||||
|
if li.parent.get('start') and not li.find_previous_sibling():
|
||||||
|
counter = int(ol.get('start'))
|
||||||
|
else:
|
||||||
|
counter += 1
|
||||||
|
if li.get('value'):
|
||||||
|
counter = li.get('value')
|
||||||
|
else:
|
||||||
|
li['value'] = counter
|
||||||
|
|
||||||
# read all list elements...
|
# read all list elements...
|
||||||
for element in soup.find_all('li'):
|
for element in soup.find_all('li'):
|
||||||
# ... and replace ul list elements with <para><bullet>•</bullet>...<para>
|
# ... and replace ul list elements with <para><bullet>•</bullet>...<para>
|
||||||
@ -167,22 +185,32 @@ def convert_html_to_reportlab(pdf, text):
|
|||||||
bullet_tag = soup.new_tag("bullet")
|
bullet_tag = soup.new_tag("bullet")
|
||||||
bullet_tag.string = u"•"
|
bullet_tag.string = u"•"
|
||||||
element.insert(0, bullet_tag)
|
element.insert(0, bullet_tag)
|
||||||
# ... and replace ol list elements with <para><bullet><seq id="%id"></seq>.</bullet>...</para>
|
# ... and replace ol list elements with <para><bullet><seqreset id="%id" base="value"><seq id="%id"></seq>.</bullet>...</para>
|
||||||
if element.parent.name == "ol":
|
if element.parent.name == "ol":
|
||||||
|
counter = None
|
||||||
# set list id if element is the first of numbered list
|
# set list id if element is the first of numbered list
|
||||||
if not element.find_previous_sibling():
|
if not element.find_previous_sibling():
|
||||||
id = random.randrange(0, 101)
|
id = random.randrange(0, 101)
|
||||||
|
if element.parent.get('start'):
|
||||||
|
counter = element.parent.get('start')
|
||||||
|
if element.get('value'):
|
||||||
|
counter = element.get('value')
|
||||||
# nested lists
|
# nested lists
|
||||||
if element.ul or element.ol:
|
if element.ul or element.ol:
|
||||||
for i in element.find_all('li'):
|
nested_list = element.find_all('li')
|
||||||
element.insert_before(i)
|
for i in reversed(nested_list):
|
||||||
element.clear()
|
element.insert_after(i)
|
||||||
else:
|
|
||||||
element.name = "para"
|
element.attrs = {}
|
||||||
element.insert(0, soup.new_tag("bullet"))
|
element.name = "para"
|
||||||
element.bullet.insert(0, soup.new_tag("seq"))
|
element.insert(0, soup.new_tag("bullet"))
|
||||||
element.bullet.seq['id'] = id
|
element.bullet.insert(0, soup.new_tag("seq"))
|
||||||
element.bullet.insert(1, ".")
|
element.bullet.seq['id'] = id
|
||||||
|
if counter:
|
||||||
|
element.bullet.insert(0, soup.new_tag("seqreset"))
|
||||||
|
element.bullet.seqreset['id'] = id
|
||||||
|
element.bullet.seqreset['base'] = int(counter) - 1
|
||||||
|
element.bullet.insert(2, ".")
|
||||||
# remove tags which are not supported by reportlab (replace tags with their children tags)
|
# remove tags which are not supported by reportlab (replace tags with their children tags)
|
||||||
for tag in soup.find_all('ul'):
|
for tag in soup.find_all('ul'):
|
||||||
tag.unwrap()
|
tag.unwrap()
|
||||||
@ -190,8 +218,60 @@ def convert_html_to_reportlab(pdf, text):
|
|||||||
tag.unwrap()
|
tag.unwrap()
|
||||||
for tag in soup.find_all('li'):
|
for tag in soup.find_all('li'):
|
||||||
tag.unwrap()
|
tag.unwrap()
|
||||||
|
|
||||||
|
# use tags which are supported by reportlab
|
||||||
|
# replace <s> to <strike>
|
||||||
|
for tag in soup.find_all('s'):
|
||||||
|
tag.name = "strike"
|
||||||
|
|
||||||
|
# replace <del> to <strike>
|
||||||
|
for tag in soup.find_all('del'):
|
||||||
|
tag.name = "strike"
|
||||||
|
|
||||||
|
for tag in soup.find_all('a'):
|
||||||
|
# remove a tags without href attribute
|
||||||
|
if not tag.get('href'):
|
||||||
|
tag.extract()
|
||||||
|
for tag in soup.find_all('img'):
|
||||||
|
# remove img tags without src attribute
|
||||||
|
if not tag.get('src'):
|
||||||
|
tag.extract()
|
||||||
|
|
||||||
|
# replace style attributes in <span> tags
|
||||||
for tag in soup.find_all('span'):
|
for tag in soup.find_all('span'):
|
||||||
tag.unwrap()
|
if tag.get('style'):
|
||||||
|
# replace style attribute "text-decoration: line-through;" to <strike> tag
|
||||||
|
if 'text-decoration: line-through' in str(tag['style']):
|
||||||
|
strike_tag = soup.new_tag("strike")
|
||||||
|
strike_tag.string = tag.string
|
||||||
|
tag.replace_with(strike_tag)
|
||||||
|
# replace style attribute "text-decoration: underline;" to <u> tag
|
||||||
|
elif 'text-decoration: underline' in str(tag['style']):
|
||||||
|
u_tag = soup.new_tag("u")
|
||||||
|
u_tag.string = tag.string
|
||||||
|
tag.replace_with(u_tag)
|
||||||
|
# replace style attribute "color: #xxxxxx;" to "<font backcolor='#xxxxxx'>...</font>"
|
||||||
|
elif 'background-color: ' in str(tag['style']):
|
||||||
|
font_tag = soup.new_tag("font")
|
||||||
|
color = re.findall('background-color: (.*?);', str(tag['style']))
|
||||||
|
if color:
|
||||||
|
font_tag['backcolor'] = color
|
||||||
|
if tag.string:
|
||||||
|
font_tag.string = tag.string
|
||||||
|
tag.replace_with(font_tag)
|
||||||
|
# replace style attribute "color: #xxxxxx;" to "<font color='#xxxxxx'>...</font>"
|
||||||
|
elif 'color: ' in str(tag['style']):
|
||||||
|
font_tag = soup.new_tag("font")
|
||||||
|
color = re.findall('color: (.*?);', str(tag['style']))
|
||||||
|
if color:
|
||||||
|
font_tag['color'] = color
|
||||||
|
if tag.string:
|
||||||
|
font_tag.string = tag.string
|
||||||
|
tag.replace_with(font_tag)
|
||||||
|
else:
|
||||||
|
tag.unwrap()
|
||||||
|
else:
|
||||||
|
tag.unwrap()
|
||||||
# print paragraphs with numbers
|
# print paragraphs with numbers
|
||||||
text = soup.body.contents
|
text = soup.body.contents
|
||||||
paragraph_number = 1
|
paragraph_number = 1
|
||||||
|
@ -152,18 +152,20 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
.factory('MotionForm', [
|
.factory('MotionForm', [
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
'operator',
|
'operator',
|
||||||
|
'Editor',
|
||||||
'Category',
|
'Category',
|
||||||
'Config',
|
'Config',
|
||||||
'Mediafile',
|
'Mediafile',
|
||||||
'Tag',
|
'Tag',
|
||||||
'User',
|
'User',
|
||||||
'Workflow',
|
'Workflow',
|
||||||
function (gettextCatalog, operator, Category, Config, Mediafile, Tag, User, Workflow) {
|
function (gettextCatalog, operator, Editor, Category, Config, Mediafile, Tag, User, Workflow) {
|
||||||
return {
|
return {
|
||||||
// ngDialog for motion form
|
// ngDialog for motion form
|
||||||
getDialog: function (motion) {
|
getDialog: function (motion) {
|
||||||
|
var resolve = {}
|
||||||
if (motion) {
|
if (motion) {
|
||||||
var resolve = {
|
resolve = {
|
||||||
motion: function() {
|
motion: function() {
|
||||||
return motion;
|
return motion;
|
||||||
},
|
},
|
||||||
@ -172,6 +174,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();}
|
||||||
return {
|
return {
|
||||||
template: 'static/templates/motions/motion-form.html',
|
template: 'static/templates/motions/motion-form.html',
|
||||||
controller: (motion) ? 'MotionUpdateCtrl' : 'MotionCreateCtrl',
|
controller: (motion) ? 'MotionUpdateCtrl' : 'MotionCreateCtrl',
|
||||||
@ -187,6 +190,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
angular.forEach(workflows, function(workflow) {
|
angular.forEach(workflows, function(workflow) {
|
||||||
workflow.name = gettextCatalog.getString(workflow.name);
|
workflow.name = gettextCatalog.getString(workflow.name);
|
||||||
});
|
});
|
||||||
|
var images = Mediafile.getAllImages();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'identifier',
|
key: 'identifier',
|
||||||
@ -220,20 +224,24 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'text',
|
key: 'text',
|
||||||
type: 'textarea',
|
type: 'editor',
|
||||||
templateOptions: {
|
templateOptions: {
|
||||||
label: gettextCatalog.getString('Text'),
|
label: gettextCatalog.getString('Text'),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
|
data: {
|
||||||
|
tinymceOption: Editor.getOptions(images)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'reason',
|
key: 'reason',
|
||||||
type: 'textarea',
|
type: 'editor',
|
||||||
templateOptions: {
|
templateOptions: {
|
||||||
label: gettextCatalog.getString('Reason')
|
label: gettextCatalog.getString('Reason'),
|
||||||
},
|
},
|
||||||
ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
|
data: {
|
||||||
|
tinymceOption: Editor.getOptions(images)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'disable_versioning',
|
key: 'disable_versioning',
|
||||||
|
@ -218,12 +218,12 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<h3 translate>Text</h3>
|
<h3 translate>Text</h3>
|
||||||
<div ng-bind-html="motion.getText(version)"></div>
|
<div ng-bind-html="motion.getText(version) | trusted"></div>
|
||||||
|
|
||||||
<!-- reason -->
|
<!-- reason -->
|
||||||
<div ng-if="motion.getReason(version) != ''">
|
<div ng-if="motion.getReason(version) != ''">
|
||||||
<h3 translate>Reason</h3>
|
<h3 translate>Reason</h3>
|
||||||
<div ng-bind-html="motion.getReason()"></div>
|
<div ng-bind-html="motion.getReason() | trusted"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- attachments -->
|
<!-- attachments -->
|
||||||
|
@ -15,4 +15,3 @@
|
|||||||
</button>
|
</button>
|
||||||
</formly-form>
|
</formly-form>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -62,9 +62,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text -->
|
<!-- Text -->
|
||||||
<div ng-bind-html="motion.getText()"></div>
|
<div ng-bind-html="motion.getText() | trusted"></div>
|
||||||
|
|
||||||
<!-- Reason -->
|
<!-- Reason -->
|
||||||
<h3 ng-if="motion.getReason()" translate>Reason</h3>
|
<h3 ng-if="motion.getReason()" translate>Reason</h3>
|
||||||
<div ng-bind-html="motion.getReason()"></div>
|
<div ng-bind-html="motion.getReason() | trusted"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -235,8 +235,10 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
|
|||||||
.factory('UserForm', [
|
.factory('UserForm', [
|
||||||
'$http',
|
'$http',
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
|
'Editor',
|
||||||
'Group',
|
'Group',
|
||||||
function ($http, gettextCatalog, Group) {
|
'Mediafile',
|
||||||
|
function ($http, gettextCatalog, Editor, Group, Mediafile) {
|
||||||
return {
|
return {
|
||||||
// ngDialog for user form
|
// ngDialog for user form
|
||||||
getDialog: function (user) {
|
getDialog: function (user) {
|
||||||
@ -256,6 +258,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
|
|||||||
},
|
},
|
||||||
// angular-formly fields for user form
|
// angular-formly fields for user form
|
||||||
getFormFields: function (hideOnCreateForm) {
|
getFormFields: function (hideOnCreateForm) {
|
||||||
|
var images = Mediafile.getAllImages();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'username',
|
key: 'username',
|
||||||
@ -334,12 +337,13 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'about_me',
|
key: 'about_me',
|
||||||
type: 'textarea',
|
type: 'editor',
|
||||||
templateOptions: {
|
templateOptions: {
|
||||||
label: gettextCatalog.getString('About me'),
|
label: gettextCatalog.getString('About me'),
|
||||||
description: gettextCatalog.getString('Profile text.')
|
|
||||||
},
|
},
|
||||||
ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
|
data: {
|
||||||
|
tinymceOption: Editor.getOptions(images)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'is_present',
|
key: 'is_present',
|
||||||
@ -549,10 +553,12 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
|
|||||||
.controller('UserProfileCtrl', [
|
.controller('UserProfileCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$state',
|
'$state',
|
||||||
|
'Editor',
|
||||||
'User',
|
'User',
|
||||||
'user',
|
'user',
|
||||||
function($scope, $state, User, user) {
|
function($scope, $state, Editor, User, user) {
|
||||||
$scope.user = user; // autoupdate is not activated
|
$scope.user = user; // autoupdate is not activated
|
||||||
|
$scope.tinymceOption = Editor.getOptions();
|
||||||
$scope.save = function (user) {
|
$scope.save = function (user) {
|
||||||
User.save(user, { method: 'PATCH' }).then(
|
User.save(user, { method: 'PATCH' }).then(
|
||||||
function(success) {
|
function(success) {
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="textAbout" translate>About me</label>
|
<label for="textAbout" translate>About me</label>
|
||||||
<textarea ng-model="user.about_me" class="form-control" name="textAbout" />
|
<textarea ng-model="user.about_me" ui-tinymce="tinymceOption" class="form-control" name="textAbout" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" ng-click="save(user)" class="btn btn-primary" translate>
|
<button type="submit" ng-click="save(user)" class="btn btn-primary" translate>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"gulp-cssnano": "~2.1.0",
|
"gulp-cssnano": "~2.1.0",
|
||||||
"gulp-if": "~2.0.0",
|
"gulp-if": "~2.0.0",
|
||||||
"gulp-jshint": "~2.0.0",
|
"gulp-jshint": "~2.0.0",
|
||||||
|
"gulp-rename": "~1.2.2",
|
||||||
"gulp-uglify": "~1.5.2",
|
"gulp-uglify": "~1.5.2",
|
||||||
"main-bower-files": "~2.11.1",
|
"main-bower-files": "~2.11.1",
|
||||||
"po2json": "~0.4.1",
|
"po2json": "~0.4.1",
|
||||||
|
Loading…
Reference in New Issue
Block a user