Custom fonts for pdf and projector

This commit is contained in:
FinnStutzenstein 2018-01-30 16:12:02 +01:00
parent 8042beda60
commit dfb40684ee
21 changed files with 493 additions and 119 deletions

View File

@ -112,6 +112,7 @@ Core:
- Set default of projector resolution to 1220x915 [#2549].
- Preparations for the SAML plugin; Fixed caching of main views [#3535].
- Removed unnecessary OPTIONS request in config [#3541].
- Added possibility to upload custom fonts for projector and pdf [#3568].
Mediafiles:
- Fixed reloading of PDF on page change [#3274].

View File

@ -80,9 +80,9 @@ gulp.task('pdf-worker', function () {
gulp.task('pdf-worker-libs', function () {
return gulp.src([
path.join('bower_components', 'pdfmake', 'build', 'pdfmake.min.js'),
path.join('bower_components', 'pdfmake', 'build', 'vfs_fonts.js'),
])
.pipe(concat('pdf-worker-libs.js'))
.pipe(gulpif(argv.production, uglify()))
.pipe(rename('pdf-worker-libs.js'))
.pipe(gulp.dest(path.join(output_directory, 'js', 'workers')));
});

View File

@ -19,7 +19,7 @@ INPUT_TYPE_MAPPING = {
'colorpicker': str,
'datetimepicker': int,
'majorityMethod': str,
'logo': dict,
'static': dict,
'translations': list,
}
@ -125,9 +125,9 @@ class ConfigHandler:
valuecopy[id] = commentsfield
value = valuecopy
if config_variable.input_type == 'logo':
if config_variable.input_type == 'static':
if not isinstance(value, dict):
raise ConfigError(_('logo has to be a dict.'))
raise ConfigError(_('This has to be a dict.'))
whitelist = (
'path',
'display_name',

View File

@ -259,7 +259,7 @@ def get_config_variables():
default_value={
'display_name': 'Projector logo',
'path': ''},
input_type='logo',
input_type='static',
weight=301,
group='Logo',
hidden=True)
@ -269,7 +269,7 @@ def get_config_variables():
default_value={
'display_name': 'Projector header image',
'path': ''},
input_type='logo',
input_type='static',
weight=302,
group='Logo',
hidden=True)
@ -279,7 +279,7 @@ def get_config_variables():
default_value={
'display_name': 'Web interface header logo',
'path': ''},
input_type='logo',
input_type='static',
weight=303,
group='Logo',
hidden=True)
@ -290,7 +290,7 @@ def get_config_variables():
default_value={
'display_name': 'PDF header logo',
'path': ''},
input_type='logo',
input_type='static',
weight=310,
group='Logo',
hidden=True)
@ -300,7 +300,7 @@ def get_config_variables():
default_value={
'display_name': 'PDF footer logo',
'path': ''},
input_type='logo',
input_type='static',
weight=311,
group='Logo',
hidden=True)
@ -310,11 +310,67 @@ def get_config_variables():
default_value={
'display_name': 'PDF ballot paper logo',
'path': ''},
input_type='logo',
input_type='static',
weight=312,
group='Logo',
hidden=True)
# Fonts
yield ConfigVariable(
name='fonts_available',
default_value=[
'font_regular',
'font_italic',
'font_bold',
'font_bold_italic'],
weight=320,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_regular',
default_value={
'display_name': 'Font regular',
'default': 'static/fonts/Roboto-Regular.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_italic',
default_value={
'display_name': 'Font italic',
'default': 'static/fonts/Roboto-Medium.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_bold',
default_value={
'display_name': 'Font bold',
'default': 'static/fonts/Roboto-Condensed-Regular.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
yield ConfigVariable(
name='font_bold_italic',
default_value={
'display_name': 'Font bold italic',
'default': 'static/fonts/Roboto-Condensed-Light.woff',
'path': ''},
input_type='static',
weight=321,
group='Font',
hidden=True)
# Custom translations
yield ConfigVariable(
name='translations',

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-01-30 13:00
from __future__ import unicode_literals
from django.contrib.auth.models import Permission
from django.db import migrations
def delete_old_logo_permission(apps, schema_editor):
"""
Deletes the old 'can_manage_logo' permission which is replaced with
'can_manage_logos_and_fonts'. If this is a fresh database, no permission
will be deleted, in fact the old permission does not exist. Django creates
the permission after all migration and the old one is not generated.
If this is an old database, the new permission will be created and the old
one deleted. Also it will be assigned to the groups, which had the old permission.
"""
perm = Permission.objects.filter(codename='can_manage_logos')
if len(perm):
perm = perm.get()
# Save content_type for manual creation of new permissions.
content_type = perm.content_type
# Save groups. list() is necessary to evaluate the database query right now.
groups = list(perm.group_set.all())
# Delete permission
perm.delete()
# Create new permission
perm = Permission.objects.create(
codename='can_manage_logos_and_fonts',
name='Can manage logos and fonts',
content_type=content_type)
for group in groups:
group.permissions.add(perm)
group.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20180123_0903'),
]
operations = [
migrations.AlterModelOptions(
name='configstore',
options={
'default_permissions': (),
'permissions': (
('can_manage_config', 'Can manage configuration'),
('can_manage_logos_and_fonts', 'Can manage logos and fonts')
)
},
),
migrations.RunPython(
delete_old_logo_permission
),
]

View File

@ -281,7 +281,7 @@ class ConfigStore(RESTModelMixin, models.Model):
default_permissions = ()
permissions = (
('can_manage_config', 'Can manage configuration'),
('can_manage_logos', 'Can manage logos'))
('can_manage_logos_and_fonts', 'Can manage logos and fonts'))
@classmethod
def get_collection_string(cls):

View File

@ -3,22 +3,22 @@
*/
@font-face {
font-family: $font;
src: $font-src;
font-weight: 400;
font-style: normal;
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-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-family: $font-condensed;
src: $font-condensed-src;
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: $font-condensed-light;

View File

@ -1,9 +1,10 @@
/** Fonts **/
$font: 'Roboto';
/* 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: 'Roboto Medium';
$font-medium: 'OSFont Medium';
$font-medium-src: url('../fonts/Roboto-Medium.woff') format('woff');
$font-condensed: 'Roboto Condensed';
$font-condensed: 'OSFont Condensed';
$font-condensed-src: url('../fonts/Roboto-Condensed-Regular.woff') format('woff');
$font-condensed-light: 'Roboto Condensed Light';
$font-condensed-light: 'OSFont Condensed Light';
$font-condensed-light-src: url('../fonts/Roboto-Condensed-Light.woff') format('woff');

View File

@ -2,7 +2,6 @@
/* General */
@import "variables";
@import "fonts";
@import "helper";
@import "ui-override";

View File

@ -15,4 +15,3 @@
@import "../../../motions/static/css/motions/site";
@import "../../../users/static/css/users/site";
@import "../../../mediafiles/static/css/mediafiles/site";

View File

@ -826,36 +826,78 @@ angular.module('OpenSlidesApp.core', [
getAll: function () {
var self = this;
return _.map(this.getKeys(), function (key) {
return self.getFromKey(key);
return self.get(key);
});
},
getFromKey: function (key) {
get: function (key) {
var config = Config.get(key);
if (config) {
config.value.key = key;
return config.value;
}
},
isMediafileUsedAsLogo: function (mediafile) {
return _.find(this.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
},
canMediafileBeUsedAsLogo: function (mediafile) {
return mediafile.is_image;
},
setMediafile: function (key, mediafile) {
set: function (key, path) {
var config = Config.get(key);
if (!mediafile || mediafile.canBeUsedAsLogo()) {
config.value.path = mediafile ? mediafile.mediafileUrl : '';
if (config) {
config.value.path = path;// ? mediafile.mediafileUrl : '';
Config.save(key);
}
},
getLogosForMediafile: function (mediafile) {
return _.filter(this.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
};
}
])
.factory('Fonts', [
'Config',
'gettext',
function (Config, gettext) {
var extensionFormatMap = {
'ttf': 'truetype',
'woff': 'woff',
};
return {
getKeys: function () {
return Config.get('fonts_available').value;
},
getAll: function () {
var self = this;
return _.map(this.getKeys(), function (key) {
return self.get(key);
});
},
get: function (key) {
var config = Config.get(key);
if (config) {
config.value.key = key;
return config.value;
}
},
getUrl: function (key) {
var font = this.get(key);
if (font) {
var path = font.path;
if (!path) {
return font.default;
}
return path;
}
},
getForCss: function (key) {
var url = this.getUrl(key);
if (url) {
var ext = _.last(url.split('.'));
return "url('" + url + "') format('" +
extensionFormatMap[ext] + "')";
}
},
set: function (key, path) {
var config = Config.get(key);
if (config) {
config.value.path = path;
Config.save(key);
}
},
};
}
])

View File

@ -10,20 +10,18 @@ var document = {
};
var window = this;
// PdfMake and Fonts
// PdfMake
importScripts('/static/js/workers/pdf-worker-libs.js');
// Set default font family.
// To use custom ttf font files you have to replace the vfs_fonts.js file.
// See https://github.com/pdfmake/pdfmake/wiki/Custom-Fonts---client-side
// "PdfFont" is used as generic name in core/pdf.js. Adjust the four
// font style names only.
// "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: 'Roboto-Regular.ttf',
bold: 'Roboto-Medium.ttf',
italics: 'Roboto-Italic.ttf',
bolditalics: 'Roboto-Italic.ttf'
normal: 'OSFont-regular.ttf',
bold: 'OSFont-bold.ttf',
italics: 'OSFont-italic.ttf',
bolditalics: 'OSFont-bold_italic.ttf'
}
};
@ -106,8 +104,9 @@ var replaceFooter = function (doc) {
// Create PDF on message and return the base64 decoded document
self.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
var doc = data.pdfDocument;
pdfMake.vfs = data.vfs; // Set custom fonts.
var doc = data.pdfDocument;
replaceFooter(doc);
replacePlaceholder(doc.content);

View File

@ -1061,13 +1061,99 @@ angular.module('OpenSlidesApp.core.pdf', [])
}
])
// Creates the virtual filesystem for PdfMake.
.factory('PdfVfs', [
'$q',
'$http',
'Fonts',
'Config',
function ($q, $http, Fonts, Config) {
var urlCache = {}; // Caches the get request. Maps urls to base64 data ready to use.
var loadFont = function (url) {
return $q(function (resolve, reject) {
// Get font
return $http.get(url, {responseType: 'blob'}).then(function (success) {
// Convert to base64
var reader = new FileReader();
reader.readAsDataURL(success.data);
reader.onloadend = function() {
resolve(reader.result.split(',')[1]);
};
}, function (error) {
reject(error);
});
});
};
/*
* Returns a map from urls to arrays of font types used by PdfMake.
* E.g. if the font "regular" and bold" have the urls "fonts/myFont.ttf",
* the map fould be "fonts/myFont.ttf": ["OSFont-regular.ttf", "OSFont-bold.ttf"]
*/
var getUrlMapping = function () {
var urlMap = {};
var fonts = ['regular', 'italic', 'bold', 'bold_italic'];
_.forEach(fonts, function (font) {
var url = Fonts.getUrl('font_' + font);
if (!urlMap[url]) {
urlMap[url] = [];
}
urlMap[url].push('OSFont-' + font + '.ttf');
});
return urlMap;
};
/*
* Create the virtual filesystem needed by PdfMake for the fonts. Gets the url
* mapping and loads all fonts via get requests or the urlCache.
*/
var getVfs = function () {
return $q(function (resolve, reject) {
var vfs = {};
var urls = getUrlMapping();
var promises = _.chain(urls)
.map(function (filenames, url) {
if (urlCache[url]) {
// Just save the cache data into vfs.
_.forEach(filenames, function (filename) {
vfs[filename] = urlCache[url];
});
return false; // No promise here, it was all cached.
} else {
// Not in the cache, get the font and save the data into vfs.
return loadFont(url).then(function (data) {
urlCache[url] = data;
_.forEach(filenames, function (filename) {
vfs[filename] = data;
});
});
}
})
.filter(function (promise) {
return promise;
})
.value();
$q.all(promises).then(function () {
resolve(vfs);
});
});
};
return {
get: getVfs,
};
}
])
.factory('PdfCreate', [
'$timeout',
'$q',
'gettextCatalog',
'FileSaver',
'PdfVfs',
'Messaging',
function ($timeout, $q, gettextCatalog, FileSaver, Messaging) {
function ($timeout, $q, gettextCatalog, FileSaver, PdfVfs, Messaging) {
var filenameMessageMap = {};
var b64toBlob = function(b64Data) {
var byteCharacters = atob(b64Data);
@ -1105,40 +1191,46 @@ angular.module('OpenSlidesApp.core.pdf', [])
return {
getBase64FromDocument: function (pdfDocument) {
return $q(function (resolve, reject) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
pdfWorker.addEventListener('message', function (event) {
resolve(event.data);
PdfVfs.get().then(function (vfs) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
pdfWorker.addEventListener('message', function (event) {
resolve(event.data);
});
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
pdfWorker.postMessage(JSON.stringify({
pdfDocument: pdfDocument,
vfs: vfs,
}));
});
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
pdfWorker.postMessage(JSON.stringify({
pdfDocument: pdfDocument
}));
});
},
// Struckture of pdfDocuments: { filname1: doc, filename2: doc, ...}
getBase64FromMultipleDocuments: function (pdfDocuments) {
return $q(function (resolve, reject) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
var resultCount = 0;
var base64Map = {}; // Maps filename to base64
pdfWorker.addEventListener('message', function (event) {
resultCount++;
var data = JSON.parse(event.data);
base64Map[data.filename] = data.base64;
if (resultCount === _.keys(pdfDocuments).length) {
resolve(base64Map);
}
});
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
_.forEach(pdfDocuments, function (doc, filename) {
pdfWorker.postMessage(JSON.stringify({
filename: filename,
pdfDocument: doc
}));
PdfVfs.get().then(function (vfs) {
var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
var resultCount = 0;
var base64Map = {}; // Maps filename to base64
pdfWorker.addEventListener('message', function (event) {
resultCount++;
var data = JSON.parse(event.data);
base64Map[data.filename] = data.base64;
if (resultCount === _.keys(pdfDocuments).length) {
resolve(base64Map);
}
});
pdfWorker.addEventListener('error', function (event) {
reject(event);
});
_.forEach(pdfDocuments, function (doc, filename) {
pdfWorker.postMessage(JSON.stringify({
filename: filename,
pdfDocument: doc,
vfs: vfs,
}));
});
});
});
},

View File

@ -78,12 +78,13 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
}
])
.controller('LanguageCtrl', [
.controller('LanguageAndFontCtrl', [
'$scope',
'Languages',
'Config',
'ProjectorID',
function ($scope, Languages, Config, ProjectorID) {
'Fonts',
function ($scope, Languages, Config, ProjectorID, Fonts) {
// for the dynamic title
$scope.projectorId = ProjectorID();
@ -98,6 +99,18 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
}
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');
});
}
])

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ selectedLanguage }}" ng-controller="LanguageCtrl" class="no-js">
<html lang="{{ selectedLanguage }}" ng-controller="LanguageAndFontCtrl" class="no-js">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ selectedLanguage }}" ng-controller="LanguageCtrl" class="no-js">
<html lang="{{ selectedLanguage }}" ng-controller="LanguageAndFontCtrl" class="no-js">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">
@ -7,6 +7,34 @@
<link rel="stylesheet" href="static/css/openslides-libs.css">
<link rel="stylesheet" href="static/css/openslides-projector.css">
<link rel="icon" href="/static/img/favicon.png">
<style type="text/css">
@font-face {
font-family: 'OSFont';
src: {{ font }};
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OSFont Medium';
src: {{ font_medium }};
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OSFont Condensed';
src: {{ font_condensed }};
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: 'OSFont Condensed Light';
src: {{ font_condensed_light }};
font-weight: 100;
font-style: normal;
}
</style>
<script src="static/js/openslides-libs.js"></script>
<script src="static/js/openslides.js"></script>
<script src="static/js/openslides-templates.js"></script>

View File

@ -600,11 +600,12 @@ class ConfigViewSet(ModelViewSet):
# enabled.
result = self.request.user.is_authenticated() or anonymous_is_enabled()
elif self.action in ('partial_update', 'update'):
# The user needs 'core.can_manage_logos' for all config values
# starting with 'logo'. For all other config values th euser needs
# The user needs 'core.can_manage_logos_and_fonts' for all config values
# starting with 'logo' and 'font'. For all other config values th euser needs
# the default permissions 'core.can_manage_config'.
if self.kwargs['pk'].startswith('logo'):
result = has_perm(self.request.user, 'core.can_manage_logos')
pk = self.kwargs['pk']
if pk.startswith('logo') or pk.startswith('font'):
result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts')
else:
result = has_perm(self.request.user, 'core.can_manage_config')
else:

View File

@ -24,8 +24,9 @@ angular.module('OpenSlidesApp.mediafiles.list', [
'Mediafile',
'MediafileForm',
'Logos',
'Fonts',
function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, osTablePagination,
ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos) {
ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos, Fonts) {
$scope.$watch(function () {
return Mediafile.lastModified();
}, function () {
@ -259,21 +260,9 @@ angular.module('OpenSlidesApp.mediafiles.list', [
);
};
/** Logos **/
/** Logos and fonts **/
$scope.logos = Logos.getAll();
$scope.toggleLogo = function (mediafile, logo) {
if (!$scope.hasLogo(mediafile, logo)) {
Logos.setMediafile(logo.key, mediafile);
} else {
Logos.setMediafile(logo.key);
}
};
$scope.hasLogo = function (mediafile, logo) {
var allUrls = _.map(mediafile.getLogos(), function (logo) {
return logo.path;
});
return _.includes(allUrls, logo.path);
};
$scope.fonts = Fonts.getAll();
$scope.hasProjectorHeaderLogo = function (mediafile) {
return _.some(mediafile.getLogos(), function (logo) {
return logo.key === 'logo_projector_header';

View File

@ -13,7 +13,8 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
'gettext',
'jsDataModel',
'Logos',
function (DS, gettext, jsDataModel, Logos) {
'Fonts',
function (DS, gettext, jsDataModel, Logos, Fonts) {
var name = 'mediafiles/mediafile';
return DS.defineResource({
name: name,
@ -50,13 +51,60 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
});
},
isUsedAsLogo: function () {
return Logos.isMediafileUsedAsLogo(this);
var mediafile = this;
return _.find(Logos.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
},
canBeUsedAsLogo: function () {
return Logos.canMediafileBeUsedAsLogo(this);
return this.is_image;
},
getLogos: function () {
return Logos.getLogosForMediafile(this);
var mediafile = this;
return _.filter(Logos.getAll(), function (logoPlaceholder) {
return logoPlaceholder.path === mediafile.mediafileUrl;
});
},
hasLogo: function (logo) {
var allUrls = _.map(this.getLogos(), function (logo) {
return logo.path;
});
return _.includes(allUrls, logo.path);
},
toggleLogo: function (logo) {
if (this.hasLogo(logo)) {
Logos.set(logo.key);
} else {
Logos.set(logo.key, this.mediafileUrl);
}
},
isUsedAsFont: function () {
var mediafile = this;
return _.find(Fonts.getAll(), function (font) {
return font.path === mediafile.mediafileUrl;
});
},
canBeUsedAsFont: function () {
return this.is_font;
},
getFonts: function () {
var mediafile = this;
return _.filter(Fonts.getAll(), function (font) {
return font.path === mediafile.mediafileUrl;
});
},
hasFont: function (font) {
var allUrls = _.map(this.getFonts(), function (font) {
return font.path;
});
return _.includes(allUrls, font.path);
},
toggleFont: function (font) {
if (this.hasFont(font)) {
Fonts.set(font.key);
} else {
Fonts.set(font.key, this.mediafileUrl);
}
},
},
computed: {
@ -77,6 +125,11 @@ angular.module('OpenSlidesApp.mediafiles.resources', [
is_presentable: ['is_pdf', 'is_image', 'is_video', function (is_pdf, is_image, is_video) {
return (is_pdf && !this.mediafile.encrypted) || is_image || is_video;
}],
is_font: [function () {
var FONT_FILE_EXTENSIONS = ['ttf', 'woff'];
var ext = _.last(this.mediafile.name.split('.'));
return _.includes(FONT_FILE_EXTENSIONS, ext);
}],
mediafileUrl: [function () {
return this.media_url_prefix + this.mediafile.name;
}],

View File

@ -291,8 +291,12 @@
<div class="icon-column"> <!-- horizontal block -->
<i ng-style="{'visibility': mediafile.hidden ? 'visible' : 'hidden'}" class="fa fa-lock fa-lg"
title="{{ 'Is hidden' | translate }}"></i>
<i ng-style="{'visibility': mediafile.isUsedAsLogo() ? 'visible' : 'hidden'}" class="fa fa-picture-o fa-lg spacer-left"
title="{{ 'Is used as a logo' | translate }}" os-perms="core.can_manage_logos"></i>
<span os-perms="core.can_manage_logos_and_fonts" class="spacer-left">
<i ng-if="mediafile.isUsedAsLogo()" class="fa fa-picture-o fa-lg"
title="{{ 'Is used as a logo' | translate }}"></i>
<i ng-if="mediafile.isUsedAsFont()" class="fa fa-font fa-lg"
title="{{ 'Is used as a font' | translate }}"></i>
</span>
</div>
<div class="title-column">
<div> <!-- vertical block -->
@ -325,7 +329,7 @@
</div>
<div style="width: 40%;" class="pull-right optional">
<!-- Logo placeholder dropdown for manage user -->
<div os-perms="core.can_manage_logos"
<div os-perms="core.can_manage_logos_and_fonts"
ng-mouseover="mediafile.logoHover=true"
ng-mouseleave="mediafile.logoHover=false"
ng-show="mediafile.canBeUsedAsLogo()">
@ -354,14 +358,49 @@
</span>
<ul class="dropdown-menu" aria-labelledby="dropdownLogos{{ mediafile.id }}">
<li ng-repeat="logo in logos">
<a href ng-click="toggleLogo(mediafile, logo)">
<i class="fa fa-check" ng-if="hasLogo(mediafile, logo)"></i>
<a href ng-click="mediafile.toggleLogo(logo)">
<i class="fa fa-check" ng-if="mediafile.hasLogo(logo)"></i>
{{ logo.display_name | translate }}
</a>
</li>
</ul>
</span>
</div>
<!-- Font placeholder dropdown for manage user -->
<div os-perms="core.can_manage_logos_and_fonts"
ng-mouseover="mediafile.fontHover=true"
ng-mouseleave="mediafile.fontHover=false"
ng-show="mediafile.canBeUsedAsFont()">
<span uib-dropdown>
<span id="dropdownFont{{ mediafile.id }}" class="pointer nobr" uib-dropdown-toggle>
<span uib-tooltip="{{ 'Manage fonts' | translate }}" tooltip-class="nobr">
<span ng-if="!mediafile.isUsedAsFont()" ng-show="mediafile.hover">
<i class="fa fa-font"></i>
<i class="fa fa-plus"></i>
</span>
<span ng-if="mediafile.isUsedAsFont()">
<span ng-repeat="font in mediafile.getFonts()">
<i class="fa fa-font spacer-right"
ng-style="{'visibility': $first ? 'visible' : 'hidden'}"></i>
<small>
{{ font.display_name | translate }}<span ng-if="!$last">,</br></span>
</small>
</span>
</span>
</span>
<i class="fa fa-cog fa-lg spacer-left" ng-show="mediafile.fontHover && mediafile.isUsedAsFont()"></i>
</span>
<ul class="dropdown-menu" aria-labelledby="dropdownFonts{{ mediafile.id }}">
<li ng-repeat="font in fonts">
<a href ng-click="mediafile.toggleFont(font)">
<i class="fa fa-check" ng-if="mediafile.hasFont(font)"></i>
{{ font.display_name | translate }}
</a>
</li>
</ul>
</span>
</div>
</div>
</div>

View File

@ -39,7 +39,7 @@ def create_builtin_groups_and_admin(**kwargs):
'assignments.can_nominate_self',
'assignments.can_see',
'core.can_manage_config',
'core.can_manage_logos',
'core.can_manage_logos_and_fonts',
'core.can_manage_projector',
'core.can_manage_tags',
'core.can_manage_chat',
@ -116,7 +116,7 @@ def create_builtin_groups_and_admin(**kwargs):
permission_dict['core.can_see_frontpage'],
permission_dict['core.can_see_projector'],
permission_dict['core.can_manage_config'],
permission_dict['core.can_manage_logos'],
permission_dict['core.can_manage_logos_and_fonts'],
permission_dict['core.can_manage_projector'],
permission_dict['core.can_manage_tags'],
permission_dict['core.can_use_chat'],